web components – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Mon, 13 Feb 2023 15:10:44 +0000 en-US hourly 1 https://wordpress.org/?v=6.2.2 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 web components – CSS-Tricks https://css-tricks.com 32 32 45537868 An Approach to Lazy Loading Custom Elements https://css-tricks.com/an-approach-to-lazy-loading-custom-elements/ https://css-tricks.com/an-approach-to-lazy-loading-custom-elements/#comments Mon, 13 Feb 2023 15:10:41 +0000 https://css-tricks.com/?p=376991 We’re fans of Custom Elements around here. Their design makes them particularly amenable to lazy loading, which can be a boon for performance.

Inspired by a colleague’s experiments, I recently set about writing a simple auto-loader: Whenever a custom …


An Approach to Lazy Loading Custom Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We’re fans of Custom Elements around here. Their design makes them particularly amenable to lazy loading, which can be a boon for performance.

Inspired by a colleague’s experiments, I recently set about writing a simple auto-loader: Whenever a custom element appears in the DOM, we wanna load the corresponding implementation if it’s not available yet. The browser then takes care of upgrading such elements from there on out.

Chances are you won’t actually need all this; there’s usually a simpler approach. Used deliberately, the techniques shown here might still be a useful addition to your toolset.

For consistency, we want our auto-loader to be a custom element as well — which also means we can easily configure it via HTML. But first, let’s identify those unresolved custom elements, step by step:

class AutoLoader extends HTMLElement {
  connectedCallback() {
    let scope = this.parentNode;
    this.discover(scope);
  }
}
customElements.define("ce-autoloader", AutoLoader);

Assuming we’ve loaded this module up-front (using async is ideal), we can drop a <ce-autoloader> element into the <body> of our document. That will immediately start the discovery process for all child elements of <body>, which now constitutes our root element. We could limit discovery to a subtree of our document by adding <ce-autoloader> to the respective container element instead — indeed, we might even have multiple instances for different subtrees.

Of course, we still have to implement that discover method (as part of the AutoLoader class above):

discover(scope) {
  let candidates = [scope, ...scope.querySelectorAll("*")];
  for(let el of candidates) {
    let tag = el.localName;
    if(tag.includes("-") && !customElements.get(tag)) {
      this.load(tag);
    }
  }
}

Here we check our root element along with every single descendant (*). If it’s a custom element — as indicated by hyphenated tags — but not yet upgraded, we’ll attempt to load the corresponding definition. Querying the DOM that way might be expensive, so we should be a little careful. We can alleviate load on the main thread by deferring this work:

connectedCallback() {
  let scope = this.parentNode;
  requestIdleCallback(() => {
    this.discover(scope);
  });
}

requestIdleCallback is not universally supported yet, but we can use requestAnimationFrame as a fallback:

let defer = window.requestIdleCallback || requestAnimationFrame;

class AutoLoader extends HTMLElement {
  connectedCallback() {
    let scope = this.parentNode;
    defer(() => {
      this.discover(scope);
    });
  }
  // ...
}

Now we can move on to implementing the missing load method to dynamically inject a <script> element:

load(tag) {
  let el = document.createElement("script");
  let res = new Promise((resolve, reject) => {
    el.addEventListener("load", ev => {
      resolve(null);
    });
    el.addEventListener("error", ev => {
      reject(new Error("failed to locate custom-element definition"));
    });
  });
  el.src = this.elementURL(tag);
  document.head.appendChild(el);
  return res;
}

elementURL(tag) {
  return `${this.rootDir}/${tag}.js`;
}

Note the hard-coded convention in elementURL. The src attribute’s URL assumes there’s a directory where all custom element definitions reside (e.g. <my-widget>/components/my-widget.js). We could come up with more elaborate strategies, but this is good enough for our purposes. Relegating this URL to a separate method allows for project-specific subclassing when needed:

class FancyLoader extends AutoLoader {
  elementURL(tag) {
    // fancy logic
  }
}

Either way, note that we’re relying on this.rootDir. This is where the aforementioned configurability comes in. Let’s add a corresponding getter:

get rootDir() {
  let uri = this.getAttribute("root-dir");
  if(!uri) {
    throw new Error("cannot auto-load custom elements: missing `root-dir`");
  }
  if(uri.endsWith("/")) { // remove trailing slash
    return uri.substring(0, uri.length - 1);
  }
  return uri;
}

You might be thinking of observedAttributes now, but that doesn’t really make things easier. Plus updating root-dir at runtime seems like something we’re never going to need.

Now we can — and must — configure our elements directory: <ce-autoloader root-dir="/components">.

With this, our auto-loader can do its job. Except it only works once, for elements that already exist when the auto-loader is initialized. We’ll probably want to account for dynamically added elements as well. That’s where MutationObserver comes into play:

connectedCallback() {
  let scope = this.parentNode;
  defer(() => {
    this.discover(scope);
  });
  let observer = this._observer = new MutationObserver(mutations => {
    for(let { addedNodes } of mutations) {
      for(let node of addedNodes) {
        defer(() => {
          this.discover(node);
        });
      }
    }
  });
  observer.observe(scope, { subtree: true, childList: true });
}

disconnectedCallback() {
  this._observer.disconnect();
}

This way, the browser notifies us whenever a new element appears in the DOM — or rather, our respective subtree — which we then use to restart the discovery process. (You might argue we’re re-inventing custom elements here, and you’d be kind of correct.)

Our auto-loader is now fully functional. Future enhancements might look into potential race conditions and investigate optimizations. But chances are this is good enough for most scenarios. Let me know in the comments if you have a different approach and we can compare notes!


An Approach to Lazy Loading Custom Elements originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/an-approach-to-lazy-loading-custom-elements/feed/ 4 376991
Using Web Components With Next (or Any SSR Framework) https://css-tricks.com/using-web-components-with-next-or-any-ssr-framework/ https://css-tricks.com/using-web-components-with-next-or-any-ssr-framework/#respond Wed, 05 Oct 2022 13:05:43 +0000 https://css-tricks.com/?p=373787 In my previous post we looked at Shoelace, which is a component library with a full suite of UX components that are beautiful, accessible, and — perhaps unexpectedly — built with Web Components. This means they can be used …


Using Web Components With Next (or Any SSR Framework) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In my previous post we looked at Shoelace, which is a component library with a full suite of UX components that are beautiful, accessible, and — perhaps unexpectedly — built with Web Components. This means they can be used with any JavaScript framework. While React’s Web Component interoperability is, at present, less than ideal, there are workarounds.

But one serious shortcoming of Web Components is their current lack of support for server-side rendering (SSR). There is something called the Declarative Shadow DOM (DSD) in the works, but current support for it is pretty minimal, and it actually requires buy-in from your web server to emit special markup for the DSD. There’s currently work being done for Next.js that I look forward to seeing. But for this post, we’ll look at how to manage Web Components from any SSR framework, like Next.js, today.

We’ll wind up doing a non-trivial amount of manual work, and slightly hurting our page’s startup performance in the process. We’ll then look at how to minimize these performance costs. But make no mistake: this solution is not without tradeoffs, so don’t expect otherwise. Always measure and profile.

The problem

Before we dive in, let’s take a moment and actually explain the problem. Why don’t Web Components work well with server-side rendering?

Application frameworks like Next.js take React code and run it through an API to essentially “stringify” it, meaning it turns your components into plain HTML. So the React component tree will render on the server hosting the web app, and that HTML will be sent down with the rest of the web app’s HTML document to your user’s browser. Along with this HTML are some <script> tags that load React, along with the code for all your React components. When a browser processes these <script> tags, React will re-render the component tree, and match things up with the SSR’d HTML that was sent down. At this point, all of the effects will start running, the event handlers will wire up, and the state will actually… contain state. It’s at this point that the web app becomes interactive. The process of re-processing your component tree on the client, and wiring everything up is called hydration.

So, what does this have to do with Web Components? Well, when you render something, say the same Shoelace <sl-tab-group> component we visited last time:

<sl-tab-group ref="{tabsRef}">
  <sl-tab slot="nav" panel="general"> General </sl-tab>
  <sl-tab slot="nav" panel="custom"> Custom </sl-tab>
  <sl-tab slot="nav" panel="advanced"> Advanced </sl-tab>
  <sl-tab slot="nav" panel="disabled" disabled> Disabled </sl-tab>

  <sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
  <sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
  <sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
  <sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
</sl-tab-group>

