Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think

We’ve discussed a lot about the internals of using CSS in this ongoing series on web components, but there are a few special pseudo-elements and pseudo-classes that, like good friends, willingly smell your possibly halitotic breath before you go talk to that potential love interest. You know, they help you out when you need it most. And, like a good friend will hand you a breath mint, these pseudo-elements and pseudo-classes provide you with some solutions both from within the web component and from outside the web component — the website where the web component lives.

I’m specifically referring to the ::part and ::slotted pseudo-elements, and the :defined, :host, and :host-context pseudo-classes. They give us extra ways to interact with web components. Let’s examine them closer.

Article series

The ::part pseudo-element

::part, in short, allows you to pierce the shadow tree, which is just my Lord-of-the-Rings-y way to say it lets you style elements inside the shadow DOM from outside the shadow DOM. In theory, you should encapsulate all of your styles for the shadow DOM within the shadow DOM, i.e. within a <style> element in your <template> element.

So, given something like this from the very first part of this series, where you have an <h2> in your <template>, your styles for that <h2> should all be in the <style> element.

<template id="zprofiletemplate">
  <style>
    h2 {
      font-size: 3em;
      margin: 0 0 0.25em 0;
      line-height: 0.8;
    }
    /* other styles */
  </style>
  <div class="profile-wrapper">
    <div class="info">
      <h2>
        <slot name="zombie-name">Zombie Bob</slot>
      </h2>
      <!-- other zombie profile info -->
    </div>
</template>

But sometimes we might need to style an element in the shadow DOM based on information that exists on the page. For instance, let’s say we have a page for each zombie in the undying love system with matches. We could add a class to profiles based on how close of a match they are. We could then, for instance, highlight a match’s name if he/she/it is a good match. The closeness of a match would vary based on whose list of potential matches is being shown and we won’t know that information until we’re on that page, so we can’t bake the functionality into the web component. Since the <h2> is in the shadow DOM, though, we can’t access or style it from outside the shadow DOM meaning a selector of zombie-profile h2 on the matches page won’t work.

But, if we make a slight adjustment to the <template> markup by adding a part attribute to the <h2>:

<template id="zprofiletemplate">
  <style>
    h2 {
      font-size: 3em;
      margin: 0 0 0.25em 0;
      line-height: 0.8;
    }
    /* other styles */
  </style>
  <div class="profile-wrapper">
    <div class="info">
      <h2 part="zname">
        <slot name="zombie-name">Zombie Bob</slot>
      </h2>
      <!-- other zombie profile info -->
    </div>
</template>

Like a spray of Bianca in the mouth, we now have the superpowers to break through the shadow DOM barrier and style those elements from outside of the <template>:

/* External stylesheet */
.high-match::part(zname) {
  color: blue;
}
.medium-match::part(zname) {
  color: navy;
}
.low-match::part(zname) {
  color: slategray;
}

There are lots of things to consider when it comes to using CSS ::part. For example, styling an element inside of a part is a no-go:

/* frowny-face emoji */
.high-match::part(zname) span { ... }

But you can add a part attribute on that element and style it via its own part name.

What happens if we have a web component inside another web component, though? Will ::part still work? If the web component appears in the page’s markup, i.e. you’re slotting it in, ::part works just fine from the main page’s CSS.

<zombie-profile class="high-match">
  <img slot="profile-image" src="https://assets.codepen.io/1804713/leroy.png" />
  <span slot="zombie-name">Leroy</span>
  <zombie-details slot="zdetails">
    <!-- Leroy's details -->
  </zombie-details>
</zombie-profile>

But if the web component is in the template/shadow DOM, then ::part cannot pierce both shadow trees, just the first one. We need to bring the ::part into the light… so to speak. We can do that with an exportparts attribute.

To demonstrate this we’ll add a “watermark” behind the profiles using a web component. (Why? Believe it or not this was the least contrived example I could come up with.) Here are our templates: (1) the template for <zombie-watermark>, and (2) the same template for <zombie-profile> but with added a <zombie-watermark> element on the end.

<template id="zwatermarktemplate">
  <style>
    div {
    text-transform: uppercase;
      font-size: 2.1em;
      color: rgb(0 0 0 / 0.1);
      line-height: 0.75;
      letter-spacing: -5px;
    }
    span {
      color: rgb( 255 0 0 / 0.15);
    }
  </style>
  <div part="watermark">
    U n d y i n g  L o v e  U n d y i n g  L o v e  U n d y i n g  L o v e  <span part="copyright">©2 0 2 7 U n d y i n g  L o v e  U n L t d .</span>
  <!-- Repeat this a bunch of times so we can cover the background of the profile -->
  </div> 
</template>
<template id="zprofiletemplate">
  <style>
    ::part(watermark) {
      color: rgb( 0 0 255 / 0.1);
    }
    /* More styles */
  </style>
  <!-- zombie-profile markup -->
  <zombie-watermark exportparts="copyright"></zombie-watermark>
</template>
<style>
  /* External styles */
  ::part(copyright) {
    color: rgb( 0 100 0 / 0.125);
  }
</style>

Since ::part(watermark) is only one shadow DOM above the <zombie-watermark>, it works fine from within the <zombie-profile>’s template styles. Also, since we’ve used exportparts="copyright" on the <zombie-watermark>, the copyright part has been pushed up into the <zombie-profile>‘s shadow DOM and ::part(copyright) now works even in external styles, but ::part(watermark) will not work outside the <zombie-profile>’s template.

We can also forward and rename parts with that attribute:

<zombie-watermark exportparts="copyright: cpyear"></zombie-watermark>
/* Within zombie-profile's shadow DOM */

/* happy-face emoji */
::part(cpyear) { ... }

/* frowny-face emoji */
::part(copyright) { ... }

Structural pseudo-classes (:nth-child, etc.) don’t work on parts either, but you can use pseudo-classes like :hover. Let’s animate the high match names a little and make them shake as they’re lookin’ for some lovin’. Okay, I heard that and agree it’s awkward. Let’s… uh… make them more, shall we say, noticeable, with a little movement.

.high::part(name):hover {
  animation: highmatch 1s ease-in-out;
}

The ::slotted pseudo-element

The ::slotted CSS pseudo-element actually came up when we covered interactive web components. The basic idea is that ::slotted represents any content in a slot in a web component, i.e. the element that has the slot attribute on it. But, where ::part pierces through the shadow DOM to make a web component’s elements accessible to outside styles, ::slotted remains encapsulated in the <style> element in the component’s <template> and accesses the element that’s technically outside the shadow DOM.

In our <zombie-profile> component, for example, each profile image is inserted into the element through the slot="profile-image".

<zombie-profile>
  <img slot="profile-image" src="photo.jpg" /> 
  <!-- rest of the content -->
</zombie-profile>

That means we can access that image — as well as any image in any other slot — like this:

::slotted(img) {
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 1em 0 0;
}

Similarly, we could select all slots with ::slotted(*) regardless of what element it is. Just beware that ::slotted has to select an element — text nodes are immune to ::slotted zombie styles. And children of the element in the slot are inaccessible.

The :defined pseudo-class

:defined matches all defined elements (I know, surprising, right?), both built-in and custom. If your custom element is shuffling along like a zombie avoiding his girlfriend’s dad’s questions about his “living” situation, you may not want the corpses of the content to show while you’re waiting for them to come back to life errr… load.

You can use the :defined pseudo-class to hide a web component before it’s available — or “defined” — like this:

:not(:defined) {
  display: none;
}

You can see how :defined acts as a sort of mint in the mouth of our component styles, preventing any broken content from showing (or bad breath from leaking) while the page is still loading. Once the element’s defined, it’ll automatically appear because it’s now, you know, defined and not not defined.

I added a setTimeout of five seconds to the web component in the following demo. That way, you can see that <zombie-profile> elements are not shown while they are undefined. The <h1> and the <div> that holds the <zombie-profile> components are still there. It’s just the <zombie-profile> web component that gets display: none since they are not yet defined.

The :host pseudo-class

Let’s say you want to make styling changes to the custom element itself. While you could do this from outside the custom element (like tightening that N95), the result would not be encapsulated, and additional CSS would have to be transferred to wherever this custom element is placed.

It’d be very convenient then to have a pseudo-class that can reach outside the shadow DOM and select the shadow root. That CSS pseudo-class is :host.

In previous examples throughout this series, I set the <zombie-profile> width from the main page’s CSS, like this:

zombie-profile {
  width: calc(50% - 1em);
}

With :host, however, I can set that width from inside the web component, like this:

:host {
  width: calc(50% - 1em);
}

In fact, there was a div with a class of .profile-wrapper in my examples that I can now remove because I can use the shadow root as my wrapper with :host. That’s a nice way to slim down the markup.

You can do descendant selectors from the :host, but only descendants inside the shadow DOM can be accessed — nothing that’s been slotted into your web component (without using ::slotted).

Showing the parts of the HTML that are relevant to the :host pseudo-element.

That said, :host isn’t a one trick zombie. It can also take a parameter, e.g. a class selector, and will only apply styling if the class is present.

:host(.high) {
  border: 2px solid blue;
}

This allows you to make changes should certain classes be added to the custom element.

You can also pass pseudo-classes in there, like :host(:last-child) and :host(:hover).

The :host-context pseudo-class

Now let’s talk about :host-context. It’s like our friend :host(), but on steroids. While :host gets you the shadow root, it won’t tell you anything about the context in which the custom element lives or its parent and ancestor elements.

:host-context, on the other hand, throws the inhibitions to the wind, allowing you to follow the DOM tree up the rainbow to the leprechaun in a leotard. Just note that at the time I’m writing this, :host-context is unsupported in Firefox or Safari. So use it for progressive enhancement.

Here’s how it works. We’ll split our list of zombie profiles into two divs. The first div will have all of the high zombie matches with a .bestmatch class. The second div will hold all the medium and low love matches with a .worstmatch class.

<div class="profiles bestmatch">
  <zombie-profile class="high">
    <!-- etc. -->
  </zombie-profile>
  <!-- more profiles -->
</div>

<div class="profiles worstmatch">
  <zombie-profile class="medium">
    <!-- etc. -->
  </zombie-profile>
  <zombie-profile class="low">
    <!-- etc. -->
  </zombie-profile>
  <!-- more profiles -->
</div>

Let’s say we want to apply different background colors to the .bestmatch and .worstmatch classes. We are unable to do this with just :host:

:host(.bestmatch) {
  background-color: #eef;
}
:host(.worstmatch) {
  background-color: #ddd;
}

That’s because our best and worst match classes are not on our custom elements. What we want is to be able to select the profiles’s parent elements from within the shadow DOM. :host-context pokes past the custom element to match the, er, match classes we want to style.

:host-context(.bestmatch) {
  background-color: #eef;
}
:host-context(.worstmatch) {
  background-color: #ddd;
}

Well, thanks for hanging out despite all the bad breath. (I know you couldn’t tell, but above when I was talking about your breath, I was secretly talking about my breath.)

How would you use ::part, ::slotted, :defined, :host, and :host-context in your web component? Let me know in the comments. (Or if you have cures to chronic halitosis, my wife would be very interested in to hear more.)


Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think originally published on CSS-Tricks. You should get the newsletter.

Context-Aware Web Components Are Easier Than You Think

Another aspect of web components that we haven’t talked about yet is that a JavaScript function is called whenever a web component is added or removed from a page. These lifecycle callbacks can be used for many things, including making an element aware of its context.

Article series

The four lifecycle callbacks of web components

There are four lifecycle callbacks that can be used with web components:

  • connectedCallback: This callback fires when the custom element is attached to the element.
  • disconnectedCallback: This callback fires when the element is removed from the document.
  • adoptedCallback: This callback fires when the element is added to a new document.
  • attributeChangedCallback: This callback fires when an attribute is changed, added or removed, as long as that attribute is being observed.

Let’s look at each of these in action.

Our post-apocalyptic person component

Two renderings of the web component side-by-side, the left is a human, and the right is a zombie.

We’ll start by creating a web component called <postapocalyptic-person>. Every person after the apocalypse is either a human or a zombie and we’ll know which one based on a class — either .human or .zombie — that’s applied to the parent element of the <postapocalyptic-person> component. We won’t do anything fancy with it (yet), but we’ll add a shadowRoot we can use to attach a corresponding image based on that classification.

customElements.define(
  "postapocalyptic-person",
  class extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: "open" });
    }
}

Our HTML looks like this:

<div class="humans">
  <postapocalyptic-person></postapocalyptic-person>
</div>
<div class="zombies">
  <postapocalyptic-person></postapocalyptic-person>
</div>

Inserting people with connectedCallback

When a <postapocalyptic-person> is loaded on the page, the connectedCallback() function is called.

connectedCallback() {
  let image = document.createElement("img");
  if (this.parentNode.classList.contains("humans")) {
    image.src = "https://assets.codepen.io/1804713/lady.png";
    this.shadowRoot.appendChild(image);
  } else if (this.parentNode.classList.contains("zombies")) {
    image.src = "https://assets.codepen.io/1804713/ladyz.png";
    this.shadowRoot.appendChild(image);
  }
}

This makes sure that an image of a human is output when the <postapocalyptic-person> is a human, and a zombie image when the component is a zombie.

Be careful working with connectedCallback. It runs more often than you might realize, firing any time the element is moved and could (baffling-ly) even run after the node is no longer connected — which can be an expensive performance cost. You can use this.isConnected to know whether the element is connected or not.

Counting people with connectedCallback() when they are added

Let’s get a little more complex by adding a couple of buttons to the mix. One will add a <postapocalyptic-person>, using a “coin flip” approach to decide whether it’s a human or a zombie. The other button will do the opposite, removing a <postapocalyptic-person> at random. We’ll keep track of how many humans and zombies are in view while we’re at it.

<div class="btns">
  <button id="addbtn">Add Person</button>
  <button id="rmvbtn">Remove Person</button> 
  <span class="counts">
    Humans: <span id="human-count">0</span> 
    Zombies: <span id="zombie-count">0</span>
  </span>
</div>

Here’s what our buttons will do:

let zombienest = document.querySelector(".zombies"),
  humancamp = document.querySelector(".humans");

document.getElementById("addbtn").addEventListener("click", function () {
  // Flips a "coin" and adds either a zombie or a human
  if (Math.random() > 0.5) {
    zombienest.appendChild(document.createElement("postapocalyptic-person"));
  } else {
    humancamp.appendChild(document.createElement("postapocalyptic-person"));
  }
});
document.getElementById("rmvbtn").addEventListener("click", function () {
  // Flips a "coin" and removes either a zombie or a human
  // A console message is logged if no more are available to remove.
  if (Math.random() > 0.5) {
    if (zombienest.lastElementChild) {
      zombienest.lastElementChild.remove();
    } else {
      console.log("No more zombies to remove");
    }
  } else {
    if (humancamp.lastElementChild) {
      humancamp.lastElementChild.remove();
    } else {
      console.log("No more humans to remove");
    }
  }
});

Here’s the code in connectedCallback() that counts the humans and zombies as they are added:

connectedCallback() {
  let image = document.createElement("img");
  if (this.parentNode.classList.contains("humans")) {
    image.src = "https://assets.codepen.io/1804713/lady.png";
    this.shadowRoot.appendChild(image);
    // Get the existing human count.
    let humancount = document.getElementById("human-count");
    // Increment it
    humancount.innerHTML = parseInt(humancount.textContent) + 1;
  } else if (this.parentNode.classList.contains("zombies")) {
    image.src = "https://assets.codepen.io/1804713/ladyz.png";
    this.shadowRoot.appendChild(image);
    // Get the existing zombie count.
    let zombiecount = document.getElementById("zombie-count");
    // Increment it
    zombiecount.innerHTML = parseInt(zombiecount.textContent) + 1;
  }
}