…React (or honestly any JavaScript framework) will see those tags and simply pass them along. React (or Svelte, or Solid) are not responsible for turning those tags into nicely-formatted tabs. The code for that is tucked away inside of whatever code you have that defines those Web Components. In our case, that code is in the Shoelace library, but the code can be anywhere. What’s important is when the code runs.

Normally, the code registering these Web Components will be pulled into your application’s normal code via a JavaScript import. That means this code will wind up in your JavaScript bundle and execute during hydration which means that, between your user first seeing the SSR’d HTML and hydration happening, these tabs (or any Web Component for that matter) will not render the correct content. Then, when hydration happens, the proper content will display, likely causing the content around these Web Components to move around and fit the properly formatted content. This is known as a flash of unstyled content, or FOUC. In theory, you could stick markup in between all of those <sl-tab-xyz> tags to match the finished output, but this is all but impossible in practice, especially for a third-party component library like Shoelace.

Moving our Web Component registration code

So the problem is that the code to make Web Components do what they need to do won’t actually run until hydration occurs. For this post, we’ll look at running that code sooner; immediately, in fact. We’ll look at custom bundling our Web Component code, and manually adding a script directly to our document’s <head> so it runs immediately, and blocks the rest of the document until it does. This is normally a terrible thing to do. The whole point of server-side rendering is to not block our page from processing until our JavaScript has processed. But once done, it means that, as the document is initially rendering our HTML from the server, the Web Components will be registered and will both immediately and synchronously emit the right content.

In our case, we’re just looking to run our Web Component registration code in a blocking script. This code isn’t huge, and we’ll look to significantly lessen the performance hit by adding some cache headers to help with subsequent visits. This isn’t a perfect solution. The first time a user browses your page will always block while that script file is loaded. Subsequent visits will cache nicely, but this tradeoff might not be feasible for you — e-commerce, anyone? Anyway, profile, measure, and make the right decision for your app. Besides, in the future it’s entirely possible Next.js will fully support DSD and Web Components.

Getting started

All of the code we’ll be looking at is in this GitHub repo and deployed here with Vercel. The web app renders some Shoelace components along with text that changes color and content upon hydration. You should be able to see the text change to “Hydrated,” with the Shoelace components already rendering properly.

Custom bundling Web Component code

Our first step is to create a single JavaScript module that imports all of our Web Component definitions. For the Shoelace components I’m using, my code looks like this:

import { setDefaultAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry";

import "@shoelace-style/shoelace/dist/components/tab/tab.js";
import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js";
import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js";

import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";

setDefaultAnimation("dialog.show", {
  keyframes: [
    { opacity: 0, transform: "translate3d(0px, -20px, 0px)" },
    { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
  ],
  options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});
setDefaultAnimation("dialog.hide", {
  keyframes: [
    { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
    { opacity: 0, transform: "translate3d(0px, 20px, 0px)" },
  ],
  options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});

It loads the definitions for the <sl-tab-group> and <sl-dialog> components, and overrides some default animations for the dialog. Simple enough. But the interesting piece here is getting this code into our application. We cannot simply import this module. If we did that, it’d get bundled into our normal JavaScript bundles and run during hydration. This would cause the FOUC we’re trying to avoid.

While Next.js does have a number of webpack hooks to custom bundle things, I’ll use Vite instead. First, install it with npm i vite and then create a vite.config.js file. Mine looks like this:

import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
  build: {
    outDir: path.join(__dirname, "./shoelace-dir"),
    lib: {
      name: "shoelace",
      entry: "./src/shoelace-bundle.js",
      formats: ["umd"],
      fileName: () => "shoelace-bundle.js",
    },
    rollupOptions: {
      output: {
        entryFileNames: `[name]-[hash].js`,
      },
    },
  },
});

This will build a bundle file with our Web Component definitions in the shoelace-dir folder. Let’s move it over to the public folder so that Next.js will serve it. And we should also keep track of the exact name of the file, with the hash on the end of it. Here’s a Node script that moves the file and writes a JavaScript module that exports a simple constant with the name of the bundle file (this will come in handy shortly):

const fs = require("fs");
const path = require("path");

const shoelaceOutputPath = path.join(process.cwd(), "shoelace-dir");
const publicShoelacePath = path.join(process.cwd(), "public", "shoelace");

const files = fs.readdirSync(shoelaceOutputPath);

const shoelaceBundleFile = files.find(name => /^shoelace-bundle/.test(name));

fs.rmSync(publicShoelacePath, { force: true, recursive: true });

fs.mkdirSync(publicShoelacePath, { recursive: true });
fs.renameSync(path.join(shoelaceOutputPath, shoelaceBundleFile), path.join(publicShoelacePath, shoelaceBundleFile));
fs.rmSync(shoelaceOutputPath, { force: true, recursive: true });

fs.writeFileSync(path.join(process.cwd(), "util", "shoelace-bundle-info.js"), `export const shoelacePath = "/shoelace/${shoelaceBundleFile}";`);

Here’s a companion npm script:

"bundle-shoelace": "vite build && node util/process-shoelace-bundle",

That should work. For me, util/shoelace-bundle-info.js now exists, and looks like this:

export const shoelacePath = "/shoelace/shoelace-bundle-a6f19317.js";

Loading the script

Let’s go into the Next.js \_document.js file and pull in the name of our Web Component bundle file:

import { shoelacePath } from "../util/shoelace-bundle-info";

Then we manually render a <script> tag in the <head>. Here’s what my entire _document.js file looks like:

import { Html, Head, Main, NextScript } from "next/document";
import { shoelacePath } from "../util/shoelace-bundle-info";

export default function Document() {
  return (
    <Html>
      <Head>
        <script src={shoelacePath}></script>
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

And that should work! Our Shoelace registration will load in a blocking script and be available immediately as our page processes the initial HTML.

Improving performance

We could leave things as they are but let’s add caching for our Shoelace bundle. We’ll tell Next.js to make these Shoelace bundles cacheable by adding the following entry to our Next.js config file:

async headers() {
  return [
    {
      source: "/shoelace/shoelace-bundle-:hash.js",
      headers: [
        {
          key: "Cache-Control",
          value: "public,max-age=31536000,immutable",
        },
      ],
    },
  ];
}

Now, on subsequent browses to our site, we see the Shoelace bundle caching nicely!

DevTools Sources panel open and showing the loaded Shoelace bundle.

If our Shoelace bundle ever changes, the file name will change (via the :hash portion from the source property above), the browser will find that it does not have that file cached, and will simply request it fresh from the network.

Wrapping up

This may have seemed like a lot of manual work; and it was. It’s unfortunate Web Components don’t offer better out-of-the-box support for server-side rendering.

But we shouldn’t forget the benefits they provide: it’s nice being able to use quality UX components that aren’t tied to a specific framework. It’s aldo nice being able to experiment with brand new frameworks, like Solid, without needing to find (or hack together) some sort of tab, modal, autocomplete, or whatever component.


Using Web Components With Next (or Any SSR Framework) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-web-components-with-next-or-any-ssr-framework/feed/ 0 373787
Introducing Shoelace, a Framework-Independent Component-Based UX Library https://css-tricks.com/shoelace-component-frameowrk-introduction/ https://css-tricks.com/shoelace-component-frameowrk-introduction/#respond Tue, 04 Oct 2022 13:01:53 +0000 https://css-tricks.com/?p=373703 This is a post about Shoelace, a component library by Cory LaViska, but with a twist. It defines all your standard UX components: tabs, modals, accordions, auto-completes, and much, much more. They look beautiful out of the …


Introducing Shoelace, a Framework-Independent Component-Based UX Library originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
This is a post about Shoelace, a component library by Cory LaViska, but with a twist. It defines all your standard UX components: tabs, modals, accordions, auto-completes, and much, much more. They look beautiful out of the box, are accessible, and fully customizable. But rather than creating these components in React, or Solid, or Svelte, etc., it creates them with Web Components; this means you can use them with any framework.

Some preliminary things

Web Components are great, but there’s currently a few small hitches to be aware of.

React

I said they work in any JavaScript framework, but as I’ve written before, React’s support for Web Components is currently poor. To address this, Shoelace actually created wrappers just for React.

Another option, which I personally like, is to create a thin React component that accepts the tag name of a Web Component and all of its attributes and properties, then does the dirty work of handling React’s shortcomings. I talked about this option in a previous post. I like this solution because it’s designed to be deleted. The Web Component interoperability problem is currently fixed in React’s experimental branch, so once that’s shipped, any thin Web Component-interoperable component you’re using could be searched, and removed, leaving you with direct Web Component usages, without any React wrappers.

Server-Side Rendering (SSR)

Support for SSR is also poor at the time of this writing. In theory, there’s something called Declarative Shadow DOM (DSD) which would enable SSR. But browser support is minimal, and in any event, DSD actually requires server support to work right, which means Next, Remix, or whatever you happen to use on the server will need to become capable of some special handling.

That said, there are other ways to get Web Components to just work with a web app that’s SSR’d with something like Next. The short version is that the scripts registering your Web Components need to run in a blocking script before your markup is parsed. But that’s a topic for another post.

Of course, if you’re building any kind of client-rendered SPA, this is a non-issue. This is what we’ll work with in this post.

Let’s start

Since I want this post to focus on Shoelace and on its Web Component nature, I’ll be using Svelte for everything. I’ll also be using this Stackblitz project for demonstration. We’ll build this demo together, step-by-step, but feel free to open that REPL up anytime to see the end result.

I’ll show you how to use Shoelace, and more importantly, how to customize it. We’ll talk about Shadow DOMs and which styles they block from the outside world (as well as which ones they don’t). We’ll also talk about the ::part CSS selector — which may be entirely new to you — and we’ll even see how Shoelace allows us to override and customize its various animations.

If you find you like Shoelace after reading this post and want to try it in a React project, my advice is to use a wrapper like I mentioned in the introduction. This will allow you to use any of Shoelace’s components, and it can be removed altogether once React ships the Web Component fixes they already have (look for that in version 19).

Introducing Shoelace

Shoelace has fairly detailed installation instructions. At its most simple, you can dump <script> and <style> tags into your HTML doc, and that’s that. For any production app, though, you’ll probably want to selectively import only what you want, and there are instructions for that, too.

With Shoelace installed, let’s create a Svelte component to render some content, and then go through the steps to fully customize it. To pick something fairly non-trivial, I went with the tabs and a dialog (commonly referred to as a modal) components. Here’s some markup taken largely from the docs:

<sl-tab-group>
  <sl-tab slot="nav" panel="general">General</sl-tab>
  <sl-tab slot="nav" panel="custom">Custom</sl-tab>
  <sl-tab slot="nav" panel="advanced">Advanced</sl-tab>
  <sl-tab slot="nav" panel="disabled" disabled>Disabled</sl-tab>

  <sl-tab-panel name="general">This is the general tab panel.</sl-tab-panel>
  <sl-tab-panel name="custom">This is the custom tab panel.</sl-tab-panel>
  <sl-tab-panel name="advanced">This is the advanced tab panel.</sl-tab-panel>
  <sl-tab-panel name="disabled">This is a disabled tab panel.</sl-tab-panel>
</sl-tab-group>

<sl-dialog no-header label="Dialog">
  Hello World!
  <button slot="footer" variant="primary">Close</button>
</sl-dialog>

<br />
<button>Open Dialog</button>

This renders some nice, styled tabs. The underline on the active tab even animates nicely, and slides from one active tab to the next.

Four horizontal tab headings with the first active in blue with placeholder content contained in a panel below.
Default tabs in Shoelace

I won’t waste your time running through every inch of the APIs that are already well-documented on the Shoelace website. Instead, let’s look into how best to interact with, and fully customize these Web Components.

Interacting with the API: methods and events

Calling methods and subscribing to events on a Web Component might be slightly different than what you’re used to with your normal framework of choice, but it’s not too complicated. Let’s see how.

Tabs

The tabs component (<sl-tab-group>) has a show method, which manually shows a particular tab. In order to call this, we need to get access to the underlying DOM element of our tabs. In Svelte, that means using bind:this. In React, it’d be a ref. And so on. Since we’re using Svelte, let’s declare a variable for our tabs instance:

<script>
  let tabs;
</script>

…and bind it:

<sl-tab-group bind:this="{tabs}"></sl-tab-group>

Now we can add a button to call it:

<button on:click={() => tabs.show("custom")}>Show custom</button>

It’s the same idea for events. There’s a sl-tab-show event that fires when a new tab is shown. We could use addEventListener on our tabs variable, or we can use Svelte’s on:event-name shortcut.

<sl-tab-group bind:this={tabs} on:sl-tab-show={e => console.log(e)}>

That works and logs the event objects as you show different tabs.

Event object meta shown in DevTools.

Typically we render tabs and let the user click between them, so this work isn’t usually even necessary, but it’s there if you need it. Now let’s get the dialog component interactive.

Dialog

The dialog component (<sl-dialog>) takes an open prop which controls whether the dialog is… open. Let’s declare it in our Svelte component:

<script>
  let tabs;
  let open = false;
</script>

It also has an sl-hide event for when the dialog is hidden. Let’s pass our open prop and bind to the hide event so we can reset it when the user clicks outside of the dialog content to close it. And let’s add a click handler to that close button to set our open prop to false, which would also close the dialog.

<sl-dialog no-header {open} label="Dialog" on:sl-hide={() => open = false}>
  Hello World!
  <button slot="footer" variant="primary" on:click={() => open = false}>Close</button>
</sl-dialog>

Lastly, let’s wire up our open dialog button:

<button on:click={() => (open = true)}>Open Dialog</button>

And that’s that. Interacting with a component library’s API is more or less straightforward. If that’s all this post did, it would be pretty boring.

But Shoelace — being built with Web Components — means that some things, particularly styles, will work a bit differently than we might be used to.

Customize all the styles!

As of this writing, Shoelace is still in beta and the creator is considering changing some default styles, possibly even removing some defaults altogether so they’ll no longer override your host application’s styles. The concepts we’ll cover are relevant either way, but don’t be surprised if some of the Shoelace specifics I mention are different when you go to use it.

As nice as Shoelace’s default styles are, we might have our own designs in our web app, and we’ll want our UX components to match. Let’s see how we’d go about that in a Web Components world.

We won’t try to actually improve anything. The Shoelace creator is a far better designer than I’ll ever be. Instead, we’ll just look at how to change things, so you can adapt to your own web apps.

A quick tour of Shadow DOMs

Take a peek at one of those tab headers in your DevTools; it should look something like this:

The tabs component markup shown in DevTools.

Our tab element has created a div container with a .tab and .tab--active class, and a tabindex, while also displaying the text we entered for that tab. But notice that it’s sitting inside of a shadow root. This allows Web Component authors to add their own markup to the Web Component while also providing a place for the content we provide. Notice the <slot> element? That basically means “put whatever content the user rendered between the Web Component tags here.”

So the <sl-tab> component creates a shadow root, adds some content to it to render the nicely-styled tab header along with a placeholder (<slot>) that renders our content inside.

Encapsulated styles

One of the classic, more frustrating problems in web development has always been styles cascading to places where we don’t want them. You might worry that any style rules in our application which specify something like div.tab would interfere with these tabs. It turns out this isn’t a problem; shadow roots encapsulate styles. Styles from outside the shadow root do not affect what’s inside the shadow root (with some exceptions which we’ll talk about), and vice versa.

The exceptions to this are inheritable styles. You, of course, don’t need to apply a font-family style for every element in your web app. Instead, you can specify your font-family once, on :root or html and have it inherit everywhere beneath it. This inheritance will, in fact, pierce the shadow root as well.

CSS custom properties (often called “css variables”) are a related exception. A shadow root can absolutely read a CSS property that is defined outside the shadow root; this will become relevant in a moment.

The ::part selector

What about styles that don’t inherit. What if we want to customize something like cursor, which doesn’t inherit, on something inside of the shadow root. Are we out of luck? It turns out we’re not. Take another look at the tab element image above and its shadow root. Notice the part attribute on the div? That allows you to target and style that element from outside the shadow root using the ::part selector. We’ll walk through an example is a bit.

Overriding Shoelace styles

Let’s see each of these approaches in action. As of now, a lot of Shoelace styles, including fonts, receive default values from CSS custom properties. To align those fonts with your application’s styles, override the custom props in question. See the docs for info on which CSS variables Shoelace is using, or you can simply inspect the styles in any given element in DevTools.

Inheriting styles through the shadow root

Open the app.css file in the src directory of the StackBlitz project. In the :root section at the bottom, you should see a letter-spacing: normal; declaration. Since the letter-spacing property is inheritable, try setting a new value, like 2px. On save, all content, including the tab headers defined in the shadow root, will adjust accordingly.

Four horizontal tab headers with the first active in blue with plqceholder content contained in a panel below. The text is slightly stretched with letter spacing.

Overwriting Shoelace CSS variables

The <sl-tab-group> component reads an --indicator-color CSS custom property for the active tab’s underline. We can override this with some basic CSS:

sl-tab-group {
  --indicator-color: green;
}

And just like that, we now have a green indicator!

Four horizontal tab headers with the first active with blue text and a green underline.

Querying parts

In the version of Shoelace I’m using right now (2.0.0-beta.83), any non-disabled tab has a pointer cursor. Let’s change that to a default cursor for the active (selected) tab. We already saw that the <sl-tab> element adds a part="base" attribute on the container for the tab header. Also, the currently selected tab receives an active attribute. Let’s use these facts to target the active tab, and change the cursor:

sl-tab[active]::part(base) {
  cursor: default;
}

And that’s that!

Customizing animations

For some icing on the metaphorical cake, let’s see how Shoelace allows us to customize animations. Shoelace uses the Web Animations API, and exposes a setDefaultAnimation API to control how different elements animate their various interactions. See the docs for specifics, but as an example, here’s how you might change Shoelace’s default dialog animation from expanding outward, and shrinking inward, to instead animate in from the top, and drop down while hiding.

import { setDefaultAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry";

setDefaultAnimation("dialog.show", {
  keyframes: [
    { opacity: 0, transform: "translate3d(0px, -20px, 0px)" },
    { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
  ],
  options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});
setDefaultAnimation("dialog.hide", {
  keyframes: [
    { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
    { opacity: 0, transform: "translate3d(0px, 20px, 0px)" },
  ],
  options: { duration: 200, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});

That code is in the App.svelte file. Comment it out to see the original, default animation.

Wrapping up

Shoelace is an incredibly ambitious component library that’s built with Web Components. Since Web Components are framework-independent, they can be used in any project, with any framework. With new frameworks starting to come out with both amazing performance characteristics, and also ease of use, the ability to use quality user experience widgets which aren’t tied to any one framework has never been more compelling.


Introducing Shoelace, a Framework-Independent Component-Based UX Library originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/shoelace-component-frameowrk-introduction/feed/ 0 373703
Building Interoperable Web Components That Even Work With React https://css-tricks.com/building-interoperable-web-components-react/ https://css-tricks.com/building-interoperable-web-components-react/#comments Tue, 07 Jun 2022 13:57:57 +0000 https://css-tricks.com/?p=366222 Those of us who’ve been web developers more than a few years have probably written code using more than one JavaScript framework. With all the choices out there — React, Svelte, Vue, Angular, Solid — it’s all but inevitable. One …


Building Interoperable Web Components That Even Work With React originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Those of us who’ve been web developers more than a few years have probably written code using more than one JavaScript framework. With all the choices out there — React, Svelte, Vue, Angular, Solid — it’s all but inevitable. One of the more frustrating things we have to deal with when working across frameworks is re-creating all those low-level UI components: buttons, tabs, dropdowns, etc. What’s particularly frustrating is that we’ll typically have them defined in one framework, say React, but then need to rewrite them if we want to build something in Svelte. Or Vue. Or Solid. And so on.

Wouldn’t it be better if we could define these low-level UI components once, in a framework-agnostic way, and then re-use them between frameworks? Of course it would! And we can; web components are the way. This post will show you how.

As of now, the SSR story for web components is a bit lacking. Declarative shadow DOM (DSD) is how a web component is server-side rendered, but, as of this writing, it’s not integrated with your favorite application frameworks like Next, Remix or SvelteKit. If that’s a requirement for you, be sure to check the latest status of DSD. But otherwise, if SSR isn’t something you’re using, read on.

First, some context

Web Components are essentially HTML elements that you define yourself, like <yummy-pizza> or whatever, from the ground up. They’re covered all over here at CSS-Tricks (including an extensive series by Caleb Williams and one by John Rhea) but we’ll briefly walk through the process. Essentially, you define a JavaScript class, inherit it from HTMLElement, and then define whatever properties, attributes and styles the web component has and, of course, the markup it will ultimately render to your users.

Being able to define custom HTML elements that aren’t bound to any particular component is exciting. But this freedom is also a limitation. Existing independently of any JavaScript framework means you can’t really interact with those JavaScript frameworks. Think of a React component which fetches some data and then renders some other React component, passing along the data. This wouldn’t really work as a web component, since a web component doesn’t know how to render a React component.

Web components particularly excel as leaf components. Leaf components are the last thing to be rendered in a component tree. These are the components which receive some props, and render some UI. These are not the components sitting in the middle of your component tree, passing data along, setting context, etc. — just pure pieces of UI that will look the same, no matter which JavaScript framework is powering the rest of the app.

The web component we’re building

Rather than build something boring (and common), like a button, let’s build something a little bit different. In my last post we looked at using blurry image previews to prevent content reflow, and provide a decent UI for users while our images load. We looked at base64 encoding a blurry, degraded versions of our images, and showing that in our UI while the real image loaded. We also looked at generating incredibly compact, blurry previews using a tool called Blurhash.

That post showed you how to generate those previews and use them in a React project. This post will show you how to use those previews from a web component so they can be used by any JavaScript framework.

But we need to walk before we can run, so we’ll walk through something trivial and silly first to see exactly how web components work.

Everything in this post will build vanilla web components without any tooling. That means the code will have a bit of boilerplate, but should be relatively easy to follow. Tools like Lit or Stencil are designed for building web components and can be used to remove much of this boilerplate. I urge you to check them out! But for this post, I’ll prefer a little more boilerplate in exchange for not having to introduce and teach another dependency.

A simple counter component

Let’s build the classic “Hello World” of JavaScript components: a counter. We’ll render a value, and a button that increments that value. Simple and boring, but it’ll let us look at the simplest possible web component.

In order to build a web component, the first step is to make a JavaScript class, which inherits from HTMLElement:

class Counter extends HTMLElement {}

The last step is to register the web component, but only if we haven’t registered it already:

if (!customElements.get("counter-wc")) {
  customElements.define("counter-wc", Counter);
}

And, of course, render it:

<counter-wc></counter-wc>

And everything in between is us making the web component do whatever we want it to. One common lifecycle method is connectedCallback, which fires when our web component is added to the DOM. We could use that method to render whatever content we’d like. Remember, this is a JS class inheriting from HTMLElement, which means our this value is the web component element itself, with all the normal DOM manipulation methods you already know and love.

At it’s most simple, we could do this:

class Counter extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<div style='color: green'>Hey</div>";
  }
}

if (!customElements.get("counter-wc")) {
  customElements.define("counter-wc", Counter);
}

…which will work just fine.

The word "hey" in green.

Adding real content

Let’s add some useful, interactive content. We need a <span> to hold the current number value and a <button> to increment the counter. For now, we’ll create this content in our constructor and append it when the web component is actually in the DOM:

constructor() {
  super();
  const container = document.createElement('div');

  this.valSpan = document.createElement('span');

  const increment = document.createElement('button');
  increment.innerText = 'Increment';
  increment.addEventListener('click', () => {
    this.#value = this.#currentValue + 1;
  });

  container.appendChild(this.valSpan);
  container.appendChild(document.createElement('br'));
  container.appendChild(increment);

  this.container = container;
}

connectedCallback() {
  this.appendChild(this.container);
  this.update();
}

If you’re really grossed out by the manual DOM creation, remember you can set innerHTML, or even create a template element once as a static property of your web component class, clone it, and insert the contents for new web component instances. There’s probably some other options I’m not thinking of, or you can always use a web component framework like Lit or Stencil. But for this post, we’ll continue to keep it simple.

Moving on, we need a settable JavaScript class property named value

#currentValue = 0;

set #value(val) {
  this.#currentValue = val;
  this.update();
}

It’s just a standard class property with a setter, along with a second property to hold the value. One fun twist is that I’m using the private JavaScript class property syntax for these values. That means nobody outside our web component can ever touch these values. This is standard JavaScript that’s supported in all modern browsers, so don’t be afraid to use it.

Or feel free to call it _value if you prefer. And, lastly, our update method:

update() {
  this.valSpan.innerText = this.#currentValue;
}

It works!

The counter web component.

Obviously this is not code you’d want to maintain at scale. Here’s a full working example if you’d like a closer look. As I’ve said, tools like Lit and Stencil are designed to make this simpler.

Adding some more functionality

This post is not a deep dive into web components. We won’t cover all the APIs and lifecycles; we won’t even cover shadow roots or slots. There’s endless content on those topics. My goal here is to provide a decent enough introduction to spark some interest, along with some useful guidance on actually using web components with the popular JavaScript frameworks you already know and love.

To that end, let’s enhance our counter web component a bit. Let’s have it accept a color attribute, to control the color of the value that’s displayed. And let’s also have it accept an increment property, so consumers of this web component can have it increment by 2, 3, 4 at a time. And to drive these state changes, let’s use our new counter in a Svelte sandbox — we’ll get to React in a bit.

We’ll start with the same web component as before and add a color attribute. To configure our web component to accept and respond to an attribute, we add a static observedAttributes property that returns the attributes that our web component listens for.

static observedAttributes = ["color"];

With that in place, we can add a attributeChangedCallback lifecycle method, which will run whenever any of the attributes listed in observedAttributes are set, or updated.

attributeChangedCallback(name, oldValue, newValue) {
  if (name === "color") {
    this.update();
  }
}

Now we update our update method to actually use it:

update() {
  this.valSpan.innerText = this._currentValue;
  this.valSpan.style.color = this.getAttribute("color") || "black";
}

Lastly, let’s add our increment property:

increment = 1;

Simple and humble.

Using the counter component in Svelte

Let’s use what we just made. We’ll go into our Svelte app component and add something like this:

<script>
  let color = "red";
</script>

<style>
  main {
    text-align: center;
  }
</style>

<main>
  <select bind:value={color}>
    <option value="red">Red</option>
    <option value="green">Green</option>
    <option value="blue">Blue</option>
  </select>

  <counter-wc color={color}></counter-wc>
</main>

And it works! Our counter renders, increments, and the dropdown updates the color. As you can see, we render the color attribute in our Svelte template and, when the value changes, Svelte handles the legwork of calling setAttribute on our underlying web component instance. There’s nothing special here: this is the same thing it already does for the attributes of any HTML element.

Things get a little bit interesting with the increment prop. This is not an attribute on our web component; it’s a prop on the web component’s class. That means it needs to be set on the web component’s instance. Bear with me, as things will wind up much simpler in a bit.

First, we’ll add some variables to our Svelte component:

let increment = 1;
let wcInstance;

Our powerhouse of a counter component will let you increment by 1, or by 2:

<button on:click={() => increment = 1}>Increment 1</button>
<button on:click={() => increment = 2}>Increment 2</button>

But, in theory, we need to get the actual instance of our web component. This is the same thing we always do anytime we add a ref with React. With Svelte, it’s a simple bind:this directive:

<counter-wc bind:this={wcInstance} color={color}></counter-wc>

Now, in our Svelte template, we listen for changes to our component’s increment variable and set the underlying web component property.

$: {
  if (wcInstance) {
    wcInstance.increment = increment;
  }
}

You can test it out over at this live demo.

We obviously don’t want to do this for every web component or prop we need to manage. Wouldn’t it be nice if we could just set increment right on our web component, in markup, like we normally do for component props, and have it, you know, just work? In other words, it’d be nice if we could delete all usages of wcInstance and use this simpler code instead:

<counter-wc increment={increment} color={color}></counter-wc>

It turns out we can. This code works; Svelte handles all that legwork for us. Check it out in this demo. This is standard behavior for pretty much all JavaScript frameworks.

So why did I show you the manual way of setting the web component’s prop? Two reasons: it’s useful to understand how these things work and, a moment ago, I said this works for “pretty much” all JavaScript frameworks. But there’s one framework which, maddeningly, does not support web component prop setting like we just saw.

React is a different beast

React. The most popular JavaScript framework on the planet does not support basic interop with web components. This is a well-known problem that’s unique to React. Interestingly, this is actually fixed in React’s experimental branch, but for some reason wasn’t merged into version 18. That said, we can still track the progress of it. And you can try this yourself with a live demo.

The solution, of course, is to use a ref, grab the web component instance, and manually set increment when that value changes. It looks like this:

import React, { useState, useRef, useEffect } from 'react';
import './counter-wc';

export default function App() {
  const [increment, setIncrement] = useState(1);
  const [color, setColor] = useState('red');
  const wcRef = useRef(null);

  useEffect(() => {
    wcRef.current.increment = increment;
  }, [increment]);

  return (
    <div>
      <div className="increment-container">
        <button onClick={() => setIncrement(1)}>Increment by 1</button>
        <button onClick={() => setIncrement(2)}>Increment by 2</button>
      </div>

      <select value={color} onChange={(e) => setColor(e.target.value)}>
        <option value="red">Red</option>
        <option value="green">Green</option>
        <option value="blue">Blue</option>
      </select>

      <counter-wc ref={wcRef} increment={increment} color={color}></counter-wc>
    </div>
  );
}

As we discussed, coding this up manually for every web component property is simply not scalable. But all is not lost because we have a couple of options.

Option 1: Use attributes everywhere

We have attributes. If you clicked the React demo above, the increment prop wasn’t working, but the color correctly changed. Can’t we code everything with attributes? Sadly, no. Attribute values can only be strings. That’s good enough here, and we’d be able to get somewhat far with this approach. Numbers like increment can be converted to and from strings. We could even JSON stringify/parse objects. But eventually we’ll need to pass a function into a web component, and at that point we’d be out of options.

Option 2: Wrap it

There’s an old saying that you can solve any problem in computer science by adding a level of indirection (except the problem of too many levels of indirection). The code to set these props is pretty predictable and simple. What if we hide it in a library? The smart folks behind Lit have one solution. This library creates a new React component for you after you give it a web component, and list out the properties it needs. While clever, I’m not a fan of this approach.

Rather than have a one-to-one mapping of web components to manually-created React components, what I prefer is just one React component that we pass our web component tag name to (counter-wc in our case) — along with all the attributes and properties — and for this component to render our web component, add the ref, then figure out what is a prop and what is an attribute. That’s the ideal solution in my opinion. I don’t know of a library that does this, but it should be straightforward to create. Let’s give it a shot!

This is the usage we’re looking for:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

wcTag is the web component tag name; the rest are the properties and attributes we want passed along.

Here’s what my implementation looks like:

import React, { createElement, useRef, useLayoutEffect, memo } from 'react';

const _WcWrapper = (props) => {
  const { wcTag, children, ...restProps } = props;
  const wcRef = useRef(null);

  useLayoutEffect(() => {
    const wc = wcRef.current;

    for (const [key, value] of Object.entries(restProps)) {
      if (key in wc) {
        if (wc[key] !== value) {
          wc[key] = value;
        }
      } else {
        if (wc.getAttribute(key) !== value) {
          wc.setAttribute(key, value);
        }
      }
    }
  });

  return createElement(wcTag, { ref: wcRef });
};

export const WcWrapper = memo(_WcWrapper);

The most interesting line is at the end:

return createElement(wcTag, { ref: wcRef });

This is how we create an element in React with a dynamic name. In fact, this is what React normally transpiles JSX into. All our divs are converted to createElement("div") calls. We don’t normally need to call this API directly but it’s there when we need it.

Beyond that, we want to run a layout effect and loop through every prop that we’ve passed to our component. We loop through all of them and check to see if it’s a property with an in check that checks the web component instance object as well as its prototype chain, which will catch any getters/setters that wind up on the class prototype. If no such property exists, it’s assumed to be an attribute. In either case, we only set it if the value has actually changed.

If you’re wondering why we use useLayoutEffect instead of useEffect, it’s because we want to immediately run these updates before our content is rendered. Also, note that we have no dependency array to our useLayoutEffect; this means we want to run this update on every render. This can be risky since React tends to re-render a lot. I ameliorate this by wrapping the whole thing in React.memo. This is essentially the modern version of React.PureComponent, which means the component will only re-render if any of its actual props have changed — and it checks whether that’s happened via a simple equality check.

The only risk here is that if you’re passing an object prop that you’re mutating directly without re-assigning, then you won’t see the updates. But this is highly discouraged, especially in the React community, so I wouldn’t worry about it.

Before moving on, I’d like to call out one last thing. You might not be happy with how the usage looks. Again, this component is used like this:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

Specifically, you might not like passing the web component tag name to the <WcWrapper> component and prefer instead the @lit-labs/react package above, which creates a new individual React component for each web component. That’s totally fair and I’d encourage you to use whatever you’re most comfortable with. But for me, one advantage with this approach is that it’s easy to delete. If by some miracle React merges proper web component handling from their experimental branch into main tomorrow, you’d be able to change the above code from this:

<WcWrapper wcTag="counter-wc" increment={increment} color={color} />

…to this:

<counter-wc ref={wcRef} increment={increment} color={color} />

You could probably even write a single codemod to do that everywhere, and then delete <WcWrapper> altogether. Actually, scratch that: a global search and replace with a RegEx would probably work.

The implementation

I know, it seems like it took a journey to get here. If you recall, our original goal was to take the image preview code we looked at in my last post, and move it to a web component so it can be used in any JavaScript framework. React’s lack of proper interop added a lot of detail to the mix. But now that we have a decent handle on how to create a web component, and use it, the implementation will almost be anti-climactic.

I’ll drop the entire web component here and call out some of the interesting bits. If you’d like to see it in action, here’s a working demo. It’ll switch between my three favorite books on my three favorite programming languages. The URL for each book will be unique each time, so you can see the preview, though you’ll likely want to throttle things in your DevTools Network tab to really see things taking place.

View entire code
class BookCover extends HTMLElement {
  static observedAttributes = ['url'];

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'url') {
      this.createMainImage(newValue);
    }
  }

  set preview(val) {
    this.previewEl = this.createPreview(val);
    this.render();
  }

  createPreview(val) {
    if (typeof val === 'string') {
      return base64Preview(val);
    } else {
      return blurHashPreview(val);
    }
  }

  createMainImage(url) {
    this.loaded = false;
    const img = document.createElement('img');
    img.alt = 'Book cover';
    img.addEventListener('load', () =&gt; {
      if (img === this.imageEl) {
        this.loaded = true;
        this.render();
      }
    });
    img.src = url;
    this.imageEl = img;
  }

  connectedCallback() {
    this.render();
  }

  render() {
    const elementMaybe = this.loaded ? this.imageEl : this.previewEl;
    syncSingleChild(this, elementMaybe);
  }
}

First, we register the attribute we’re interested in and react when it changes:

static observedAttributes = ['url'];

attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'url') {
    this.createMainImage(newValue);
  }
}

This causes our image component to be created, which will show only when loaded:

createMainImage(url) {
  this.loaded = false;
  const img = document.createElement('img');
  img.alt = 'Book cover';
  img.addEventListener('load', () => {
    if (img === this.imageEl) {
      this.loaded = true;
      this.render();
    }
  });
  img.src = url;
  this.imageEl = img;
}

Next we have our preview property, which can either be our base64 preview string, or our blurhash packet:

set preview(val) {
  this.previewEl = this.createPreview(val);
  this.render();
}

createPreview(val) {
  if (typeof val === 'string') {
    return base64Preview(val);
  } else {
    return blurHashPreview(val);
  }
}

This defers to whichever helper function we need:

function base64Preview(val) {
  const img = document.createElement('img');
  img.src = val;
  return img;
}

function blurHashPreview(preview) {
  const canvasEl = document.createElement('canvas');
  const { w: width, h: height } = preview;

  canvasEl.width = width;
  canvasEl.height = height;

  const pixels = decode(preview.blurhash, width, height);
  const ctx = canvasEl.getContext('2d');
  const imageData = ctx.createImageData(width, height);
  imageData.data.set(pixels);
  ctx.putImageData(imageData, 0, 0);

  return canvasEl;
}

And, lastly, our render method:

connectedCallback() {
  this.render();
}