Updating counts with disconnectedCallback

Next, we can use disconnectedCallback() to decrement the number as a humans and zombies are removed. However, we are unable to check the class of the parent element because the parent element with the corresponding class is already gone by the time disconnectedCallback is called. We could set an attribute on the element, or add a property to the object, but since the image’s src attribute is already determined by its parent element, we can use that as a proxy for knowing whether the web component being removed is a human or zombie.

disconnectedCallback() {
  let image = this.shadowRoot.querySelector('img');
  // Test for the human image
  if (image.src == "https://assets.codepen.io/1804713/lady.png") {
    let humancount = document.getElementById("human-count");
    humancount.innerHTML = parseInt(humancount.textContent) - 1; // Decrement count
  // Test for the zombie image
  } else if (image.src == "https://assets.codepen.io/1804713/ladyz.png") {
    let zombiecount = document.getElementById("zombie-count");
    zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1; // Decrement count
  }
}

Beware of clowns!

Now (and I’m speaking from experience here, of course) the only thing scarier than a horde of zombies bearing down on your position is a clown — all it takes is one! So, even though we’re already dealing with frightening post-apocalyptic zombies, let’s add the possibility of a clown entering the scene for even more horror. In fact, we’ll do it in such a way that there’s a possibility any human or zombie on the screen is secretly a clown in disguise!

I take back what I said earlier: a single zombie clown is scarier than even a group of “normal” clowns. Let’s say that if any sort of clown is found — be it human or zombie — we separate them from the human and zombie populations by sending them to a whole different document — an <iframe> jail, if you will. (I hear that “clowning” may be even more contagious than zombie contagion.)

And when we move a suspected clown from the current document to an <iframe>, it doesn’t destroy and recreate the original node; rather it adopts and connects said node, first calling adoptedCallback then connectedCallback.

We don’t need anything in the <iframe> document except a body with a .clowns class. As long as this document is in the iframe of the main document — not viewed separately — we don’t even need the <postapocalyptic-person> instantiation code. We’ll include one space for humans, another space for zombies, and yes, the clowns’s jail… errr… <iframe> of… fun.

<div class="btns">
  <button id="addbtn">Add Person</button>
  <button id="jailbtn">Jail Potential Clown</button>
</div>
<div class="humans">
  <postapocalyptic-person></postapocalyptic-person>
</div>
<div class="zombies">
  <postapocalyptic-person></postapocalyptic-person>
</div>
<iframe class="clowniframeoffun” src="adoptedCallback-iframe.html">
</iframe>

Our “Add Person” button works the same as it did in the last example: it flips a digital coin to randomly insert either a human or a zombie. When we hit the “Jail Potential Clown” button another coin is flipped and takes either a zombie or a human, handing them over to <iframe> jail.

document.getElementById("jailbtn").addEventListener("click", function () {
  if (Math.random() > 0.5) {
    let human = humancamp.querySelector('postapocalyptic-person');
    if (human) {
      clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(human));
    } else {
      console.log("No more potential clowns at the human camp");
    }
  } else {
    let zombie = zombienest.querySelector('postapocalyptic-person');
    if (zombie) {
      clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(zombie));
    } else {
      console.log("No more potential clowns at the zombie nest");
    }
  }
});

Revealing clowns with adoptedCallback

In the adoptedCallback we’ll determine whether the clown is of the zombie human variety based on their corresponding image and then change the image accordingly. connectedCallback will be called after that, but we don’t have anything it needs to do, and what it does won’t interfere with our changes. So we can leave it as is.

adoptedCallback() {
  let image = this.shadowRoot.querySelector("img");
  if (this.parentNode.dataset.type == "clowns") {
    if (image.src.indexOf("lady.png") != -1) { 
      // Sometimes, the full URL path including the domain is saved in `image.src`.
      // Using `indexOf` allows us to skip the unnecessary bits. 
      image.src = "ladyc.png";
      this.shadowRoot.appendChild(image);
    } else if (image.src.indexOf("ladyz.png") != -1) {
      image.src = "ladyzc.png";
      this.shadowRoot.appendChild(image);
    }
  }
}

Detecting hidden clowns with attributeChangedCallback

Finally, we have the attributeChangedCallback. Unlike the the other three lifecycle callbacks, we need to observe the attributes of our web component in order for the the callback to fire. We can do this by adding an observedAttributes() function to the custom element’s class and have that function return an array of attribute names.

static get observedAttributes() {
  return [“attribute-name”];
}

Then, if that attribute changes — including being added or removed — the attributeChangedCallback fires.

Now, the thing you have to worry about with clowns is that some of the humans you know and love (or the ones that you knew and loved before they turned into zombies) could secretly be clowns in disguise. I’ve set up a clown detector that looks at a group of humans and zombies and, when you click the “Reveal Clowns” button, the detector will (through completely scientific and totally trustworthy means that are not based on random numbers choosing an index) apply data-clown="true" to the component. And when this attribute is applied, attributeChangedCallback fires and updates the component’s image to uncover their clownish colors.

I should also note that the attributeChangedCallback takes three parameters:

  • the name of the attribute
  • the previous value of the attribute
  • the new value of the attribute

Further, the callback lets you make changes based on how much the attribute has changed, or based on the transition between two states.

Here’s our attributeChangedCallback code:

attributeChangedCallback(name, oldValue, newValue) {
  let image = this.shadowRoot.querySelector("img");
  // Ensures that `data-clown` was the attribute that changed,
  // that its value is true, and that it had an image in its `shadowRoot`
  if (name="data-clown" && this.dataset.clown && image) {
    // Setting and updating the counts of humans, zombies,
    // and clowns on the page
    let clowncount = document.getElementById("clown-count"),
    humancount = document.getElementById("human-count"),
    zombiecount = document.getElementById("zombie-count");
    if (image.src.indexOf("lady.png") != -1) {
      image.src = "https://assets.codepen.io/1804713/ladyc.png";
      this.shadowRoot.appendChild(image);
      // Update counts
      clowncount.innerHTML = parseInt(clowncount.textContent) + 1;
      humancount.innerHTML = parseInt(humancount.textContent) - 1;
    } else if (image.src.indexOf("ladyz.png") != -1) {
      image.src = "https://assets.codepen.io/1804713/ladyzc.png";
      this.shadowRoot.appendChild(image);
      // Update counts
      clowncount.innerHTML = parseInt(clowncount.textContent) + 1;
      zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1;
    }
  }
}

And there you have it! Not only have we found out that web component callbacks and creating context-aware custom elements are easier than you may have thought, but detecting post-apocalyptic clowns, though terrifying, is also easier that you thought. What kind of devious, post-apocalyptic clowns can you detect with these web component callback functions?


Context-Aware Web Components Are Easier Than You Think originally published on CSS-Tricks. You should get the newsletter and become a supporter.

Supercharging Built-In Elements With Web Components “is” Easier Than You Think

We’ve already discussed how creating web components is easier than you think, but there’s another aspect of the specification that we haven’t discussed yet and it’s a way to customize (nay, supercharge) a built-in element. It’s similar to creating fully custom or “autonomous” elements — like the <zombie-profile> element from the previous articles—but requires a few differences.

Customized built-in elements use an is attribute to tell the browser that this built-in element is no mild-mannered, glasses-wearing element from Kansas, but is, in fact, the faster than a speeding bullet, ready to save the world, element from planet Web Component. (No offense intended, Kansans. You’re super too.)

Supercharging a mild-mannered element not only gives us the benefits of the element’s formatting, syntax, and built-in features, but we also get an element that search engines and screen readers already know how to interpret. The screen reader has to guess what’s going on in a <my-func> element, but has some idea of what’s happening in a <nav is="my-func"> element. (If you have func, please, for the love of all that is good, don’t put it in an element. Think of the children.)

It’s important to note here that Safari (and a handful of more niche browsers) only support autonomous elements and not these customized built-in elements. We’ll discuss polyfills for that later.

Until we get the hang of this, let’s start by rewriting the <apocalyptic-warning> element we created back in our first article as a customized built-in element. (The code is also available in the CodePen demo.)

The changes are actually fairly simple. Instead of extending the generic HTMLElement, we’ll extend a specific element, in this case the <div> element which has the class HTMLDivElement. We’ll also add a third argument to the customElements.defines function: {extends: 'div'}.

customElements.define(
  "apocalyptic-warning",
  class ApocalypseWarning extends HTMLDivElement {
    constructor() {
      super();
      let warning = document.getElementById("warningtemplate");
      let mywarning = warning.content;

      const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(
        mywarning.cloneNode(true)
      );
    }
  },
  { extends: "div" }
);

Lastly, we’ll update our HTML from <apocalyptic-warning> tags to <div> tags that include an is attribute set to “apocalyptic-warning” like this:

<div is="apocalyptic-warning">
  <span slot="whats-coming">Undead</span>
</div>

Reminder: If you’re looking at the below in Safari, you won’t see any beautiful web component goodness *shakes fist at Safari*

Only certain elements can have a shadow root attached to them. Some of this is because attaching a shadow root to, say, an <a> element or <form> element could have security implications. The list of available elements is mostly layout elements, such as <article>, <section>, <aside>, <main>, <header>, <div>, <nav>, and <footer>, plus text-related elements like <p>, <span>, <blockquote>, and <h1><h6>. Last but not least, we also get the body element and any valid autonomous custom element.

Adding a shadow root is not the only thing we can do to create a web component. At its base, a web component is a way to bake functionality into an element and we don’t need additional markup in the shadows to do that. Let’s create an image with a built-in light box feature to illustrate the point.

We’ll take a normal <img> element and add two attributes: first, the is attribute that signifies this <img> is a customized built-in element; and a data attribute that holds the path to the larger image that we’ll show in the light box. (Since I’m using an SVG, I just used the same URL, but you could easily have a smaller raster image embedded in the site and a larger version of it in the light box.)

<img is="light-box" src="https://assets.codepen.io/1804713/ninja2.svg" data-lbsrc="https://assets.codepen.io/1804713/ninja2.svg" alt="Silent but Undeadly Zombie Ninja" />

Since we can’t do a shadow DOM for this <img>, there’s no need for a <template> element, <slot> elements, or any of those other things. We also won’t have any encapsulated styles.

So, let’s skip straight to the JavaScript:

customElements.define(
  "light-box",
  class LightBox extends HTMLImageElement {
    constructor() {
      super();
      // We’re creating a div element to use as the light box. We’ll eventually insert it just before the image in question.
      let lb = document.createElement("div");
      // Since we can’t use a shadow DOM, we can’t encapsulate our styles there. We could add these styles to the main CSS file, but they could bleed out if we do that, so I’m setting all styles for the light box div right here
      lb.style.display = "none";
      lb.style.position = "absolute";
      lb.style.height = "100vh";
      lb.style.width = "100vw";
      lb.style.top = 0;
      lb.style.left = 0;
      lb.style.background =
        "rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";
      lb.style.backgroundSize = "contain";

      lb.addEventListener("click", function (evt) {
        // We’ll close our light box by clicking on it
        this.style.display = "none";
      });
      this.parentNode.insertBefore(lb, this); // This inserts the light box div right before the image
      this.addEventListener("click", function (evt) {
        // Opens the light box when the image is clicked.
        lb.style.display = "block";
      });
    }
  },
  { extends: "img" }
);

Now that we know how customized built-in elements work, we need to move toward ensuring they’ll work everywhere. Yes, Safari, this stink eye is for you.

WebComponents.org has a generalized polyfill that handles both customized built-in elements and autonomous elements, but because it can handle so much, it may be a lot more than you need, particularly if all you’re looking to do is support customized built-in elements in Safari.

Since Safari supports autonomous custom elements, we can swap out the <img> with an autonomous custom element such as <lightbox-polyfill>. “This will be like two lines of code!” the author naively said to himself. Thirty-seven hours of staring at a code editor, two mental breakdowns, and a serious reevaluation of his career path later, he realized that he’d need to start typing if he wanted to write those two lines of code. It also ended up being more like sixty lines of code (but you’re probably good enough to do it in like ten lines).

The original code for the light box can mostly stand as-is (although we’ll add a new autonomous custom element shortly), but it needs a few small adjustments. Outside the definition of the custom element, we need to set a Boolean.

let customBuiltInElementsSupported = false;

Then within the LightBox constructor, we set the Boolean to true. If customized built-in elements aren’t supported, the constructor won’t run and the Boolean won’t be set to true; thus we have a direct test for whether customized built-in elements are supported.

Before we use that test to replace our customized built-in element, we need to create an autonomous custom element to be used as a polyfill, namely <lightbox-polyfill>.

customElements.define(
  "lightbox-polyfill", // We extend the general HTMLElement instead of a specific one
  class LightBoxPoly extends HTMLElement { 
    constructor() {
      super();

      // This part is the same as the customized built-in element’s constructor
      let lb = document.createElement("div");
      lb.style.display = "none";
      lb.style.position = "absolute";
      lb.style.height = "100vh";
      lb.style.width = "100vw";
      lb.style.top = 0;
      lb.style.left = 0;
      lb.style.background =
        "rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";
      lb.style.backgroundSize = "contain";

      // Here’s where things start to diverge. We add a `shadowRoot` to the autonomous custom element because we can’t add child nodes directly to the custom element in the constructor. We could use an HTML template and slots for this, but since we only need two elements, it's easier to just create them in JavaScript.
      const shadowRoot = this.attachShadow({ mode: "open" });

      // We create an image element to display the image on the page
      let lbpimg = document.createElement("img");

      // Grab the `src` and `alt` attributes from the autonomous custom element and set them on the image
      lbpimg.setAttribute("src", this.getAttribute("src"));
      lbpimg.setAttribute("alt", this.getAttribute("alt"));

      // Add the div and the image to the `shadowRoot`
      shadowRoot.appendChild(lb);
      shadowRoot.appendChild(lbpimg);

      // Set the event listeners so that you show the div when the image is clicked, and hide the div when the div is clicked.
      lb.addEventListener("click", function (evt) {
        this.style.display = "none";
      });
      lbpimg.addEventListener("click", function (evt) {
        lb.style.display = "block";
      });
    }
  }
);

Now that we have the autonomous element ready, we need some code to replace the customized <img> element when it’s unsupported in the browser.

if (!customBuiltInElementsSupported) {
  // Select any image with the `is` attribute set to `light-box`
  let lbimgs = document.querySelectorAll('img[is="light-box"]');
  for (let i = 0; i < lbimgs.length; i++) { // Go through all light-box images
    let replacement = document.createElement("lightbox-polyfill"); // Create an autonomous custom element

    // Grab the image and div from the `shadowRoot` of the new lighbox-polyfill element and set the attributes to those originally on the customized image, and set the background on the div.
    replacement.shadowRoot.querySelector("img").setAttribute("src", lbimgs[i].getAttribute("src"));
    replacement.shadowRoot.querySelector("img").setAttribute("alt", lbimgs[i].getAttribute("alt"));
    replacement.shadowRoot.querySelector("div").style.background =
      "rgba(0,0,0, 0.7) url(" + lbimgs[i].dataset.lbsrc + ") no-repeat center";

    // Stick the new lightbox-polyfill element into the DOM just before the image we’re replacing
    lbimgs[i].parentNode.insertBefore(replacement, lbimgs[i]);
    // Remove the customized built-in image
    lbimgs[i].remove();
  }
}

So there you have it! We not only built autonomous custom elements, but customized built-in elements as well — including how to make them work in Safari. And we get all the benefits of structured, semantic HTML elements to boot including giving screen readers and search engines an idea of what these custom elements are.

Go forth and customize yon built-in elements with impunity!