render() {
  const elementMaybe = this.loaded ? this.imageEl : this.previewEl;
  syncSingleChild(this, elementMaybe);
}

And a few helpers methods to tie everything together:

export function syncSingleChild(container, child) {
  const currentChild = container.firstElementChild;
  if (currentChild !== child) {
    clearContainer(container);
    if (child) {
      container.appendChild(child);
    }
  }
}

export function clearContainer(el) {
  let child;

  while ((child = el.firstElementChild)) {
    el.removeChild(child);
  }
}

It’s a little bit more boilerplate than we’d need if we build this in a framework, but the upside is that we can re-use this in any framework we’d like — although React will need a wrapper for now, as we discussed.

Odds and ends

I’ve already mentioned Lit’s React wrapper. But if you find yourself using Stencil, it actually supports a separate output pipeline just for React. And the good folks at Microsoft have also created something similar to Lit’s wrapper, attached to the Fast web component library.

As I mentioned, all frameworks not named React will handle setting web component properties for you. Just note that some have some special flavors of syntax. For example, with Solid.js, <your-wc value={12}> always assumes that value is a property, which you can override with an attr prefix, like <your-wc attr:value={12}>.

Wrapping up

Web components are an interesting, often underused part of the web development landscape. They can help reduce your dependence on any single JavaScript framework by managing your UI, or “leaf” components. While creating these as web components — as opposed to Svelte or React components — won’t be as ergonomic, the upside is that they’ll be widely reusable.