The post Supercharging Built-In Elements With Web Components “is” Easier Than You Think appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Using Web Components in WordPress is Easier Than You Think

Now that we’ve seen that web components and interactive web components are both easier than you think, let’s take a look at adding them to a content management system, namely WordPress.

There are three major ways we can add them. First, through manual input into the siteputting them directly into widgets or text blocks, basically anywhere we can place other HTML. Second, we can add them as the output of a theme in a theme file. And, finally, we can add them as the output of a custom block.

Loading the web component files

Now whichever way we end up adding web components, there’s a few things we have to ensure:

  1. our custom element’s template is available when we need it,
  2. any JavaScript we need is properly enqueued, and
  3. any un-encapsulated styles we need are enqueued.

We’ll be adding the <zombie-profile> web component from my previous article on interactive web components. Check out the code over at CodePen.

Let’s hit that first point. Once we have the template it’s easy enough to add that to the WordPress theme’s footer.php file, but rather than adding it directly in the theme, it’d be better to hook into wp_footer so that the component is loaded independent of the footer.php file and independent of the overall theme— assuming that the theme uses wp_footer, which most do. If the template doesn’t appear in your theme when you try it, double check that wp_footer is called in your theme’s footer.php template file.

<?php function diy_ezwebcomp_footer() { ?>
  <!-- print/echo Zombie profile template code. -->
  <!-- It's available at https://codepen.io/undeadinstitute/pen/KKNLGRg -->
<?php } 
add_action( 'wp_footer', 'diy_ezwebcomp_footer');

Next is to enqueue our component’s JavaScript. We can add the JavaScript via wp_footer as well, but enqueueing is the recommended way to link JavaScript to WordPress. So let’s put our JavaScript in a file called ezwebcomp.js (that name is totally arbitrary), stick that file in the theme’s JavaScript directory (if there is one), and enqueue it (in the functions.php file).

wp_enqueue_script( 'ezwebcomp_js', get_template_directory_uri() . '/js/ezwebcomp.js', '', '1.0', true );

We’ll want to make sure that last parameter is set to true , i.e. it loads the JavaScript before the closing body tag. If we load it in the head instead, it won’t find our HTML template and will get super cranky (throw a bunch of errors.)

If you can fully encapsulate your web component, then you can skip this next step. But if you (like me) are unable to do it, you’ll need to enqueue those un-encapsulated styles so that they’re available wherever the web component is used. (Similar to JavaScript, we could add this directly to the footer, but enqueuing the styles is the recommended way to do it). So we’ll enqueue our CSS file:

wp_enqueue_style( 'ezwebcomp_style', get_template_directory_uri() . '/ezwebcomp.css', '', '1.0', 'screen' );

That wasn’t too tough, right? And if you don’t plan to have any users other than Administrators use it, you should be all set for adding these wherever you want them. But that’s not always the case, so we’ll keep moving ahead!

Don’t filter out your web component

WordPress has a few different ways to both help users create valid HTML and prevent your Uncle Eddie from pasting that “hilarious” picture he got from Shady Al directly into the editor (complete with scripts to pwn every one of your visitors).

So when adding web-components directly into blocks or widgets, we’ll need to be careful about WordPress’s built-in code filtering . Disabling it all together would let Uncle Eddie (and, by extension, Shady Al) run wild, but we can modify it to let our awesome web component through the gate that (thankfully) keeps Uncle Eddie out.

First, we can use the wp_kses_allowed filter to add our web component to the list of elements not to filter out. It’s sort of like we’re whitelisting the component, and we do that by adding it to the the allowed tags array that’s passed to the filter function.

function add_diy_ezwebcomp_to_kses_allowed( $the_allowed_tags ) {
  $the_allowed_tags['zombie-profile'] = array();
}
add_filter( 'wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed');

We’re adding an empty array to the <zombie-profile> component because WordPress filters out attributes in addition to elements—which brings us to another problem: the slot attribute (as well as part and any other web-component-ish attribute you might use) are not allowed by default. So, we have to explitcly allow them on every element on which you anticipate using them, and, by extension, any element your user might decide to add them to. (Wait, those element lists aren’t the same even though you went over it six times with each user… who knew?) Thus, below I have set slot to true on <span>, <img> and <ul>, the three elements I’m putting into slots in the <zombie-profile> component. (I also set part to true on span elements so that I could let that attribute through too.)

function add_diy_ezwebcomp_to_kses_allowed( $the_allowed_tags ) {
  $the_allowed_tags['zombie-profile'] = array();
  $the_allowed_tags\['span'\]['slot'] = true;
  $the_allowed_tags\['span'\]['part'] = true;
  $the_allowed_tags\['ul'\]['slot'] = true;
  $the_allowed_tags\['img'\]['slot'] = true;
  return $the_allowed_tags;
}
add_filter( 'wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed');

We could also enable the slot (and part) attribute in all allowed elements with something like this:

function add_diy_ezwebcomp_to_kses_allowed($the_allowed_tags) {
  $the_allowed_tags['zombie-profile'] = array();
  foreach ($the_allowed_tags as &$tag) {
    $tag['slot'] = true;
    $tag['part'] = true;
  }
  return $the_allowed_tags;
}
add_filter('wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed');

Sadly, there is one more possible wrinkle with this. You may not run into this if all the elements you’re putting in your slots are inline/phrase elements, but if you have a block level element to put into your web component, you’ll probably get into a fistfight with the block parser in the Code Editor. You may be a better fist fighter than I am, but I always lost.

The code editor is an option that allows you to inspect and edit the markup for a block.

For reasons I can’t fully explain, the client-side parser assumes that the web component should only have inline elements within it, and if you put a <ul> or <div>, <h1> or some other block-level element in there, it’ll move the closing web component tag to just after the last inline/phrase element. Worse yet, according to a note in the WordPress Developer Handbook, it’s currently “not possible to replace the client-side parser.”

While this is frustrating and something you’ll have to train your web editors on, there is a workaround. If we put the web component in a Custom HTML block directly in the Block Editor, the client-side parser won’t leave us weeping on the sidewalk, rocking back and forth, and questioning our ability to code… Not that that’s ever happened to anyone… particularly not people who write articles…

Component up the theme

Outputting our fancy web component in our theme file is straightforward as long as it isn’t updated outside the HTML block. We add it the way we would add it in any other context, and, assuming we have the template, scripts and styles in place, things will just work.

But let’s say we want to output the contents of a WordPress post or custom post type in a web component. You know, write a post and that post is the content for the component. This allows us to use the WordPress editor to pump out an archive of <zombie-profile> elements. This is great because the WordPress editor already has most of the UI we need to enter the content for one of the <zombie-profile> components:

  • The post title can be the zombie’s name.
  • A regular paragraph block in the post content can be used for the zombie’s statement.
  • The featured image can be used for the zombie’s profile picture.

That’s most of it! But we’ll still need fields for the zombie’s age, infection date, and interests. We’ll create these with WordPress’s built in Custom Fields feature.

We’ll use the template part that handles printing each post, e.g. content.php, to output the web component. First, we’ll print out the opening <zombie-profile> tag followed by the post thumbnail (if it exists).

<zombie-profile>
  <?php 
    // If the post featured image exists...
    if (has_post_thumbnail()) {
      $src = wp_get_attachment_image_url(get_post_thumbnail_id()); ?>
      <img src="<?php echo $src; ?>" slot="profile-image">
    <?php
    }
  ?>

Next we’ll print the title for the name

<?php
  // If the post title field exits...
  if (get_the_title()) { ?>
  <span slot="zombie-name"><?php echo get_the_title(); ?></span>
  <?php
  }
?>

In my code, I have tested whether these fields exist before printing them for two reasons:

  1. It’s just good programming practice (in most cases) to hide the labels and elements around empty fields.
  2. If we end up outputting an empty <span> for the name (e.g. <span slot="zombie-name"></span>), then the field will show as empty in the final profile rather than use our web component’s built-in default text, image, etc. (If you want, for instance, the text fields to be empty if they have no content, you can either put in a space in the custom field or skip the if statement in the code).

Next, we will grab the custom fields and place them into the slots they belong to. Again, this goes into the theme template that outputs the post content.

<?php
  // Zombie age
  $temp = get_post_meta(the_ID(), 'Age', true);
  if ($temp) { ?>
    <span slot="z-age"><?php echo $temp; ?></span>
    <?php
  }
  // Zombie infection date
  $temp = get_post_meta(the_ID(), 'Infection Date', true);
  if ($temp) { ?>
    <span slot="idate"><?php echo $temp; ?></span>
    <?php
  }
  // Zombie interests
  $temp = get_post_meta(the_ID(), 'Interests', true);
  if ($temp) { ?>
    <ul slot="z-interests"><?php echo $temp; ?></ul>
    <?php
  }
?>

One of the downsides of using the WordPress custom fields is that you can’t do any special formatting, A non-technical web editor who’s filling this out would need to write out the HTML for the list items (<li>) for each and every interest in the list. (You can probably get around this interface limitation by using a more robust custom field plugin, like Advanced Custom Fields, Pods, or similar.)

Lastly. we add the zombie’s statement and the closing <zombie-profile> tag.

<?php
  $temp = get_the_content();
  if ($temp) { ?>
    <span slot="statement"><?php echo $temp; ?></span>
  <?php
  }
?>
</zombie-profile>

Because we’re using the body of the post for our statement, we’ll get a little extra code in the bargain, like paragraph tags around the content. Putting the profile statement in a custom field will mitigate this, but depending on your purposes, it may also be intended/desired behavior.

You can then add as many posts/zombie profiles as you need simply by publishing each one as a post!

Block party: web components in a custom block

Creating a custom block is a great way to add a web component. Your users will be able to fill out the required fields and get that web component magic without needing any code or technical knowledge. Plus, blocks are completely independent of themes, so really, we could use this block on one site and then install it on other WordPress sites—sort of like how we’d expect a web component to work!

There are the two main parts of a custom block: PHP and JavaScript. We’ll also add a little CSS to improve the editing experience.

First, the PHP:

function ez_webcomp_register_block() {
  // Enqueues the JavaScript needed to build the custom block
  wp_register_script(
    'ez-webcomp',
    plugins_url('block.js', __FILE__),
    array('wp-blocks', 'wp-element', 'wp-editor'),
    filemtime(plugin_dir_path(__FILE__) . 'block.js')
  );

  // Enqueues the component's CSS file
  wp_register_style(
    'ez-webcomp',
    plugins_url('ezwebcomp-style.css', __FILE__),
    array(),
    filemtime(plugin_dir_path(__FILE__) . 'ezwebcomp-style.css')
  );

  // Registers the custom block within the ez-webcomp namespace
  register_block_type('ez-webcomp/zombie-profile', array(
    // We already have the external styles; these are only for when we are in the WordPress editor
    'editor_style' =&gt; 'ez-webcomp',
    'editor_script' =&gt; 'ez-webcomp',
  ));
}
add_action('init', 'ez_webcomp_register_block');

The CSS isn’t necessary, it does help prevent the zombie’s profile image from overlapping the content in the WordPress editor.

/* Sets the width and height of the image.
 * Your mileage will likely vary, so adjust as needed.
 * "pic" is a class we'll add to the editor in block.js
*/
#editor .pic img {
  width: 300px;
  height: 300px;
}
/* This CSS ensures that the correct space is allocated for the image,
 * while also preventing the button from resizing before an image is selected.
*/
#editor .pic button.components-button { 
  overflow: visible;
  height: auto;
}

The JavaScript we need is a bit more involved. I’ve endeavored to simplify it as much as possible and make it as accessible as possible to everyone, so I’ve written it in ES5 to remove the need to compile anything.

Show code
(function (blocks, editor, element, components) {
  // The function that creates elements
  var el = element.createElement;
  // Handles text input for block fields 
  var RichText = editor.RichText;
  // Handles uploading images/media
  var MediaUpload = editor.MediaUpload;
    
  // Harkens back to register_block_type in the PHP
  blocks.registerBlockType('ez-webcomp/zombie-profile', {
    title: 'Zombie Profile', //User friendly name shown in the block selector
    icon: 'id-alt', //the icon to usein the block selector
    category: 'layout',
    // The attributes are all the different fields we'll use.
    // We're defining what they are and how the block editor grabs data from them.
    attributes: {
      name: {
        // The content type
        type: 'string',
        // Where the info is available to grab
        source: 'text',
        // Selectors are how the block editor selects and grabs the content.
        // These should be unique within an instance of a block.
        // If you only have one img or one <ul> etc, you can use element selectors.
        selector: '.zname',
      },
      mediaID: {
        type: 'number',
      },
      mediaURL: {
        type: 'string',
        source: 'attribute',
        selector: 'img',
        attribute: 'src',
      },
      age: {
        type: 'string',
        source: 'text',
        selector: '.age',
      },
      infectdate: {
        type: 'date',
        source: 'text',
        selector: '.infection-date'
      },
      interests: {
        type: 'array',
        source: 'children',
        selector: 'ul',
      },
      statement: {
        type: 'array',
        source: 'children',
        selector: '.statement',
      },
  },
  // The edit function handles how things are displayed in the block editor.
  edit: function (props) {
    var attributes = props.attributes;
    var onSelectImage = function (media) {
      return props.setAttributes({
        mediaURL: media.url,
        mediaID: media.id,
      });
    };
    // The return statement is what will be shown in the editor.
    // el() creates an element and sets the different attributes of it.
    return el(
      // Using a div here instead of the zombie-profile web component for simplicity.
      'div', {
        className: props.className
      },
      // The zombie's name
      el(RichText, {
        tagName: 'h2',
        inline: true,
        className: 'zname',
        placeholder: 'Zombie Name…',
        value: attributes.name,
        onChange: function (value) {
          props.setAttributes({
            name: value
          });
        },
      }),
      el(
        // Zombie profile picture
        'div', {
          className: 'pic'
        },
        el(MediaUpload, {
          onSelect: onSelectImage,
          allowedTypes: 'image',
          value: attributes.mediaID,
          render: function (obj) {
            return el(
              components.Button, {
                className: attributes.mediaID ?
                  'image-button' : 'button button-large',
                onClick: obj.open,
              },
              !attributes.mediaID ?
              'Upload Image' :
              el('img', {
                src: attributes.mediaURL
              })
            );
          },
        })
      ),
      // We'll include a heading for the zombie's age in the block editor
      el('h3', {}, 'Age'),
      // The age field
      el(RichText, {
        tagName: 'div',
        className: 'age',
        placeholder: 'Zombie\'s Age…',
        value: attributes.age,
        onChange: function (value) {
          props.setAttributes({
            age: value
          });
        },
      }),
      // Infection date heading
      el('h3', {}, 'Infection Date'),
      // Infection date field
      el(RichText, {
        tagName: 'div',
        className: 'infection-date',
        placeholder: 'Zombie\'s Infection Date…',
        value: attributes.infectdate,
        onChange: function (value) {
          props.setAttributes({
            infectdate: value
          });
        },
      }),
      // Interests heading
      el('h3', {}, 'Interests'),
      // Interests field
      el(RichText, {
        tagName: 'ul',
        // Creates a new <li> every time `Enter` is pressed
        multiline: 'li',
        placeholder: 'Write a list of interests…',
        value: attributes.interests,
        onChange: function (value) {
          props.setAttributes({
            interests: value
          });
        },
        className: 'interests',
      }),
      // Zombie statement heading
      el('h3', {}, 'Statement'),
      // Zombie statement field
      el(RichText, {
        tagName: 'div',
        className: "statement",
        placeholder: 'Write statement…',
        value: attributes.statement,
        onChange: function (value) {
          props.setAttributes({
            statement: value
          });
        },
      })
    );
  },

  // Stores content in the database and what is shown on the front end.
  // This is where we have to make sure the web component is used.
  save: function (props) {
    var attributes = props.attributes;
    return el(
      // The <zombie-profile web component
      'zombie-profile',
      // This is empty because the web component does not need any HTML attributes
      {},
      // Ensure a URL exists before it prints
      attributes.mediaURL &&
      // Print the image
      el('img', {
        src: attributes.mediaURL,
        slot: 'profile-image'
      }),
      attributes.name &&
      // Print the name
      el(RichText.Content, {
        tagName: 'span',
        slot: 'zombie-name',
        className: 'zname',
        value: attributes.name,
      }),
      attributes.age &&
      // Print the zombie's age
      el(RichText.Content, {
        tagName: 'span',
        slot: 'z-age',
        className: 'age',
        value: attributes.age,
    }),
      attributes.infectdate &&
      // Print the infection date
      el(RichText.Content, {
        tagName: 'span',
        slot: 'idate',
        className: 'infection-date',
        value: attributes.infectdate,
    }),
      // Need to verify something is in the first element since the interests's type is array
      attributes.interests[0] &&
      // Pint the interests
      el(RichText.Content, {
        tagName: 'ul',
        slot: 'z-interests',
        value: attributes.interests,
      }),
      attributes.statement[0] &&
      // Print the statement
      el(RichText.Content, {
        tagName: 'span',
        slot: 'statement',
        className: 'statement',
        value: attributes.statement,
    })
    );
    },
  });
})(
  //import the dependencies
  window.wp.blocks,
  window.wp.blockEditor,
  window.wp.element,
  window.wp.components
);

Plugging in to web components

Now, wouldn’t it be great if some kind-hearted, article-writing, and totally-awesome person created a template that you could just plug your web component into and use on your site? Well that guy wasn’t available (he was off helping charity or something) so I did it. It’s up on github:

Do It Yourself – Easy Web Components for WordPress

The plugin is a coding template that registers your custom web component, enqueues the scripts and styles the component needs, provides examples of the custom block fields you might need, and even makes sure things are styled nicely in the editor. Put this in a new folder in /wp-content/plugins like you would manually install any other WordPress plugin, make sure to update it with your particular web component, then activate it in WordPress on the “Installed Plugins” screen.

Not that bad, right?

Even though it looks like a lot of code, we’re really doing a few pretty standard WordPress things to register and render a custom web component. And, since we packaged it up as a plugin, we can drop this into any WordPress site and start publishing zombie profiles to our heart’s content.

I’d say that the balancing act is trying to make the component work as nicely in the WordPress block editor as it does on the front end. We would have been able to knock this out with a lot less code without that consideration.

Still, we managed to get the exact same component we made in my previous articles into a CMS, which allows us to plop as many zombie profiles on the site. We combined our knowledge of web components with WordPress blocks to develop a reusable block for our reusable web component.

What sort of components will you build for your WordPress site? I imagine there are lots of possibilities here and I’m interested to see what you wind up making.

Article series

  1. Web Components Are Easier Than You Think
  2. Interactive Web Components Are Easier Than You Think
  3. Using Web Components in WordPress is Easier Than You Think

The post Using Web Components in WordPress is Easier Than You Think appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Interactive Web Components Are Easier Than You Think

In my last article, we saw that web components aren’t as scary as they seem. We looked at a super simple setup and made a zombie dating service profile, complete with a custom <zombie-profile> element. We reused the element for each profile and populated each one with unique info using the <slot> element.

Here’s how it all came together.

That was cool and a lot of fun (well, I had fun anyway…), but what if we take this idea one step further by making it interactive. Our zombie profiles are great, but for this to be a useful, post-apocalyptic dating experience you’d want to, you know, “Like” a zombie or even message them. That’s what we’re going to do in this article. We’ll leave swiping for another article. (Would swiping left be the appropriate thing for zombies?)

This article assumes a base level of knowledge about web components. If you’re new to the concept, that’s totally fine — the previous article should give you everything you need. Go ahead. Read it. I’ll wait. *Twiddles thumbs* Ready? Okay.

First, an update to the original version

Let’s pause for one second (okay, maybe longer) and look at the ::slotted() pseudo element. It was brought to my attention after the last article went out (thanks, Rose!) and it solves some (though not all) of the encapsulation issues I encountered. If you recall, we had some CSS styles outside of the component’s <template> and some inside a <style> element within the <template>. The styles inside the <template> were encapsulated but the ones outside were not.

But that’s where ::slotted comes into play. We declare an element in the selector like so:

::slotted(img) {
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 1em 0 0;
}

Now, any <img> element placed in any slot will be selected. This helps a lot!

But this doesn’t solve all of our encapsulation woes. While we can select anything directly in a slot, we cannot select any descendant of the element in the slot. So, if we have a slot with children — like the interests section of the zombie profiles — we’re unable to select them from the <style> element. Also, while ::slotted has great browser support, some things (like selecting a pseudo element, e.g., ::slotted(span)::after) will work in some browsers (hello, Chrome), but won’t work in others (hello, Safari).

So, while it’s not perfect, ::slotted does indeed provide more encapsulation than what we had before. Here’s the dating service updated to reflect that:

Back to interactive web components!

First thing I’d like to do is add a little animation to spice things up. Let’s have our zombie profile pics fade in and translate up on load.

When I first attempted this, I used img and ::slotted(img) selectors to directly animate the image. But all I got was Safari support. Chrome and Firefox would not run the animation on the slotted image, but the default image animated just fine. To get it working, I wrapped the slot in a div with a .pic class and applied the animation to the div instead.

.pic {
  animation: picfadein 1s 1s ease-in forwards;
  transform: translateY(20px);
  opacity: 0;
}

@keyframes picfadein {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

“Liking” zombies

Wouldn’t it be something to “Like” that cute zombie? I mean from the user’s perspective, of course. That seems like something an online dating service ought to have at the very least.

We’ll add a checkbox “button” that initiates a heart animation on click. Let’s add this HTML at the top of the .info div:

<input type="checkbox" id="trigger"><label class="likebtn" for="trigger">Like</label>

Here’s a heart SVG I pulled together. We know that Zombies love things to be terrible, so their heart will be an eye searing shade of chartreuse:

<svg viewBox="0 0 160 135" class="heart" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path d="M61 12V0H25v12H12v13H0v36h12v13h13v12h12v12h12v12h12v13h13v12h12v-12h13v-13h11V98h13V86h-1 13V74h12V61h12V25h-12V12h-12V0H98v12H85v13H74V12H61z" fill="#7aff00"/></svg>

Here’s the important bits of the CSS that are added to the template’s <style> element:

#trigger:checked + .likebtn {
  /* Checked state of the .likebtn. Flips the foreground/background color of the unchecked state. */
  background-color: #960B0B;
  color: #fff;
}

#trigger {
  /* With the label attached to the input with the for attribute, clicking the label checks/unchecks the box, so we can remove the checkbox. */
  display: none;
}

.heart {
  /* Start the heart off so small it's nigh invisible */
  transform: scale(0.0001);
}

@keyframes heartanim {
  /* Heart animation */
  0% { transform: scale(0.0001); }
  50% { transform: scale(1); }
  85%, 100% { transform: scale(0.4); }
}

#trigger:checked ~ .heart {
  /* Checking the checkbox initiates the animation */
  animation: 1s heartanim ease-in-out forwards;
}

Pretty much standard HTML and CSS there. Nothing fancy or firmly web-component-ish. But, hey, it works! And since it’s technically a checkbox, it’s just as easy to “unlike” a zombie as it is to “Like” one.

Messaging zombies

If you’re a post-apocalyptic single who’s ready to mingle, and see a zombie whose personality and interests match yours, you might want to message them. (And, remember, zombies aren’t concerned about looks — they’re only interested in your braaains.)

Let’s reveal a message button after a zombie is “Liked.” The fact that the Like button is a checkbox comes in handy once again, because we can use its checked state to conditionally reveal the message option with CSS. Here’s the HTML added just below the heart SVG. It can pretty much go anywhere as long as it’s a sibling of and comes after the #trigger element.

<button type="button" class="messagebtn">Message</button>

Once the #trigger checkbox is checked, we can bring the messaging button into view:

#trigger:checked ~ .messagebtn {
  display: block;
}

We’ve done a good job avoiding complexity so far, but we’re going to need to reach for a little JavaScript in here. If we click the message button, we’d expect to be able to message that zombie, right? While we could add that HTML to our <template>, for demonstration purposes, lets use some JavaScript to build it on the fly.

My first (naive) assumption was that we could just add a <script> element to the template, create an encapsulated script, and be on our merry way. Yeah, that doesn’t work. Any variables instantiated in the template get instantiated multiple times and well, JavaScript’s cranky about variables that are indistinguishable from each other. *Shakes fist at cranky JavaScript*

You probably would have done something smarter and said, “Hey, we’re already making a JavaScript constructor for this element, so why wouldn’t you put the JavaScript in there?” Well, I was right about you being smarter than me.

Let’s do just that and add JavaScript to the constructor. We’ll add a listener that, once clicked, creates and displays a form to send a message. Here’s what the constructor looks like now, smarty pants:

customElements.define('zombie-profile',
class extends HTMLElement {
  constructor() {
    super();
    let profile = document.getElementById('zprofiletemplate');
    let myprofile = profile.content;
    const shadowRoot = this.attachShadow({
      mode: 'open'
    }).appendChild(myprofile.cloneNode(true));

    // The "new" code
    // Grabbing the message button and the div wrapping the profile for later use
    let msgbtn = this.shadowRoot.querySelector('.messagebtn'),
        profileEl = this.shadowRoot.querySelector('.profile-wrapper');
    
    // Adding the event listener
    msgbtn.addEventListener('click', function (e) {

      // Creating all the elements we'll need to build our form
      let formEl = document.createElement('form'),
          subjectEl = document.createElement('input'),
          subjectlabel = document.createElement('label'),
          contentEl = document.createElement('textarea'),
          contentlabel = document.createElement('label'),
          submitEl = document.createElement('input'),
          closebtn = document.createElement('button');
        
      // Setting up the form element. The action just goes to a page I built that spits what you submitted back at you
      formEl.setAttribute('method', 'post');
      formEl.setAttribute('action', 'https://johnrhea.com/undead-form-practice.php');
      formEl.classList.add('hello');

      // Setting up a close button so we can close the message if we get shy
      closebtn.innerHTML = "x";
      closebtn.addEventListener('click', function () {
        formEl.remove();
      });

      // Setting up form fields and labels
      subjectEl.setAttribute('type', 'text');
      subjectEl.setAttribute('name', 'subj');
      subjectlabel.setAttribute('for', 'subj');
      subjectlabel.innerHTML = "Subject:";
      contentEl.setAttribute('name', 'cntnt');
      contentlabel.setAttribute('for', 'cntnt');
      contentlabel.innerHTML = "Message:";
      submitEl.setAttribute('type', 'submit');
      submitEl.setAttribute('value', 'Send Message');

      // Putting all the elments in the Form
      formEl.appendChild(closebtn);
      formEl.appendChild(subjectlabel);
      formEl.appendChild(subjectEl);
      formEl.appendChild(contentlabel);
      formEl.appendChild(contentEl);
      formEl.appendChild(submitEl);

      // Putting the form on the page
      profileEl.appendChild(formEl);
    });
  }
});

So far, so good!

Before we call it a day, there’s one last thing we need to address. There’s nothing worse than that first awkward introduction, so lets grease those post-apocalyptic dating wheels by adding the zombie’s name to the default message text. That’s a nice little convenience for the user.

Since we know that the first span in the <zombie-profile> element is always the zombie’s name, we can grab it and stick its content in a variable. (If your implementation is different and the elements’s order jumps around, you may want to use a class to ensure you always get the right one.)

let zname = this.getElementsByTagName("span")[0].innerHTML;

And then add this inside the event listener:

contentEl.innerHTML = "Hi " + zname + ",\nI like your braaains...";

That wasn’t so bad, was it? Now we know that interactive web components are just as un-scary as the zombie dating scene… well you know what I mean. Once you get over the initial hurdle of understanding the structure of a web component, it starts to make a lot more sense. Now that you’re armed with interactive web component skills, let’s see what you can come up with! What other sorts of components or interactions would make our zombie dating service even better? Make it and share it in the comments.


The post Interactive Web Components Are Easier Than You Think appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Web Components Are Easier Than You Think

When I’d go to a conference (when we were able to do such things) and see someone do a presentation on web components, I always thought it was pretty nifty (yes, apparently, I’m from 1950), but it always seemed complicated and excessive. A thousand lines of JavaScript to save four lines of HTML. The speaker would inevitably either gloss over the oodles of JavaScript to get it working or they’d go into excruciating detail and my eyes would glaze over as I thought about whether my per diem covered snacks.

But in a recent reference project to make learning HTML easier (by adding zombies and silly jokes, of course), the completist in me decided I had to cover every HTML element in the spec. Beyond those conference presentations, this was my first introduction to the <slot> and <template> elements. But as I tried to write something accurate and engaging, I was forced to delve a bit deeper.

And I’ve learned something in the process: web components are a lot easier than I remember.

Either web components have come a long way since the last time I caught myself daydreaming about snacks at a conference, or I let my initial fear of them get in the way of truly knowing them — probably both.

I’m here to tell you that you—yes, you—can create a web component. Let’s leave our distractions, fears, and even our snacks at the door for a moment and do this together.

Let’s start with the <template>

A <template> is an HTML element that allows us to create, well, a template—the HTML structure for the web component. A template doesn’t have to be a huge chunk of code. It can be as simple as:

<template>
  <p>The Zombies are coming!</p>
</template>

The <template> element is important because it holds things together. It’s like the foundation of building; it’s the base from which everything else is built. Let’s use this small bit of HTML as the template for an <apocalyptic-warning> web component—you know, as a warning when the zombie apocalypse is upon us.

Then there’s the <slot>

<slot> is merely another HTML element just like <template>. But in this case, <slot> customizes what the <template> renders on the page.

<template>
  <p>The <slot>Zombies</slot> are coming!</p>
</template>

Here, we’ve slotted (is that even a word?) the word “Zombies” in the templated markup. If we don’t do anything with the slot, it defaults to the content between the tags. That would be “Zombies” in this example.

Using <slot> is a lot like having a placeholder. We can use the placeholder as is, or define something else to go in there instead. We do that with the name attribute.

<template>
  <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

The name attribute tells the web component which content goes where in the template. Right now, we’ve got a slot called whats-coming. We’re assuming zombies are coming first in the apocalypse, but the <slot> gives us some flexibility to slot something else in, like if it ends up being a robot, werewolf, or even a web component apocalypse.

Using the component

We’re technically done “writing” the component and can drop it in anywhere we want to use it.

<apocalyptic-warning>
  <span slot="whats-coming">Halitosis Laden Undead Minions</span>
</apocalyptic-warning>

<template>
  <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

See what we did there? We put the <apocalyptic-warning> component on the page just like any other <div> or whatever. But we also dropped a <span> in there that references the name attribute of our <slot>. And what’s between that <span> is what we want to swap in for “Zombies” when the component renders.

Here’s a little gotcha worth calling out: custom element names must have a hyphen in them. It’s just one of those things you’ve gotta know going into things. The spec prescribes that to prevent conflicts in the event that HTML releases a new element with the same name.

Still with me so far? Not too scary, right? Well, minus the zombies. We still have a little work to do to make the <slot> swap possible, and that’s where we start to get into JavaScript.

Registering the component

As I said, you do need some JavaScript to make this all work, but it’s not the super complex, thousand-lined, in-depth code I always thought. Hopefully I can convince you as well.