Building Interoperable Web Components That Even Work With React originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/building-interoperable-web-components-react/feed/ 3 366222
Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think https://css-tricks.com/web-component-pseudo-classes-and-pseudo-elements/ https://css-tricks.com/web-component-pseudo-classes-and-pseudo-elements/#comments Mon, 28 Feb 2022 15:37:23 +0000 https://css-tricks.com/?p=363929 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 …


Web Component Pseudo-Classes and Pseudo-Elements are Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
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, at least in Safari, 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, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/web-component-pseudo-classes-and-pseudo-elements/feed/ 4 363929
Context-Aware Web Components Are Easier Than You Think https://css-tricks.com/context-aware-web-components/ https://css-tricks.com/context-aware-web-components/#comments Fri, 21 Jan 2022 14:08:05 +0000 https://css-tricks.com/?p=360665 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 …


Context-Aware Web Components Are Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
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, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/context-aware-web-components/feed/ 2 360665
On Yak Shaving and md-block, a new HTML element for Markdown https://css-tricks.com/on-yak-shaving-and-md-block-a-new-html-element-for-markdown/ https://css-tricks.com/on-yak-shaving-and-md-block-a-new-html-element-for-markdown/#respond Wed, 29 Dec 2021 16:03:44 +0000 https://css-tricks.com/?p=359938 Lea Verou made a Web Component for processing Markdown. Looks like there were a couple of others out there already, but I agree with Lea in that this is a good use case for the light DOM (as opposed …


On Yak Shaving and md-block, a new HTML element for Markdown originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Lea Verou made a Web Component for processing Markdown. Looks like there were a couple of others out there already, but I agree with Lea in that this is a good use case for the light DOM (as opposed to the shadow DOM that is normally quite useful for web components), and that’s what Lea’s does. The output is HTML so I can imagine it’s ideal you can style it on the page like any other type rather than have to deal with that shadow DOM. I still feel like the styling stories for shadow DOM all kinda suck.

The story of how it came to be is funny and highly relatable. You just want to build one simple thing and it turns out you have to do 15 other things and it takes the better part of a week.

The demos on the landing page for <md-block> shoot over to CodePen using the prefill API. Figured I’d embed one here too:

To Shared LinkPermalink on CSS-Tricks


On Yak Shaving and md-block, a new HTML element for Markdown originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/on-yak-shaving-and-md-block-a-new-html-element-for-markdown/feed/ 0 359938
Spicy Sections https://css-tricks.com/spicy-sections/ https://css-tricks.com/spicy-sections/#comments Tue, 07 Dec 2021 22:03:19 +0000 https://css-tricks.com/?p=356433 What if HTML had “tabs”? That would be cool, says I. Dave has been spending some of his time and energy, along with a group of “Tabvengers” from OpenUI, on this. A lot of research leads to a bit


Spicy Sections originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
What if HTML had “tabs”? That would be cool, says I. Dave has been spending some of his time and energy, along with a group of “Tabvengers” from OpenUI, on this. A lot of research leads to a bit of a plot twist:

Our research showed there are a lot of variations for what makes up a tab control. There’s a lot of variations in markup patterns as well. There’s variations written in operating systems, video games, jQuery, React components, and web components. But we think we’ve boiled some of this ocean and have come to a decent consensus on what might make for a good <tabs> element… and it isn’t <tabs>!!!

It kinda comes down to design affordances. Sure, the type of UI that looks like literal paper manilla folders is one kind of design affordance. But it’s functionally similar to a one-at-a-time accordion. And accordions are fairly similar to <details>/<summary> elements — so maybe the most helpful thing HTML could do is allow us to use different design affordances, and perhaps even switch between them as needed (say, at different widths).