You need a constructor function that registers the custom element. Otherwise, our component is like the undead: it’s there but not fully alive.

Here’s the constructor we’ll use:

// Defines the custom element with our appropriate name, <apocalyptic-warning>
customElements.define("apocalyptic-warning",

  // Ensures that we have all the default properties and methods of a built in HTML element
  class extends HTMLElement {

    // Called anytime a new custom element is created
    constructor() {

      // Calls the parent constructor, i.e. the constructor for `HTMLElement`, so that everything is set up exactly as we would for creating a built in HTML element
      super();

      // Grabs the <template> and stores it in `warning`
      let warning = document.getElementById("warningtemplate");

      // Stores the contents of the template in `mywarning`
      let mywarning = warning.content;

      const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));
    }
  });

I left detailed comments in there that explain things line by line. Except the last line:

const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));

We’re doing a lot in here. First, we’re taking our custom element (this) and creating a clandestine operative—I mean, shadow DOM. mode: open simply means that JavaScript from outside the :root can access and manipulate the elements within the shadow DOM, sort of like setting up back door access to the component.

From there, the shadow DOM has been created and we append a node to it. That node will be a deep copy of the template, including all elements and text of the template. With the template attached to the shadow DOM of the custom element, the <slot> and slot attribute take over for matching up content with where it should go.

Check this out. Now we can plop two instances of the same component, rendering different content simply by changing one element.

Styling the component

You may have noticed styling in that demo. As you might expect, we absolutely have the ability to style our component with CSS. In fact, we can include a <style> element right in the <template>.

<template id="warningtemplate">
  <style>
    p {
      background-color: pink;
      padding: 0.5em;
      border: 1px solid red;
    }
  </style>

    <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

This way, the styles are scoped directly to the component and nothing leaks out to other elements on the same page, thanks to the shadow DOM.

Now in my head, I assumed that a custom element was taking a copy of the template, inserting the content you’ve added, and then injecting that into the page using the shadow DOM. While that’s what it looks like on the front end, that’s not how it actually works in the DOM. The content in a custom element stays where it is and the shadow DOM is sort of laid on top like an overlay.

Screenshot of the HTML source of the zombie-warning component. The custom element is expanded in the shadow dam, including the style block, the custom element, and the template.

And since the content is technically outside the template, any descendant selectors or classes we use in the template’s <style> element will have no affect on the slotted content. This doesn’t allow full encapsulation the way I had hoped or expected. But since a custom element is an element, we can use it as an element selector in any ol’ CSS file, including the main stylesheet used on a page. And although the inserted material isn’t technically in the template, it is in the custom element and descendant selectors from the CSS will work.

apocalyptic-warning span {
  color: blue;
}

But beware! Styles in the main CSS file cannot access elements in the <template> or shadow DOM.

Let’s put all of this together

Let’s look at an example, say a profile for a zombie dating service, like one you might need after the apocalypse. In order to style both the default content and any inserted content, we need both a <style> element in the <template> and styling in a CSS file.

The JavaScript code is exactly the same except now we’re working with a different component name, <zombie-profile>.

customElements.define("zombie-profile",
  class extends HTMLElement {
    constructor() {
      super();
      let profile = document.getElementById("zprofiletemplate");
      let myprofile = profile.content;
      const shadowRoot = this.attachShadow({mode: "open"}).appendChild(myprofile.cloneNode(true));
    }
  }
);

Here’s the HTML template, including the encapsulated CSS:

<template id="zprofiletemplate">
  <style>
    img {
      width: 100%;
      max-width: 300px;
      height: auto;
      margin: 0 1em 0 0;
    }
    h2 {
      font-size: 3em;
      margin: 0 0 0.25em 0;
      line-height: 0.8;
    }
    h3 {
      margin: 0.5em 0 0 0;
      font-weight: normal;
    }
    .age, .infection-date {
      display: block;
    }
    span {
      line-height: 1.4;
    }
    .label {
      color: #555;
    }
    li, ul {
      display: inline;
      padding: 0;
    }
    li::after {
      content: ', ';
    }
    li:last-child::after {
      content: '';
    }
    li:last-child::before {
      content: ' and ';
    }
  </style>

  <div class="profilepic">
    <slot name="profile-image"><img src="https://assets.codepen.io/1804713/default.png" alt=""></slot>
  </div>

  <div class="info">
    <h2><slot name="zombie-name" part="zname">Zombie Bob</slot></h2>

    <span class="age"><span class="label">Age:</span> <slot name="z-age">37</slot></span>
    <span class="infection-date"><span class="label">Infection Date:</span> <slot name="idate">September 12, 2025</slot></span>

    <div class="interests">
      <span class="label">Interests: </span>
      <slot name="z-interests">
        <ul>
          <li>Long Walks on Beach</li>
          <li>brains</li>
          <li>defeating humanity</li>
        </ul>
      </slot>
    </div>

    <span class="z-statement"><span class="label">Apocalyptic Statement: </span> <slot name="statement">Moooooooan!</slot></span>

  </div>
</template>

Here’s the CSS for our <zombie-profile> element and its descendants from our main CSS file. Notice the duplication in there to ensure both the replaced elements and elements from the template are styled the same.

zombie-profile {
  width: calc(50% - 1em);
  border: 1px solid red;
  padding: 1em;
  margin-bottom: 2em;
  display: grid;
  grid-template-columns: 2fr 4fr;
  column-gap: 20px;
}
zombie-profile img {
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 1em 0 0;
}
zombie-profile li, zombie-profile ul {
  display: inline;
  padding: 0;
}
zombie-profile li::after {
  content: ', ';
}
zombie-profile li:last-child::after {
  content: '';
}
zombie-profile li:last-child::before {
  content: ' and ';
}

All together now!

While there are still a few gotchas and other nuances, I hope you feel more empowered to work with the web components now than you were a few minutes ago. Dip your toes in like we have here. Maybe sprinkle a custom component into your work here and there to get a feel for it and where it makes sense.

That’s really it. Now what are you more scared of, web components or the zombie apocalypse? I might have said web components in the not-so-distant past, but now I’m proud to say that zombies are the only thing that worry me (well, that and whether my per diem will cover snacks…)


The post Web Components Are Easier Than You Think appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Copyediting with Semantic HTML

Tracking changes is a quintessential copyediting feature for comparing versions of content. While we’re used to tracking changes in a word processing document, we actually have HTML elements capable of that. There are a lot of elements that we can use for this process. The main ones we’ll look at are <del>, <ins> and <mark>. But, as we’ll see, pairing them with other elements — including <u>, <aside> and custom markup — we can get the same sort of visual tracking changes features as something like Word, Google Docs, or even WordPress.

Side-by-side screenshots of how Pages, Google Docs and WordPress display tracked changes.
Different apps have different ways of tracking changes.

Let’s start with the <ins> element.

The <ins> designates text that should be or has been inserted. The verb tense gets a little wonky here because while the <ins> tag is suggesting an edit, it has to have, by virtue of being in the <ins> tag, already been inserted. It’s sorta like saying, “Hey, insert this things that’s technically already there.”

Notice how the browser underlines the inserted text for us. It’s nice to have that sort of visual indication, even if it could be mistaken as an underline using the <u> element, a link, or the CSS text-decoration property.

Let’s pair the insertion with the <del> element, which suggests text that should be or has been deleted.

The browser styles <del> like a strikethrough (<s>) element, but they mean different things. <del> is for content that should be removed/edited out (like that creepy seeming section above) while <s> is for content that’s no longer true or inaccurate (like the letter writer’s belief that that section would be endearing).

OK, great, so we have these semantic HTML elements and they produce some light visual indicators for content that is either inserted or deleted. But there’s something you might not know about these elements: they accept a cite  attribute that can be used to annotate the change.

cite takes a properly formatted URL that provides points somewhere to find the reasons why the change was made. That somewhere could even be an anchor on the existing page.

That’s cool, but one issue is that the citation URL isn’t actually visible or clickable. We could use some CSS magic to display it. But even then, it still won’t take you to the citation when clicked… nor can it be copied. 

That said, it does make semantically clear what’s part of the edit and what is not. If we wrap <ins> and <del> in a link (or even the other way around) it still is not clear whether the link is supposed to be part of the edited content or not.

But! There’s a second attribute that <ins> and <del> both share: datetime. And this is how we can indicate when an edit was made. Again, this is not immediately available to a user, but it keeps semantically clear what is part of the edit and what isn’t. 

HTML’s datetime format, as a machine readable date and time, requires precision and can thus be a bit, well, cranky, But it’s general tenants aren’t too hard. It’s worth noting though that, while datetime is used on other elements, such as <time>, formatting the value in a way that doesn’t include at least a specific day, month, and year on <ins> and <del> would be problematic, obscuring the date and time of an edit rather than provide clarity.

We can make things clearer with a little more CSS magic. For example, we can reveal the datetime value on hover:

Checkboxes work too:

But good editing is far more than simply adding and deleting content. It’s asking questions and figuring out what the heck the author intended. (For me personally, it’s also about saving me from embarrassing spellling and grammar mistooks).

So, meet the <mark> element.

<mark> points out text of special interest to the reader. It usually renders as a yellow background behind the content. 

If you’re the editor and want to write a note to the writer (let’s name that person Stanley Meagher) with suggestions to make Stanly’s letter more awesome (or less creepy, at the very least) and that note is large enough to warrant flow content (i.e. block level elements), then the note can be an <aside> element.

<aside class="note">Mr. Meagher, I highly recommend you remove this list of preferred cheeses and replace it with things you love about the woman you are writing to. While I'm sure there are many people for whom your list would be interesting if not welcome, that list rarely includes a romantic interest in the midst of your profession of love. Though, honestly, if she is as perfect for you as you believe, it may be the exact thing you need to test that theory.</aside>

But often you’ll want to do something inline in order to point something out or make a comment about sentence structure or word choice. Unfortunately there’s no baked in way to do that in HTML, but with a little ingenuity and some CSS you can add a note.

<span class="note">Cheesecake isn't really a "cheese"</span>

The <u> element — long an anathema to web developers for fear of confusion with a link — does actually have a use (I know, I was surprised too). It can be used to point out a misspelling (apparently squiggly and red underlines aren’t a standard browser rendering feature). It should still not be used anywhere it might be confused with an actual link and, when used, it definitely should use a color that distinguishes it from links. Red color may be appropriate to indicate an error. 

<p>Please, <u>Lura</u> tell me your answer. Will you wear my mathlete letter jacket?</p>

As we’ve seen throughout this article, the browser’s default styles for the elements we’ve covered so far are certainly helpful but can also be confusing since they are barely distinguishable from other types of content. If a user does not know that the document is showing edits, then the styling may be misconstrued or misunderstood by the user. I’d therefore suggest some additional or alternate styles to help make it clear what’s going on.

ins {
  padding: 0 0.125em;
  text-decoration: none;
  background-color: lightgreen
}
del {
  padding: 0 0.125em;
  text-decoration: none;
  background-color: pink;
}
mark {
  padding: 0 0.125em;
}
.note {
  padding: 0 0.125em;
  background-color: lightblue;
}
aside.note {
  padding: 0.5em 1em;
}
u {
  text-decoration: none;
  border-bottom: 3px red dashed;
}

I ask myself the same question every time I learn something new in HTML: How can I needlessly animate this?

It would be great if we could fade up the changes so that when you clicked a checkbox the edits would fade in as well.

The notes and text in <del> tags can’t be faded in with CSS the same way that background colors and paddings can. Also, display: none  results in no fading at all. Everything pops back in place, including the backgrounds. But using a combining the CSS visibility property with a set height and width value of 0 allows the backgrounds to smoothly fade in.