Then the question is, what HTML would support all those different designs? That actually has a pretty satisfying answer: regular ol’ header-based semantic HTML, so like:

<h2>Header</h2>
<p>Content</p>

<h2>Header</h2>
<p>Content</p>

<h2>Header</h2>
<p>Content</p>

Which means…

  1. The base HTML is sound and can render just fine as one design choice
  2. The headers can become a “tab” it that particular design
  3. The headers can become a “summary” in that particular design

This is the base of what the Tabvengers are calling <spicy-sections>. Just wrap that semantic HTML in the web component, and then use CSS to control which type of design kicks in when.

<spicy-sections>
  <h2>Header</h2>
  <p>Content</p>

  <h2>Header</h2>
  <p>Content</p>

  <h2>Header</h2>
  <p>Content</p>
</spicy-sections>
spicy-sections {
  --const-mq-affordances:
    [screen and (max-width: 40em) ] collapse |
    [screen and (min-width: 60em) ] tab-bar;
  display: block;
}

Brian Kardell made up an example:

I made one as well to get a feel for it:

Here’s a video in case you’re in a place you can’t easily pop over and resize a browser window to get a feel yourself:

This is a totally hand-built Web Component for now, but maybe it can ignite all the right conversations at the spec-writing and browser-implementing levels such that we get something along these lines in “real” HTML and CSS one day. I’d be happy about that, as that means fewer developers (including me) having to code “tabs” from scratch, and probably screw up the accessibility along the way. The more of that, the better.

If you’d like to hear more about all this, check out ShopTalk 486 at 15:17. Plus here’s some exploration from Hidde de Vries. And if you’re interested in more about Web Components and how they can be gosh-darned useful, not only for things like this, but much more in Dave’s recent talk HTML with Superpowers.


Spicy Sections originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/spicy-sections/feed/ 4 356433
Supercharging Built-In Elements With Web Components “is” Easier Than You Think https://css-tricks.com/supercharging-built-in-elements-with-web-components-is-easier-than-you-think/ https://css-tricks.com/supercharging-built-in-elements-with-web-components-is-easier-than-you-think/#comments Fri, 03 Sep 2021 14:21:38 +0000 https://css-tricks.com/?p=350889 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 …


Supercharging Built-In Elements With Web Components “is” Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
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.

Article series

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!


Supercharging Built-In Elements With Web Components “is” Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/supercharging-built-in-elements-with-web-components-is-easier-than-you-think/feed/ 6 350889
Using Web Components in WordPress is Easier Than You Think https://css-tricks.com/using-web-components-in-wordpress-is-easier-than-you-think/ https://css-tricks.com/using-web-components-in-wordpress-is-easier-than-you-think/#comments Thu, 12 Aug 2021 14:38:36 +0000 https://css-tricks.com/?p=345687 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. …


Using Web Components in WordPress is Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
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.

Article series

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 is not allowed by default. So, we have to explitcly allow it on every element on which you anticipate using it, and, by extension, any element your user might decide to add it 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.

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['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 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;
  }
  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' => 'ez-webcomp',
    'editor_script' => '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.


Using Web Components in WordPress is Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-web-components-in-wordpress-is-easier-than-you-think/feed/ 2 345687
Awesome Standalone (Web Components) https://css-tricks.com/awesome-standalone-web-components/ https://css-tricks.com/awesome-standalone-web-components/#comments Wed, 26 May 2021 17:56:34 +0000 https://css-tricks.com/?p=339759 In his last An Event Apart talk, Dave made a point that it’s really only just about right now that Web Components are becoming a practical choice for production web development. For example, it has only been about a year …


Awesome Standalone (Web Components) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In his last An Event Apart talk, Dave made a point that it’s really only just about right now that Web Components are becoming a practical choice for production web development. For example, it has only been about a year since Edge went Chromium. Before that, Edge didn’t support any Web Component stuff. If you were shipping them long ago, you were doing so with fairly big polyfills, or in a progressive-enhancement style, where if they failed, they did so gracefully or in a controlled environment, say, an intranet where everyone has the same computer (or in something like Electron).

In my opinion, Web Components still have a ways to go to be compelling to most developers, but they are getting there. One thing that I think will push their adoption along is the incredibly easy DX of pre-built components thanks to, in part, ES Modules and how easy it is to import JavaScript.

I’ve mentioned this one before: look how silly-easy it is to use Nolan Lawson’s emoji picker:

That’s one line of JavaScript and one line of HTML to get it working, and another one line of JavaScript to wire it up and return a JSON response of a selection.

Compelling, indeed. DX, you might call it.

Web Components like that aren’t alone, hence the title of this post. Dave put together a list of Awesome Standalones. That is, Web Components that aren’t a part of some bigger more complex system1, but are just little drop-in doodads that are useful on their own, just like the emoji picker. Dave’s repo lists about 20 of them.

Take this one from GitHub (the company), a copy-to-clipboard Web Component:

Pretty sweet for something that comes across the wire at ~3KB. The production story is whatever you want it to be. Use it off the CDN. Bundle it up with your stuff. Leave it as on-demand one-off. Whatever. It’s darn easy to use. In the case of this standalone, there isn’t even any Shadow DOM to deal with.

No shade on Shadow DOM, that’s perhaps the most useful feature of Web Components (and cannot be replicated by a library since it’s a native browser feature), but the options for styling it aren’t my favorite. And if you used three different standalone components with three different opinions on how to style through the Shadow DOM, that’s going to get annoying.

What I picture is developers dipping their toes into stuff like this, seeing the benefits, and using more and more of them in what they are building, and even building their own. Building a design system from Web Components seems like a real sweet spot to me, like many big names2 already do.

The dream is for people to actually consolidate common UI patterns. Like, even if we never get native HTML “tabs” it’s possible that a Web Component could provide them, get the UI, UX, and accessibility perfect, yet leave them style-able such that literally any website could use them. But first, that needs to exist.


  1. That’s a cool way to use Web Components, too, but easy gets attention, and that matters.
  2. People always mention Lightning Design System as a Web Components-based design system, but I’m not seeing it. For example, this accordion looks like semantic HTML with class names, not Web Components. What am I missing?