And there you have it: specifications and a few strategies for keeping track of edits on the web (plus an excellent example of how not to write a love letter (or, perhaps, how to write one so perfect that responding positively to it is a sign you’re soulmates).

Edit: Adrian Roselli adds some excellent accessibility information in the comments. Before you implement these ideas in production, be sure to consider those suggestions.


The post Copyediting with Semantic HTML appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

How Film School Helped Me Make Better User Experiences

Recently, I finished a sixty-day sprint where I posted hand-coded zombie themed CSS animation every day. I learned a lot, but it also took me back to film school and reminded me of so many things I learned about storytelling, cinematography, and art.

Turns out that much of what I learned back then is relevant to websites, particularly web animations. Sarah Drasner made the connection between theater and development and I thought I’d extend some of those ideas as they relate to film.

A story makes everything more engaging

Humans love stories. I don’t need to quote you statistics on the billions of dollars spent on shows and books and games each year. If you can inject story into a website — especially when it comes to animation — it’ll be that much more interesting and appealing to your audience.

There are many ways to define what a “story” is, but as far as things go for the web where animations can be quick or subtle, I think a story only requires two things: a character and an inciting incident (which is simply a plot point that brings the protagonist — or main character — into the story).

Take the “Magical Oops” demo I made over at CodePen:

There’s not much going on, but there is a story. We have a character, the scientist, who invokes an inciting incident when he fires the shrink ray at the zombie. Instead of shrinking the zombie, the ray shrinks the zombie’s hat to reveal (and ultimately be worn by) a rabbit. Will you necessarily relate to those characters? Probably not, at least personally. But the fact that something happens to them is enough of an engaging hook to draw you in.

Sure, I lean toward funny and silly storylines, but a story’s tone can be serious or any other number of things.

I’m confident you can find a story that fits your site.

A story makes everything more personable

Humans anthropomorphize anything and everything. You know exactly what that feels like if you’ve ever identified with characters in a Pixar movie, like “Toy Story” or “Inside Out.” The character you add doesn’t have to be a literal living thing or representative of a living thing. Heck, my stories are about the undead.

How does that relate to the web? Let’s say your app congratulates users when completing a task, like Slack does when all unread threads have been cleared out.

The point is to add some personality and intentionality to whatever movement you’re creating. It’s also about bringing the story — which is the user task of reviewing unread messages — to a natural (and, in this case, a happy) conclusion. That sort of feedback is not only informative, but something that makes the user part of the story in a personable way.

If a viewer can understand the subject of the story, they’ll get why something moves or changes. They’ll see it as a character — even if the subject is the user. That’s what makes something personable. (You got it! Here’s a pony. 🐴)

Watch for the human’s smirk in my “Undead Seat Driver” pen:

The smirk introduces an emotional element that further adds to the story by making the main character more relatable.

Direct attention with visual depth

One of the greatest zombie movies of all time, Citizen Kane, reached popularity for a variety of reasons. It’s a wonderful story with great acting, for one, but there’s something else you might not catch when viewing the movie today that was revolutionary at the time: deep focus photography. Deep focus allowed things in the foreground and the background and the middle ground to be in focus all at the same time. Before this, it was only possible to use one focal point at a time. Deep focus made the film almost feel like it was in 3D.

We’re not constrained by camera lenses on the web (well, aside from embedded media I suppose), but one thing that makes the deep focus photography of Citizen Kane work so well is that director Orson Welles was able to point a viewer’s attention at different planes at different times. He sometimes even had multiple things happening in multiple planes, but this was always a choice. 

Working with deep focus on the web has actually been happening for some time, even if it isn’t called that. Think of parallax scrolling and how it adds depth between backgrounds. There’s also the popular modal pattern where an element dominates the foreground while the background is either dimmed or blurred out.

That was the idea behind my “Hey, Hey, Hey!” pen that starts with a character in focus on a faraway plane who gives way to a zombie that appears in the foreground:

The opposite sort of thing occurs here in my “Nobody Here But Us Humans… 2” pen:

Try to think of a website as a 3D space and you’ll open up possibilities you may have never considered before. And while there are 3D transforms that work right now in your browser, that isn’t the only thing I’m talking about. There are tons of ways to “fake” a 3D effect using shading, shadows, relative size, blurs or other types of distortion.

For example, I used a stacking order to mimic a multi-dimensional space in my “Finally, alone with my sandwich…” pen. Notice how the human’s head rotation lends a little more credibility to the effect:

Take animation to the next level with scenes

Some of the work I’m proudest of are those where I went beyond silly characters doing silly things (although I am proud of that as well). There are two animations in particular that come to mind.

The first is what I call “Zombie Noon 2”:

The reason this one stands out to me is how the camera suddenly (and possibly as an unexpected plot twist) turns the viewer into a character in the story. Once the Zombie’s shots are fired, the camera rolls over, essentially revealing that it’s you who has been shot.

The second piece that comes to mind is called “Lunch (at) Noon” :

(I apparently got some middle school glee out of shooting hats off zombies’s heads. *shrugs* Being easily amused is cheap entertainment.)

Again, the camera puts things in a sort of first-person perspective where we’re facing a zombie chef who gets his hat shot off. The twist comes when a Ratatouille-like character is revealed under the hat, triggering a new scene by zooming in on him. Watch his eyes narrow when the focus turns to him.

Using the “camera” is an awesome way to bring an animation to the next level; it forces viewer participation. That doesn’t mean the camera should swoop and fly and zoom at every turn and with every animation, but switching from a 2D to a 3D perspective — when done well and done to deepen the experience — can enhance  a user’s experience with it.


So, as it turns out, my film school education really has paid off! There’s so much of it that directly applies to the web, and hopefully you see the same correlations that I’ve discovered.

I’d be remiss if I didn’t call out something important in this article. While I think borrowing concepts from stories and storytelling is really awesome and can be the difference between good and great experiences, they aren’t the right call in every situation. Like, what’s the point of putting a user through a story-like experience on a terms and conditions page? Legal content is typically already a somewhat tense read, so adding more tension may not be the best bet. But, hey, if you’re able to introduce a story that relieves the tension of that context, then by all means! and, let’s not forget about users who prefer reduced motion.

Bottom line: These ideas aren’t silver bullets for all cases. They’re tools to help you think about how you can take your site and your animations the extra mile and enhance a user’s experience in a pleasant way.


The post How Film School Helped Me Make Better User Experiences appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Lessons Learned from Sixty Days of Re-Animating Zombies with Hand-Coded CSS

Caution: Terrible sense of humor ahead. We’ll talk about practical stuff, but the examples pretty much all involve zombies and silly jokes. You have been warned.

I’ll be linking to individual Pens as I discuss the lessons I learned, but if you’d like to get a sense of the entire project, check out 60 days of Animation on Undead Institute. I started this project to end on August 1st, 2020, coinciding with the publication of a book I wrote featuring CSS animation, humor, and zombies — because, obviously, zombies will destroy the world if you don’t brandish your web skills and stop the apocalypse. Nothing puts the hurt on the horde like a HTML element on the move!

I had a few rules for myself throughout the project. 

  1. I would hand-code all CSS. (I’m a masochist.)
  2. The user would initiate all of the animation. (I hate coming upon an animation that’s already halfway through.) 
  3. I would use JavaScript as little as possible and never for animation. (I only ended up using JavaScript once, and that was to start audio with the final animation. I have nothing against JavaScript, it’s just not what I wanted to do here.)

Lesson 1: Eighty days is a long time.

Uh, doesn’t the title say “sixty” days? Yes, but my original goal was to do eighty days and as day one approached with less than twenty animations prepared and a three day average for each production, I freaked out and switched to sixty days. That gave me both twenty more days till the beginning date and twenty fewer pieces to do.

Lesson 1A: Sixty days is still a long time.

That’s a lot of animation to do with a limited amount of time, ideas, and even more limited artistic skills. And while I thought of dropping to thirty days, I’m glad I didn’t. Sixty days stretched me and forced me to go deeper into how CSS animation — and by extension, CSS itself — works. I’m also proudest of many of the later pieces I did as my skills increased, and I had to be more innovative and think harder about how to make things interesting. Once you’ve used all the easy options, the actual work and best results begin. (And yes, it ended up being sixty-two days because I started on June 1 and wanted to do a final animation on August 1. Starting June 3 just felt icky and wrong.)

So, the real Lesson 1: stretch yourself.

Lesson 2: Interactive animations are hard, and even harder to make responsive. 

If you want something to fly across the screen and connect with another element or appear to start another element’s move, you must use either all standard, inflexible units or all flexible units. 

Three variables determine when and where an animated element will be during any animation: duration, velocity, and distance. The duration of the animation is set in the animation property and cannot be changed in relation to screen size. The animation timing function determines the velocity; screen size can’t change that either. Thus, if the distance varies with the screen size, the timing will be off everywhere except a specific screen width and height. 

Look at Tank!. Run the animation at wide and narrow screen sizes. While I got the timing close, if you compare the two, you’ll see that the tank is in a different place relative to the zombies when the last zombies fall.

Showing the same brown take, side by side, where the tank on the left is further along than the tank on the right.

To avoid these timing issues, you can use fixed units and a large number, like 2000 or 5000 pixels or more, so that the animation will cover the width (or height) of the screen for all but the largest monitors.  

Lesson 3: If you want a responsive animation, put everything in (one of the) viewport units. 

Going halfsies on unit proportions (e.g. setting width and height in pixels, but location and movement with viewport units) will lead to unpredictable results. Don’t use both vw and vh either but one or the other; whichever will be the dominant orientation. Mixing vh and vw units will make your animation go “wonky” which I believe is the technical term. 

Take Superbly Zomborrific, for instance. It mixes pixel, vw, and vh units. The premise is that the Super Zombie is flying upward as the “camera” follows. Super Zombie smashes into a ledge and falls as the camera continues, but you wouldn’t understand that if your screen was sufficiently tall.

Two animation frames, side by side where the left shows the flying green zombie hitting a building ceiling and the right shows the zombie leaving the frame after impact.

That also means that if you need something to come in from the top — like I did in Nobody Here But Us Humans —you must set the vw height high enough to ensure that the ninja zombie isn’t visible at most aspect ratios.

Lesson 3A: Use pixel units for movements within an SVG element. 

All that said, transforming elements within an SVG element should not use viewport units. SVG tags are their own proportional universe. The SVG “pixel” will stay proportional within the SVG element to all the other SVG element children while viewport units will not. So transform with pixel units within an SVG element, but use viewport units everywhere else.

Lesson 4: SVGs scale horribly at runtime.

For animations, like Oops…, I made the SVG image of the zombie scale up to five times his size, but that makes the edges fuzzy. [Shakes fist at “scalable” vector graphics.]

/* Original code resulting in fuzzy edges */
.zombie {
  transform: scale(1);
  width: 15vw;
}

.toggle-checkbox:checked ~ .zombie {
  animation: 5s ease-in-out 0s reverseshrinkydink forwards;
}

@keyframes reverseshrinkydink {
  0% {
    transform: scale(1);
  }
  100% {
    transform: scale(5);
  }
}

I learned to set their dimensions to the final dimensions that would be in effect at the end of the animation, then use a scale transform to shrink them down to the size for the start of the animation. 

/* Revised code */
.zombie {
  transform: scale(0.2);
  width: 75vw;
}

.toggle-checkbox:checked ~ .zombie {
  animation: 5s ease-in-out 0s reverseshrinkydink forwards;
}

@keyframes reverseshrinkydink {
  0% {
    transform: scale(0.2);
  }
  100% {
    transform: scale(1);
  }
}

In short, the revised code moves from a scaled-down version of the image up to the full width and height. The browser always renders at 1, making the edges crisp and clean at a scale of 1. So instead of scaling from 1 to 5, I scaled from 0.2 to 1.

The same animation frame of a scientist holding a coffee mug standing to the left of a growing zombie where the frame on the left shows the zombie with blurry edges and the frame on the right is clear.

Lesson 5: The axis Isn’t a universal truth. 

An element’s axes stay in sync with the element, not the page. A 90-degree rotation before a translateX will change the direction of the translateX from horizontal to vertical. In Nobody Here But Us Humans… 2, I flipped the zombies using a 180-degree rotation. But positive Y values move the ninjas towards the top, and negative ones move them towards the bottom (the opposite of normal). Beware of how a rotation may affect transforms further down the line.

Showing the main character facing us in the foreground with 7 ninja characters hanging upside down from the ceiling against a light pink background.

Lesson 6. Separate complex animations into concentric elements to make easier adjustments.

When creating a complex animation that moves in multiple directions, adding wrapper divs, or rather parent elements, and animating each one individually will cut down on conflicting transforms, and prevent you from becoming a weepy mess.

For instance, in Space Cadet, I had three different transforms going on. The first is the zomb-o-naut’s moving in an up and down motion. The second is a movement across the screen. The third is a rotation. Rather than trying to do everything in a single transform, I added two wrapping elements and did one animation on each element (I also saved my hair… at least some of it.) This helped avoid the axis issues discussed in the last lesson because I performed the rotation on the innermost element, leaving its parent’s and grandparent’s axes in place.

Lesson 7: SVG and CSS transforms are the same. 

Some paths and groups and other SVG elements will already have transforms defined on them. It could be from an optimization algorithm, or perhaps it’s just how the illustration software generates the code. If a path, group, or whatever element in an SVG already has an SVG transform on it, removing that transform will reset the element, often to a bizarre location or size compared to the rest of the drawing. 

Since SVG and CSS transforms are the same, any CSS transform you do replaces the SVG transform, meaning your CSS transform will start from that bizarre location or size rather than the location or size that is set in the the SVG.

You can copy the transform from the SVG element to your CSS and set it as the starting position in CSS (updating it to the CSS syntax first, of course). You can then modify it in your CSS animation.

For instance, in Uhhh, Yeah…, my tribute to Office Space, Undead Lumbergh’s right upper arm (the #arm2 element) had a transform on it in the original SVG code.

<path id="arm2" fill="#91c1a3" fill-rule="nonzero" d="M0 171h9v9H0z" transform="translate(0 -343) scale(4 3.55)"/>
A side by side comparison of a zombie dressed in a blue button-up shirt and black suspenders while holding a coffee cup. On the left, the arm holding the coffee mugs the the correct position but the right shows the arm detached from the body.

Moving that transform to CSS like this:

<path id="arm2" fill="#91c1a3" fill-rule="nonzero" d="M0 171h9v9H0z"/>
#arm2 {
  transform: translate(0, -343px) scale(4, 3.55);
}

…I could then create an animation that doesn’t accidentally reset the location and scale:

.toggle-checkbox:checked ~ .z #arm2 { 
  animation: 6s ease-in-out 0.15s arm2move forwards;
}

@keyframes arm2move {
  0%, 100% {
    transform: translate(0, -343px) scale(4, 3.55);
  }
  40%, 60% {
    transform: translate(0, -403px) scale(4, 3.55);
  }
  50% {
    transform: translate(0, -408px) scale(4, 3.55);
  }
} 

This process is harder when the tool generating the SVG code attempts to “simplify” the transform into a matrix. While you can recreate the matrix transform by copying it into the CSS, it is a difficult task to do. You’re a better developer than me — which might be true anyway — if you can take a matrix transform and manipulate it to scale, rotate, or translate in the exact way you want.

Alternatively, you can recreate the matrix transform using translation, rotation, and scaling, but if the path is complex, the likelihood that you can recreate it in a timely manner without finding yourself in a straight jacket is low. 

The last and probably easiest option is to wrap the element in a group (<g>) tag. Add a class or ID to it for easy CSS access and transform the group itself, thus separating out the transforms as discussed in the last lesson. 

Lesson 8: Keep your sanity by using transform-origin when transforming part of an SVG

The CSS transform-origin property moves the point around which the transform happens. If you’re trying to rotate an arm — like I did in Clubbin’ It —  your animation will look more natural if you rotate the arm from the center of the shoulder, but that path’s natural transform origin is in the upper-left. Use transform-origin to fix this for smoother, more natural feel… you know that really natural pixel art look…

Four sequential frames of an animation showing a caveman character facing left, holding a large wooden club, and raising it up from the bottom to behind his head.

Transforming the origin can also be useful when scaling, like I did in Mustachioed Oops, or when rotating mouth movements, such as the dinosaur’s jaw in Super Tasty. If you don’t change the origin, the transforms will use an origin point at the upper left corner of the SVG element. 

Lesson 9: Sprite animations can be responsive

I ended up doing a lot of sprite animations for this project (i.e., where you use multiple, incremental frames and switch between them fast enough that the characters seem to move). I created the images in one wide file, added them as a background image to an element the size of a single frame, used background-size to set the background image to the width of the image, and hid the overflow. Then I used background-position and the animation timing function, step(), to walk through the images; for example: Post-Apocalyptic Celebrations.

Before the project, I always used inflexible images. I’d scale things down a little so that there would be at least a little responsive give, but I didn’t think you could make it a fully flexible width. However, if you use SVG as the background image you can then use viewport units to scale the element along with the changing screen size. The only problem is the background position. However, if you use viewport units for that, it will stay in sync. Check that out in Finally, Alone with my Sandwich…

Lesson 9A: Use viewport units to set the background size of an image when creating responsive sprite animation

As I’ve learned throughout this project, using a single type of unit  is almost always the way to go. Initially, I’d set my sprite’s background size using percentages. The math was easy (100% * (number of steps + 1)) and it worked fine in most cases. In longer animations, however, the exact frame tracking could be off and parts of the wrong sprite frame might display. The problem grows as more frames are added to the sprite. 

I’m not sure the exact reason this causes an issue, but I believe it’s because of rounding errors that compound over the length of the sprite sheet (the amount of the shift increases with the number of frames). 

For my final animation, It Ain’t Over Till the Zombie Sings, I had a dinosaur open his mouth to reveal a zombie Viking singing (while lasers fired in the background plus there was dancing, accordions playing and zombies fired from cannons, of course). Yeah, I know how to throw a party… a nerd party.

The dinosaur and viking was one of the longest sprite animations I did for the project. But when I used percentages to set the background size, the tracking would be off at certain sizes in Safari. By the end of the animation, part of the dinosaur’s nose from a different frame would appear to the right and a similar part of the nose would be missing on the left.

A large green dinosaur behind a crowd of people, all facing and looking forward.
The dinosaur on the left is missing part of his left cheek and growing a new one next to his right cheek.

This was super frustrating to diagnose because it seemed to work fine in Chrome and I’d think I fixed it in Safari only to look at a slightly different screen size and see the frame off again. However, if I used consistent units — i.e. vw for background-size, frame width, and background-position — everything worked fine. Again, it comes down to working with consistent units!

Lesson 10: Invite people into the project.

A crowd of 32 pixel-art characters from the previous demos facing the screen.

While I learned tons of things during this process, I beat my head against the wall for most of it (often until the wall broke or my head did… I can’t tell). While that’s one way to do it, even if you’re hard-headed, you’ll still end up with a headache. Invite others into your project, be it for advice, to point out an obvious blind spot you missed, provide feedback, help with the project, or simply to encourage you to keep going when the scope is stupidly and arbitrarily large. 

So let me put this lesson into practice. What are your thoughts? How will you stop the zombie hordes with CSS animation? What stupidly and arbitrarily large project will you take on to stretch yourself?


The post Lessons Learned from Sixty Days of Re-Animating Zombies with Hand-Coded CSS appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Quoting in HTML: Quotations, Citations, and Blockquotes

It’s all too common to see the incorrect HTML used for quotes in markup. In this article, let’s dig into all this, looking at different situations and different HTML tags to handle those situations.

There are three major HTML elements involved in quotations:

  • <blockquote>
  • <q>
  • <cite>

Let’s take a look.

Blockquotes

Blockquote tags are used for distinguishing quoted text from the rest of the content. My tenth grade English teacher drilled it into my head that any quote of four lines or longer should be set apart this way. The HTML spec has no such requirement, but as long as the text is a quote and you want it to be set apart from the surrounding text and tags, a blockquote is the semantic choice.

By default, browsers indent blockquotes by adding margin on each side.

See the Pen
The Blockquote Tag
by Undead Institute (@undeadinstitute)
on CodePen.

As a flow element (i.e. “block level” element), blockquote can contain other elements inside it. For example, we can drop paragraphs in there with no problem:

<blockquote>
  <p></p>
  <p></p>
</blockquote>

But it could be other elements, too, like a heading or an unordered list:

<blockquote>
    <h2></h2>
    <ul>
      <li></li>
      <li></li>
    </ul>
</blockquote>

It’s important to note that blockquotes should only be used for quotations rather than as a decorative element in a design. This also aids accessibility as screen reader users can jump between blockquotes. Thus a blockquote element used solely for aesthetics could really confuse those users. If you need something more decorative that falls outside the bounds of extended quotations, then perhaps a div with a class is the way to go.

blockquote,
.callout-block {
  /* These could share styling */
}

Quoting with Q

Q tags (<q>) are for inline quotes, or what my tenth grade teacher would say are those under four lines. Many modern browsers will automatically add quotation marks to the quote as pseudo elements but you may need a backup solution for older browsers.

See the Pen
The Q Tag
by CSS-Tricks (@css-tricks)
on CodePen.

Typical quotation marks are just as valid for inline quotes as the <q> element. The benefits of using <q>, however, are that it includes a cite attribute, automatic handling of quotation marks, and automatic handling of quote levels. <q> elements should not used for sarcasm (e.g. “you would use a <q> tag for sarcasm, wouldn’t you?”), or signifying a word with air quotes (e.g. “awesome” is an “accurate” description of the author). But if you can figure out how to mark up air quotes, please let me know. Because that would be “awesome.”

The citation attribute

Both <q> and blockquotes can use a citation (cite) attribute. This attribute holds a URL that provides context and/or a reference for the quoted material. The spec makes a point of saying that the URL can be surrounded by spaces. (I’m not sure why that’s pointed out, but if you want to anger the semantic code deities, you’ll have to do more than throw spaces around.)

<p>The officer left a note saying <q cite="https://johnrhea.com/summons">You have been summoned to appear on the 4th day of January on charges of attempted reader bribery.</q></p>

That cite attribute isn’t visible to the user by default. You could add it in with a sprinkle of CSS magic like the following demo. You could even fiddle with it further to make the citation appear on hover.

See the Pen
Attributable citations
by CSS-Tricks (@css-tricks)
on CodePen.

Neither of those options are particularly great. If you need to cite a source such that users can see it and go to it, you should do it in HTML and probably with the <cite> element, which we’ll cover next.

The citation element

The <cite> tag should be used for referencing creative work rather than the person who said or wrote the quote. In other words, it’s not for quotes. Here are the examples from the spec:

<p>My favorite book is <cite>The Reality Dysfunction</cite> by
Peter F. Hamilton. My favorite comic is <cite>Pearls Before
Swine</cite> by Stephan Pastis. My favorite track is <cite>Jive
Samba</cite> by the Cannonball Adderley Sextet.</p>

Here’s another example:

See the Pen
Cite This!
by CSS-Tricks (@css-tricks)
on CodePen.

If the author of this article told you he’d give you a cupcake, and you <cite> him by name, that would be semantically incorrect. Thus no cupcakes would change hands. If you cited the article in which he offered you a cupcake, that would be semantically correct, but since the author wouldn’t do that, you still wouldn’t get a cupcake. Sorry.

By default, browsers italicize cite tags and there’s no requirement that a <q> or <blockquote> be present to use the cite element. If you want to cite a book or other creative work, then slap it in the cite element. The semantic deities will smile on you for not using either <i> or <em> elements.

But where to put the cite element? Inside? Outside? The upside down? If we put it inside the <blockquote> or the <q>, we’re making it part of the quote. That's forbidden by the spec for just that reason.

<!-- This is apparently wrong -->
<blockquote>
  Quote about cupcake distribution from an article
  <cite>The Article</cite>
</blockquote>

Putting it outside just feels wrong and also requires you to have an enclosing element like a <div> if you wanted to style them together.

<div class="need-to-style-together">
  <blockquote>
    Quote about cupcake distribution from an article
  </blockquote>
  <cite>The Article</cite>
</div>

N.B. If you google this issue you may come across an HTML5 Doctor article from 2013 that contradicts much of what's laid out here. That said, every time it links to the docs for support, the docs agree with the article you're currently reading rather than the HTML5 Doctor article. Most likely the docs have changed since that article was written.

Hey, what about the figure element?

One way to mark up a quotation — and in a way that pleases the semantic code deities — is to put the blockquote within a figure element. Then, put the cite element and any other author or citation information in a figcaption.

<figure class="quote">
  <blockquote>
    But web browsers aren’t like web servers. If your back-end code is getting so big that it’s starting to run noticably slowly, you can throw more computing power at it by scaling up your server. That’s not an option on the front-end where you don’t really have one run-time environment—your end users have their own run-time environment with its own constraints around computing power and network connectivity.
  </blockquote>
  <figcaption>
    &mdash; Jeremy Keith, <cite>Mental models</cite>
  </figcaption>
</figure>

While this doubles the number of elements needed, there are several benefits:

  1. It’s semantically correct for all four elements.
  2. It allows you to both include and encapsulate author information beyond citing the name of the work.
  3. It gives you an easy way to style the quote without resorting to divs, spans or wretchedness.

See the Pen
It Figures You'd Say That
by CSS-Tricks (@css-tricks)
on CodePen.

None of this is for dialogue

Not <dialog>! Those are for attention-grabbing modals. Dialogue, as in, conversational exchanges between people speaking or typing to each other.

Neither <blockquote> nor <q> nor <cite> are to be used for dialogue and similar exchanges between speakers. If you’re marking up dialogue, you can use whatever makes the most sense to you. There’s no semantic way to do it. That said, the spec suggests <p> tags and punctuation with <span> or <b> tags to designate the speaker and <i> tags to mark stage directions.

Accessibility of quotes

From the research I’ve done, screen readers should not have any issue with understanding semantic-deity-approved <q>, <blockquote>, or <cite> tags.

[VIDEO]

More “ways” to “quote”

You can add quotation marks to a <blockquote> using CSS pseudo elements. The <q> element comes with quotation marks baked in so they need not be added, however adding them as pseudo-elements can be a workaround for older browsers that don’t automatically add them. Since this is how modern browsers add the quotation marks there's no danger of adding duplicate quotes. New browsers will overwrite the default pseudo elements, and older browsers that support pseudo elements will add the quotes.

But you can’t, like I did, assume that the above will always give you smart opening and closing quotes. Even if the font supports smart quotes, sometimes straight quotes will be displayed. To be safe, it’s better to use the quotes CSS property to up the intelligence on those quotation marks.

blockquote {
  quotes: "“" "”" "‘" "’";
}

See the Pen
"Quot-a-tizing" the blockquote
by CSS-Tricks (@css-tricks)
on CodePen.

Multi-level quoting

Now let’s look at quote levels. The <q> tag will automatically adjust quote levels.

Let’s say you’re in an area that uses the British convention of using single quotes. You could use the CSS quotes rule to put the opening and closing single quotes first in the list. Here’s an example of both ways:

See the Pen
Quote Within a Quote
by CSS-Tricks (@css-tricks)
on CodePen.

There is no limit to nesting. Those nested <q> elements could even be within a blockquote that’s within a blockquote.

If you add quotation marks to a blockquote, know that the blockquote does not change the quote level the way a <q> tag does. If you expect to have quotes within a blockquote, you may want to add a descendant selector rule to start <q> elements within a blockquote at the single quote level (or double quotes if you follow British conventions).

 blockquote q {
  quotes: "‘" "’" "“" "”";
}

The last quote level you put in will continue through subsequent levels of quotation. To use the double, single, double, single… convention, add more levels to the CSS quotes property.

q {
  quotes: "“" "”" "‘" "’" "“" "”" "‘" "’" "“" "”";
}

Hanging punctuation

Many typography experts will tell you that hanging the quotation marks on blockquotes looks better (and they’re right). Hanging punctuation is, in this case, quotation marks that are pushed out from the text so that the characters of the text line up vertically.

One possibility in CSS is using a slightly negative value on the text-indent property. The exact negative indentation will vary by font, so be sure to double check the spacing with the font you end up using.

blockquote {
  text-indent: -0.45em;
}

There is a nicer way to handle this by using the hanging-punctuation CSS property. It’s only supported in Safari at the time of this writing, so we’ll have to progressively enhance:

/* Fallback */
blockquote {
  text-indent: -0.45em;
}

/* If there's support, erase the indent and use the property instead */
@supports ( hanging-punctuation: first) {
  blockquote {
    text-indent: 0;
    hanging-punctuation: first;
  }
}

Using hanging-punctuation is better because it’s less fiddly. It’ll just work when it can.

See the Pen
Hanging Your Punctuation
by CSS-Tricks (@css-tricks)
on CodePen.

Can we animate quotation marks?

Of course we can.

See the Pen
Dancing Quotes
by CSS-Tricks (@css-tricks)
on CodePen.

Why you’d need to do this, I’m not totally sure, but the quotation marks in a <q> tag are added are pseudo elements in the UA stylesheet, so we’re able to select and style them — including animation — if we need to.

Wait, maybe we just solved the air quotes thing after all.

The post Quoting in HTML: Quotations, Citations, and Blockquotes appeared first on CSS-Tricks.

Great Expectations: Using Story Principles To Anticipate What Your User Expects

Great Expectations: Using Story Principles To Anticipate What Your User Expects

Great Expectations: Using Story Principles To Anticipate What Your User Expects

John Rhea

Whether it’s in a novel, the latest box office smash, or when Uncle Elmer mistook a potted cactus for a stress ball, we all love stories. There are stories we love, stories we hate, and stories we wish we’d never experienced. Most of the good stories share structure and principles that can help us create consistent website experiences. Experiences that speak to user expectations and guide them to engage with our sites in a way that benefits both of us.

In this article, we’ll pull out and discuss just a few examples of how thinking about your users’ stories can increase user engagement and satisfaction. We’ll look at deus ex machina, ensemble stories, consistency, and cognitive dissonance, all of which center on audience expectations and how your site is meeting those expectations or not.

We can define a story as the process of solving a problem. Heroes have an issue, and they set out on a quest to solve it. Sometimes that’s epic and expansive like the Lord of the Rings or Star Wars and sometimes it’s small and intimate such as Driving Miss Daisy or Rear Window. At its core, every story is about heroes who have a problem and what they do to solve it. So too are visits to a website.

The user is the hero, coming to your site because they have a problem. They need to buy a tchotchke, hire an agency or find the video game news they like. Your site can solve that problem and thus play an important role in the user’s story.

Deus Ex Machina

It’s a term meaning “god from the machine” that goes back to Greek plays — even though it’s Latin — when a large, movable scaffolding or “machine” would bring out an actor playing a god. In the context of story, it’s often used to describe something that comes out of nowhere to solve a problem. It’s like Zeus showing up at the end of a play and killing the villain. It’s not satisfying to the audience. They’ve watched the tension grow between the hero and the villain and feel cheated when Zeus releases the dramatic tension without solving that tension. They watched a journey that didn’t matter because the character they loved did not affect the ending.

The danger of deus ex machina is most visible in content marketing. You hook the audience with content that’s interesting and applicable but then bring your product/site/whatever in out of nowhere and drop the mic like you won a rap battle. The audience won’t believe your conclusion because you didn’t journey with them to find the solution.

If, however, the author integrates Zeus into the story from the beginning, Zeus will be part of the story and not a convenient plot device. Your solutions must honor the story that’s come before, the problem and the pain your users have experienced. You can then speak to how your product/site/whatever solves that problem and heals that pain.

State Farm recently launched a “Don’t Mess With My Discount!” campaign:

Kim comes in to talk to a State Farm rep who asks about a Drive Safe and Save discount. First, for the sake of the discount, Kim won’t speed up to make a meeting. Next, she makes herself and her child hold it till they can get home driving the speed limit. Last, in the midst of labor, she won’t let her partner speed up to get them to the hospital. (Don’t mess with a pregnant lady or her discount.) Lastly, it cuts back to Kim and the agent.

State Farm’s branding and their signature red color are strong presences in both bookend scenes with the State Farm representative. By the end, when they give you details about their “Drive Safe and Save” discount you know who State Farm is, how they can help you, and what you need to do to get the discount.

It’s not a funny story that’s a State Farm commercial in disguise, but a State Farm commercial that’s funny.

Throughout the ad, we know State Farm’s motivations and don’t feel duped into liking something whose only goal is to separate us from our money. They set the expectation of this story being an ad in the beginning and support that throughout.

Another Approach

Sometimes putting your name upfront in the piece might feel wrong or too self-serving. Another way to get at this is to acknowledge the user’s struggle, the pain the user or customer already feels. If your site doesn’t acknowledge that struggle, then your product/site/whatever seems detached from their reality, a deus ex machina. But if your content recognizes the struggle they’ve been through and how your site can solve their problem, the pitch for deeper engagement with your site will be a natural progression of the user’s story. It will be the answer they’ve been searching for all along.

Take this testimonial from Bizzabo:

Emily Fullmer, Director of Global Events for Greenbook said, We are now able to focus less on tedious operations, and more on creating a memorable and seamless experience for our attendees.
Bizzabo solved a real world problem for Greenbook. (Large preview)

It shows the user where Greenbook was, i.e. mired in tedious tasks, and how Bizzabo helped them get past tedium to do what Greenbook says they do best: make memorable experiences. Bizzabo doesn’t come out of the woodwork to say “I’m awesome” or solve a problem you never had. They have someone attesting to how Bizzabo solved a real problem that this real customer needed to be fixed. If you’re in the market to solve that problem too, Bizzabo might be the place to look.

Ensemble Stories

Some experiences, like some stories, aren’t about a single person. They’re about multiple people. If the story doesn’t give enough attention to each member, that person won’t seem important or like a necessary part of the story. If that person has a role in the ending, we feel cheated or think it’s a deus ex machina event. If any character is left out of a story, it should change the story. It’s the same way with websites. The user is the story’s hero, but she’s rarely the only character. If we ignore the other characters, they won’t feel needed or be interested in our websites.

Sometimes a decision involves multiple people because a single user doesn’t have the authority to decide. For instance, Drupalcon Seattle 2019 has a “Convince Your Boss” page. They showcase the benefits of the conference and provide materials to help you get your boss to agree to send you.

You could also offer a friends-and-family discount that rewards both the sharer and the sharee. (Yes, as of this moment, “sharee” is now a word.) Dropbox does this with their sharing program. If you share their service with someone else and they create an account, you get additional storage space.

Dropbox offers an additional 250 MB of space for every friend you get to join their service.
You get extra space, you get extra space, and you get extra space (when you invite a friend). (Large preview)

But you don’t have to be that explicit about targeting other audiences than the user themselves. In social networks and communities, the audience is both the user and their friends. The site won’t reach a critical mass if you don’t appeal to both. I believe Facebook beat MySpace early on by focusing on the connection between users and thus serving both the user and their friends. MySpace focused on individual expression. To put it another way, Facebook included the user’s friends in their audience while MySpace didn’t.

Serving Diametrically Opposed Heros