Awesome Standalone (Web Components) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/awesome-standalone-web-components/feed/ 10 339759
Links on Web Components https://css-tricks.com/links-on-web-components/ https://css-tricks.com/links-on-web-components/#comments Wed, 26 May 2021 17:56:12 +0000 https://css-tricks.com/?p=340901
  • How we use Web Components at GitHub — Kristján Oddsson talks about how GitHub is using web components. I remember they were very early adopters, and it says here they released a <relative-time> component in 2014! Now they’ve got a

  • Links on Web Components originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    ]]>
  • How we use Web Components at GitHub — Kristján Oddsson talks about how GitHub is using web components. I remember they were very early adopters, and it says here they released a <relative-time> component in 2014! Now they’ve got a whole bunch of open source components. So easy to use! Awesome! I wanted to poke around their HTML and see them in action, so I View’d Source and used the RegEx (<\w+-[\w|-|]+.*>) (thanks, Andrew) to look for them. Seven on the logged-in homepage, so they ain’t blowin’ smoke.
  • Using web components to encapsulate CSS and resolve design system conflicts — Tyler Williams says the encapsulation (Shadow DOM) of web components meant avoiding styling conflicts with an older CSS system. He also proves that companies that make sites for Git repos love web components.
  • Container Queries in Web Components — Max Böck shares that the :host of a web component can be the @container which is extremely great and is absolutely how all web components should be written.
  • Faster Integration with Web Components — Jason Grigsby does client work and says that web components don’t make integration fast or easy, they make integration fast and easy.
  • FicusJS — I remember being told once that native web components weren’t really meant to be used “raw” but meant to be low-level such that tooling could be built on top of them. We see that in competition amongst renderers, like lit-html vs htm. Then, in layers of tooling on top of that, like Ficus here, that adds a bunch of fancy stuff like state, methods, and events.
  • Shadow DOM and Its Effect on the Unofficial Styling API — Jim Nielsen expands on the idea I poked at on ShopTalk that the DOM is the styling API. It’s self-documenting, in a way. “As an author, you have to spend time and effort thinking about, architecting, and then documenting a styling API for your component. And as a consumer, you have to read, understand, and implement that API.” Yes. That’s why, to me, it feels like a good idea to have an option to “reach into the Shadow DOM from outside CSS” in an unencumbered way.
    • Awesome Standalones — I think Dave’s list here is exactly the kind of thing that gets developers feet wet and thinking about web components as actually useful.

    Two years ago, hold true:


    Links on Web Components originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    ]]>
    https://css-tricks.com/links-on-web-components/feed/ 4 340901
    Platform News: Using :focus-visible, BBC’s New Typeface, Declarative Shadow DOMs, A11Y and Placeholders https://css-tricks.com/platform-news-using-focus-visible-bbcs-new-typeface-declarative-shadow-doms-a11y-and-placeholders/ https://css-tricks.com/platform-news-using-focus-visible-bbcs-new-typeface-declarative-shadow-doms-a11y-and-placeholders/#comments Fri, 16 Apr 2021 14:33:05 +0000 https://css-tricks.com/?p=338429 There’s a whole lot of accessibility in this week’s news, from the nuances of using :focus-visible and input placeholders, to accessible typefaces and a Safari bug with :display: contents. Plus, a snippet for a bare-bones web component that supports …


    Platform News: Using :focus-visible, BBC’s New Typeface, Declarative Shadow DOMs, A11Y and Placeholders originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    ]]>
    There’s a whole lot of accessibility in this week’s news, from the nuances of using :focus-visible and input placeholders, to accessible typefaces and a Safari bug with :display: contents. Plus, a snippet for a bare-bones web component that supports style encapsulation.

    Now may be a good time to start using :focus-visible

    The CSS :focus-visible pseudo-class replaces :focus as the new way to create custom focus indicators for keyboard users. Chrome recently switched from :focus to :focus-visible in the user agent stylesheet and, as a result of that change, the default focus ring is no longer shown when the user clicks or taps a button.

    When switching from :focus to :focus-visible, consider backwards compatibility. Your keyboard focus indicators should be clearly visible in all browsers, not just the ones that support :focus-visible. If you only style :focus-visible, non-supporting browsers will show the default focus ring which, depending on your design, “may not be sufficiently clear or visible at all.”

    button {
      background: white;
    }
    
    button:focus-visible {
      outline: none;
      background: #ffdd00; /* gold */
    }

    A good way to start using :focus-visible today is to define the focus styles in a :focus rule and then immediately undo these same styles in a :focus:not(:focus-visible) rule. This is admittedly not the most elegant and intuitive pattern, but it works well in all browsers:

    • Browsers that don’t support :focus-visible use the focus styles defined in the :focus rule and ignore the second style rule completely (because :focus-visible is unknown to them).
    • In browsers that do support :focus-visible, the second style rule reverts the focus styles defined in the :focus rule if the :focus-visible state isn’t active as well. In other words, the focus styles defined in the :focus rule are only in effect when :focus-visible is also active.
    button:focus {
      outline: none;
      background: #ffdd00; /* gold */
    }
    
    button:focus:not(:focus-visible) {
      background: white; /* undo gold */
    }

    The BBC created a more accessible typeface

    The BBC created their own custom typeface called Reith (named after the BBC’s founder Sir John Reith). Their goal was to create a font that supports multiple languages and is easier to read, especially on small devices. The font was tested with mixed-ability user groups (dyslexia and vision impairment) and across different screen sizes.

    We [the BBC] were using Helvetica or Arial. We also had Gill Sans as the corporate typeface. These typefaces were designed a hundred years ago for the printed page [and] don’t perform well on today’s modern digital screens.

    Reith Sans can bee seen in use on BBC Sport

    Note: If you’d like to inspect Reith Sans and Reith Serif in Wakamai Fondue, you can quickly access the URLs of the WOFF2 files in the “All fonts on page” section of the Fonts pane in Firefox’s DOM inspector on BBC’s website.

    display: contents is still not accessible in Safari

    The CSS display: contents value has been supported in browsers since 2018. An element with this value “does not generate any boxes” and is effectively replaced by its children. This is especially useful in flex and grid layouts, where the contents value can be used to “promote” more deeply nested elements to flex/grid items while retaining a semantic document structure.

    Source: Manuel Rego Casasnovas

    Unfortunately, this feature originally shipped with an implementation bug that removed the element from the browser’s accessibility tree. For example, applying display: contents to a <ul> element resulted in that element no longer mentioned by screen readers. Since then, this bug has been fixed in Firefox and Chrome (in the latest version).

    View on CodePen

    In Chrome and Firefox, the screen reader informs the user that the “Main, navigation” contains a “list, 2 items.” In Safari, the latter part is missing because the <ul> and <li> elements are not present in the accessibility tree. Until Apple fixes this bug in Safari, be careful when using the contents value on semantic elements and test in screen readers to confirm that your pages are accessible in Safari as well.

    Set opacity when overriding the color of placeholder text

    Accessibility experts recommend avoiding placeholders if possible because they can be confused for pre-populated text and disappear when the user starts entering a value. However, many websites (including Wikipedia and GOV.UK) use placeholders in simple web forms that contain only a single input field, such as a search field.

    The subscription form for the CSS-Tricks newsletter uses a placeholder in the email field

    Placeholders can be styled via the widely supported ::placeholder pseudo-element. If your design calls for a custom color for placeholder text, make sure to specify both color and opacity. The latter is needed for Firefox, which applies opacity: 0.54 to ::placeholder by default. If you don’t override this value, your placeholder text may have insufficient contrast in Firefox.

    .search-field::placeholder {
      color: #727272;
      opacity: 1; /* needed for Firefox */
    }
    The placeholder text on eBay’s website is lighter in Firefox and doesn’t meet the minimum contrast requirement of 4.5:1

    Declarative shadow DOM could help popularize style encapsulation

    One of the key features of shadow DOM is style encapsulation, wherein the outer page’s style rules don’t match elements inside the shadow tree, and vice versa. In order to use this feature, you need to attach a shadow DOM tree to an element (usually a custom element, like <my-element>) and copy the element’s template (usually from a <template> element in the DOM) to the element’s newly created shadow DOM.

    These steps can only be performed in JavaScript. If you’re only interested in style encapsulation and don’t need any dynamic functionality for your element, here is the minimum amount of JavaScript required to create a custom element with a shadow DOM:

    customElements.define(
      "my-element",
      class extends HTMLElement {
        constructor() {
          super();
    
          // find <template id="my-template"> in the DOM
          let template = document.getElementById("my-template");
    
          // make a copy of the template contents…
          let content = template.content.cloneNode(true);
    
          // …and inject it into <my-element>’s shadow DOM
          this.attachShadow({ mode: "open" }).appendChild(content);
        }
      }
    );

    For an example of style encapsulation, see Miriam Suzanne’s <media-object> element on CodePen. The scoped styles are located in the <template> element in the HTML pane. Notice how this CSS code can use simple selectors, such as article, that only match elements inside <media-object>’s shadow DOM.

    JavaScript may soon no longer be required to create this type of style encapsulation in modern browsers. Earlier this week, Chrome became the first browser to ship Google’s Declarative Shadow DOM proposal. If it becomes a standard, this feature will also make it possible to use Shadow DOM in combination with server-side rendering.


    Platform News: Using :focus-visible, BBC’s New Typeface, Declarative Shadow DOMs, A11Y and Placeholders originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    ]]>
    https://css-tricks.com/platform-news-using-focus-visible-bbcs-new-typeface-declarative-shadow-doms-a11y-and-placeholders/feed/ 4 338429
    Interactive Web Components Are Easier Than You Think https://css-tricks.com/interactive-web-components-are-easier-than-you-think/ https://css-tricks.com/interactive-web-components-are-easier-than-you-think/#comments Thu, 25 Mar 2021 15:08:10 +0000 https://css-tricks.com/?p=336928 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 …


    Interactive Web Components Are Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    ]]>
    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?)

    Article series

    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.


    Interactive Web Components Are Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    ]]>
    https://css-tricks.com/interactive-web-components-are-easier-than-you-think/feed/ 6 336928
    Web Components Are Easier Than You Think https://css-tricks.com/web-components-are-easier-than-you-think/ https://css-tricks.com/web-components-are-easier-than-you-think/#comments Mon, 08 Mar 2021 16:06:03 +0000 https://css-tricks.com/?p=335065 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 …


    Web Components Are Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    ]]>
    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.

    Article series

    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…)


    Web Components Are Easier Than You Think originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    ]]>
    https://css-tricks.com/web-components-are-easier-than-you-think/feed/ 25 335065