Many sites that run on ad revenue also have to think about multiple audiences, both the users they serve and the advertisers who want to reach those users. They are equally important in the story, even if their goals are sometimes at odds. If you push one of these audiences to the side, they’ll feel like they don’t matter. When all you care about is ad revenue, users will flee because you’re not speaking to their story any longer or giving them a good experience. If advertisers can’t get good access to the user then they won’t want to pay you for ads and revenue drops off.

Just about any small market newspaper website will show you what happens when you focus only on advertisers’ desires. Newspaper revenue streams have gone so low they have to push ads hard to stay alive. Take, for instance, the major newspaper from my home state of Delaware, the News Journal. The page skips and stutters as ad content loads. Click on any story and you’ll find a short article surrounded by block after block after block of ad content. Ads are paying the bills but with this kind of user experience, I fear it won’t be for long.

Let me be clear that advertisers and users do not have to be diametrically opposed, it’s just difficult to find a balance that pleases both. Sites often lean towards one or the other and risk tipping the scales too far either way. Including the desires of both audiences in your decisions will help you keep that precarious balance.

One way to do both is to have ads conform to the essence of your website, meaning the thing that makes your site different i.e. the “killer app” or sine qua non of your website. In this way, you get ads that conform to the reason the users are going to the site. Advertisers have to conform to the ad policy, but, if it really hits on the reason users are going to the site, advertisers should get much greater engagement.

On my own site, 8wordstories.com, ads are allowed, but they’re only allowed an image, eight words of copy, and a two-word call to action. Thus when users go to the site to get pithy stories, eight words in length, the advertisements will similarly be pithy and short.

Advertisers and users do not have to be diametrically opposed, it’s just difficult to find a balance that pleases both.

Consistency

The hero doesn’t train as a medieval knight for the first half of the story and then find herself in space for the second half. That drastic shift can make the audience turn on the story for dashing their expectations. They think you did a bait-and-switch, showing them the medieval story they wanted and then switching to a space story they didn’t want.

If you try to hook users with free pie, but you sell tubas, you will get lots of pie lovers and very few tuba lovers. Worse yet is to have the free pie contingent on buying a tuba. The thing they want comes with a commitment or price tag they don’t. This happens a lot with a free e-book when you have to create an account and fill out a lengthy form. For me, that price has often been too high.

Make sure the way you’re hooking the audience is consistent with what you want them to read, do, or buy. If you sell tubas offer a free tuba lesson or polishing cloth. This’ll ensure they want what you provide and they’ll think of you the next time they need to buy a tuba.

That said, it doesn’t mean you can’t offer free pie, but it shouldn’t get them in the door, it should push them over the edge.

Audible gives you a thirty-day free trial plus an audio book to keep even if you don’t stay past the trial. They’re giving you a taste of the product. When you say, “I want more.” You know where to get it.

While not offering a freebie, Dinnerly (and most of the other bazillion meal kit delivery companies) offers a big discount on your first few orders, encouraging new customers to try them out. This can be an especially good model for products or services that have fixed costs with enticing new customers.

Dinnerly offers a discount on each of your first three meal kits.
Hmmm… maybe they should offer free pie. (Large preview)

Cognitive Dissonance

There’s another danger concerning consistency, but this one’s more subtle. If you’re reading a medieval story and the author says the “trebuchet launched a rock straight and true, like a spaceship into orbit.” It might be an appropriate allusion for a modern audience, but it’s anachronistic in a medieval story, a cognitive dissonance. Something doesn’t quite make sense or goes against what they know to be true. In the same way, websites that break the flow of their content can alienate their audience without even meaning to (such as statistics that seem unbelievable or are so specific anyone could achieve them).

112% of people reading this article are physically attractive.

(Here’s lookin’ at you, reader.)

This article is the number one choice by physicians in Ohio who drive Yugos.

(Among other questions, why would a European car driving, Ohioan Doctor read a web user experience article?)

These “statistics” break the flow of the website because they make the user stop and wonder about the website’s reputability. Any time a user is pulled out of the flow of a website, they must decide whether to continue with the website or go watch cat videos.

Recently, I reviewed proposals for a website build at my day job. The developers listed in the proposal gave me pause. One with the title “Lead Senior Developer” had seven years of experience. That seemed low for a “lead, senior” developer, but possible. The next guy was just a “web developer” but had twenty years of experience. Even if that’s all correct, their juxtaposition made them look ridiculous. That cognitive dissonance pulled me out of the flow of the proposal and made me question the firm’s abilities.

Similarly poor quality photos, pixelated graphics, unrelated images, tpyos, mispelllings, weird bolding and anything else that sticks out potato can cause cognitive dissonance and tank a proposal or website (or article). The more often you break the spell of the site, the harder it will be for clients/users to believe you/your product/site/thing are as good as you say. Those cat videos will win every time because they always meet the “lolz” expectation.

Conclusion

Users have many expectations when they come to your site. Placing your users in the context of a story helps you understand those expectations and their motivations. You’ll see what they want and expect, but also what they need. Once you know their needs, you can meet those needs. And, if you’ll pardon my sense of humor, you can both …live happily ever after.

Smashing Editorial (cct, ra, yk, il)

The User’s Perspective: Using Story Structure To Stand In Your User’s Shoes

The User’s Perspective: Using Story Structure To Stand In Your User’s Shoes

The User’s Perspective: Using Story Structure To Stand In Your User’s Shoes

John Rhea

Every user interaction with your website is part of a story. The user—the hero—finds themselves on a journey through your website on the way to their goal. If you can see this journey from their perspective, you can better understand what they need at each step, and align your goals with theirs.

My first article on websites and story structure, Once Upon a Time: Using Story Structure for Better Engagement, goes into more depth on story structure (the frame around which we build the house of a story) and how it works. But here’s a quick refresher before we jump into implications:

The Hero’s Journey

Most stories follow a simple structure that Joseph Campbell in his seminal work, Hero with a Thousand Faces, called the Hero’s Journey. We’ll simplify it to a hybrid of the plot structure you learned in high school and the Hero’s Journey. We’ll then take that and apply it to how a user interacts with a website.

The Hero’s journey begins in the ordinary world. An inciting incident happens to draw the hero into the story. The hero prepares to face the ordeal/climax. The hero actually faces the ordeal. Then the hero must return to the ordinary world, his problem solved by the story.
Once upon a time… a hero went on a journey. (Large preview)
Ordinary World The ordinary world is where the user starts (their every day) before they’ve met your website.
Inciting Incident/Call To Adventure Near the beginning of any story, something will happen to the hero that will push (or pull) them into the story (the inciting incident/call to adventure). It will give them a problem they need to resolve. Similarly, a user has a problem they need to be solved, and your website might be just the thing to solve it. Sometimes though, a hero would rather stay in their safe, ordinary world. It’s too much cognitive trouble for the user to check out a new site. But their problem — their call to adventure — will not be ignored. It will drive the user into interacting with your site.
Preparation/Rising Action They’ve found your website and they think it might work to solve their problem, but they need to gather information and prepare to make a final decision.
The Ordeal/Climax In stories, the ordeal is usually the fight with the big monster, but here it’s the fight to decide to use your site. Whether they love the video game news you cover or need the pen you sell or believe in the graphic design prowess of your agency, they have to make the choice to engage.
The Road Back/Falling Action Having made the decision to engage, the road back is about moving forward with that purchase, regular reading, or requesting the quote.
Resolution Where they apply your website to their problem and their problem is *mightily* solved!
Return With Elixir The user returns to the ordinary world and tells everyone about their heroic journey.

The User’s Perspective

Seeing the website from the user’s perspective is the most important part of this. The Hero’s Journey, as I use it, is a framework for better understanding your user and their state of mind during any given interaction on your site. If you understand where they are in their story, you can get a clearer picture of where and how you fit in (or don’t) to their story. Knowing where you are or how you can change your relationship to the user will make a world of difference in how you run your website, email campaigns, and any other interaction you have with them.

Numerous unsubscribes might not be a rejection of the product, but that you sent too many emails without enough value. Great testimonials that don’t drive engagement might be too vague or focused on how great you are, not what solutions you solve. A high bounce rate on your sign up page might be because you focused more on your goals and not enough on your users’ goals. Your greatest fans might not be talking about you to their friends, not because they don’t like you, but because you haven’t given them the opportunity for or incentivized the sharing. Let’s look at a few of these problems.

Plan For The Refusal Of The Call To Adventure

Often the hero doesn’t want to engage in the story or the user doesn’t want to expend the cognitive energy to look at another new site. But your user has come to your site because of their call to adventure—the problem that has pushed them to seek a solution—even if they don’t want to. If you can plan for a user’s initial rejection of you and your site, you’ll be ready to counteract it and mollify their concerns.

Follow up or reminder emails are one way to help the user engage. This is not a license to stuff your content down someone’s throat. But if we know that one or even seven user touches aren’t enough to draw someone in and engage them with your site, you can create two or eight or thirty-seven user touches.

Sometimes these touches need to happen outside of your website; you need to reach out to users rather than wait for them to come back to you. One important thing here, though, is not to send the same email thirty-seven times. The user already refused that first touch. The story’s hero rarely gets pulled into the story by the same thing that happens again, but rather the same bare facts looked at differently.

So vary your approach. Do email, social media, advertising, reward/referral programs, and so on. Or use the same medium with a different take on the same bare facts and/or new information that builds on the previous touches. Above all, though, ensure every touch has value. If it doesn’t, each additional touch will get more annoying and the user will reject your call forever.

Nick Stephenson is an author who tries to help other authors sell more books. He has a course called Your First 10K Readers and recently launched a campaign with the overall purpose of getting people to register for the course.

Before he opened registration, though, he sent a series of emails. The first was a thanks-for-signing-up-to-the-email-list-and-here’s-a-helpful-case-study email. He also said he would send you the first video in a three-part series in about five minutes. The second email came six minutes later and had a summary of what’s covered in the video and a link to the video itself. The next day he emailed with a personal story about his own struggles and a link to an article on why authors fail (something authors are very concerned about). Day 3 saw email number four… you know what? Let’s just make a chart.

Day Value/Purpose Email #
1 Case Study 1
1 Video 1 of 3 2
2 Personal Story and Why Authors Fail Article 3
3 Video 2 of 3 4
4 Honest discussion of his author revenue and a relevant podcast episode 5
5 Video 3 of 3 6
6 Testimonial Video 7
7 Registration Opens Tomorrow 8
8 Registration Info and a pitch on how working for yourself is awesome 9

By this point, he’s hooked a lot of users. They’ve had a week of high quality, free content related to their concerns. He’s paid it forward and now they can take it to the next level.

I’m sure some people unsubscribed, but I’m also sure a lot more people will be buying his course than with one or even two emails. He’s given you every opportunity to refuse the call and done eight different emails with resources in various formats to pull you back in and get you going on the journey.

I’ve Traveled This Road Before

It takes a lot less work to follow a path than to strike a new one. If you have testimonials, they can be signposts in the wilderness. While some of them can and should refer to the ordeal (things that might prevent a user from engaging with you), the majority of them should refer to how the product/website/thing will solve whatever problem the user set out to solve.

“This article was amazing!” says the author’s mother, or “I’m so proud of how he turned out… it was touch-and-go there for a while,” says the author’s father. While these are positive (mostly), they aren’t helpful. They tell you nothing about the article.

Testimonials should talk about the road traveled: “This article was awesome because it helped me see where we were going wrong, how to correct course, and how to blow our competitor out of the water,” says the author’s competitor. The testimonials can connect with the user where they are and show them how the story unfolded.

This testimonial for ChowNow talks about where they’ve been and why ChowNow worked better than their previous setup.

“Life before ChowNow was very chaotic — we got a lot of phone calls, a lot of mistyped orders. So with ChowNow, the ability to see the order from the customer makes it so streamlined.” John Sungkamee, Owner, Emporium Thai Cuisine
“I struggled with the same things you did, but this website helped me through.” (Large preview)

So often we hear a big promise in testimonials. “Five stars”, “best film of the year,” or “my son always does great.” But they don’t give us any idea of what it took to get where they are, that special world the testifier now lives in. And, even if that company isn’t selling a scam, your results will vary.

We want to trumpet our best clients, but we also want to ground those promises in unasterisked language. If we don’t, the user’s ordeal may be dealing with our broken promises, picking up the pieces and beginning their search all over again.

The Ordeal Is Not Their Goal

While you need to help users solve any problems preventing them from choosing you in their ordeal, the real goal is for them to have their problem solved. It’s easy to get these confused because your ordeal in your story is getting the user to buy in and engage with your site.

Your goal is for them to buy in/engage and your ordeal is getting them to do that. Their goal is having their problem solved and their ordeal is choosing you to solve that problem. If you conflate your goal and their goal then their problem won’t get solved and they won’t have a good experience with you.

This crops up whenever you push sales and profits over customer happiness. Andrew Mason, founder of Groupon, in his interview with Alex Bloomberg on the podcast “Without Fail”, discusses some of his regrets about his time at Groupon. The company started out with a one-deal-a-day email — something he felt was a core of the business. But under pressure to meet the growth numbers their investors wanted (their company goals), they tried things that weren’t in line with the customer’s goals.

Note: I have edited the below for length and clarity. The relevant section of the interview starts at about 29:10.

Alex: “There was one other part of this [resignation] letter that says, ‘my biggest regrets are the moments that I let a lack of data override my intuition on what’s best for our company’s customers.’ What did you mean by that?”

Andrew: “Groupon started out with these really tight principles about how the site was going to work and really being pro customer. As we expanded and as we went after growth at various points, people in the company would say, ‘hey why don’t we try running two deals a day? Why don’t we start sending two emails a day?’ And I think that sounds awful, like who wants that? Who wants to get two emails every single day from a company? And they'd be like, ‘Well sure, it sounds awful to you. But we’re a data driven company. Why don’t we let the data decide?’ ...And we’d do a test and it would show that maybe people would unsubscribe at a slightly higher rate but the increase in purchasing would more than make up for it. You'd get in a situation like: ‘OK, I guess we can do this. It doesn’t feel right, but it does seem like a rational decision.’ ...And of course the problem was when you’re in hypergrowth like [we were] ...you don’t have time to see what is going to happen to the data in the long term. The churn caught up with [us]. And people unsubscribed at higher rates and then before [we] knew it, the service had turned into… a vestige of what it once was.”

Without Fail, Groupon's Andrew Mason: Pt. 2, The Fall (Oct. 8, 2018)

Tools For The Return With The Elixir

Your users have been on a journey. They’ve conquered their ordeal and done what you hoped they would, purchased your product, consumed your content or otherwise engaged with you. These are your favorite people. They’re about to go back to their ordinary world, to where they came from. Right here at this pivot is when you want to give them tools to tell how awesome their experience was. Give them the opportunity to leave a testimonial or review, offer a friends-and-family discount, or to share your content.

SunTrust allows electronic check deposit through their mobile app. For a long while, right after a deposit, they would ask you if you wanted to rate their app. That’s the best time to ask. The user has just put money in their account and are feeling the best they can while using the app.

suntrust app check deposit screen
“Money, money, money! Review us please?” (Large preview)

The only problem was is that they asked you after every deposit. So if you had twelve checks to put in after your birthday, they’d ask you every time. By the third check number, this was rage inducing and I’m certain they got negative reviews. They asked at the right time, but pestered rather than nudged and — harkening back to the refusal of the call section — they didn’t vary their approach or provide value with each user touch.

Note: Suntrust has since, thankfully, changed this behavior and no longer requests a rating after every deposit.

Whatever issue you’re trying to solve, the Hero’s Journey helps you see through your user’s eyes. You’ll better understand their triumphs and pain and be ready to take your user interactions to the next level.

So get out there, put on some user shoes, and make your users heroic!

Smashing Editorial (cc, il)