The View Transitions API And Delightful UI Animations (Part 2)

Last time we met, I introduced you to the View Transitions API. We started with a simple default crossfade transition and applied it to different use cases involving elements on a page transitioning between two states. One of those examples took the basic idea of adding products to a shopping cart on an e-commerce site and creating a visual transition that indicates an item added to the cart.

The View Transitions API is still considered an experimental feature that’s currently supported only in Chrome at the time I’m writing this, but I’m providing that demo below as well as a video if your browser is unable to support the API.

Those diagrams illustrate (1) the origin page, (2) the destination page, (3) the type of transition, and (4) the transition elements. The following is a closer look at the transition elements, i.e., the elements that receive the transition and are tracked by the API.

So, what we’re working with are two transition elements: a header and a card component. We will configure those together one at a time.

Header Transition Elements

The default crossfade transition between the pages has already been set, so let’s start by registering the header as a transition element by assigning it a view-transition-name. First, let’s take a peek at the HTML:

<div class="header__wrapper">
  <!-- Link back arrow -->
  <a class="header__link header__link--dynamic" href="/">
    <svg ...><!-- ... --></svg>
  </a>
  <!-- Page title -->
  <h1 class="header__title">
    <a href="/" class="header__link-logo">
      <span class="header__logo--deco">Vinyl</span>Emporium </a>
  </h1>
  <!-- ... -->
</div>

When the user navigates between the homepage and an item details page, the arrow in the header appears and disappears — depending on which direction we’re moving — while the title moves slightly to the right. We can use display: none to handle the visibility.

/* Hide back arrow on the homepage */
.home .header__link--dynamic {
    display: none;
}

We’re actually registering two transition elements within the header: the arrow (.header__link--dynamic) and the title (.header__title). We use the view-transition-name property on both of them to define the names we want to call those elements in the transition:

@supports (view-transition-name: none) {
  .header__link--dynamic {
    view-transition-name: header-link;
  }
  .header__title {
    view-transition-name: header-title;
  }
}

Note how we’re wrapping all of this in a CSS @supports query so it is scoped to browsers that actually support the View Transitions API. So far, so good!

To do that, let’s start by defining our transition elements and assign transition names to the elements we’re transitioning between the product image (.product__image--deco) and the product disc behind the image (.product__media::before).

@supports (view-transition-name: none) {
  .product__image--deco {
    view-transition-name: product-lp;
  }
 .product__media::before {
    view-transition-name: flap;
  }
  ::view-transition-group(product-lp) {
    animation-duration: 0.25s;
    animation-timing-function: ease-in;
  }
  ::view-transition-old(product-lp),
  ::view-transition-new(product-lp) {
    /* Removed the crossfade animation */
    mix-blend-mode: normal;
    animation: none;
  }
}

Notice how we had to remove the crossfade animation from the product image’s old (::view-transition-old(product-lp)) and new (::view-transition-new(product-lp)) states. So, for now, at least, the album disc changes instantly the moment it’s positioned back behind the album image.

But doing this messed up the transition between our global header navigation and product details pages. Navigating from the item details page back to the homepage results in the album disc remaining visible until the view transition finishes rather than running when we need it to.

Let’s configure the router to match that structure. Each route gets a loader function to handle page data.

import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Category, { loader as categoryLoader } from "./pages/Category";
import Details, { loader as detailsLoader } from "./pages/Details";
import Layout from "./components/Layout";

/* Other imports */

const router = createBrowserRouter([
  {
    /* Shared layout for all routes */
    element: <Layout />,
    children: [
      {
        /* Homepage is going to load a default (first) category */
        path: "/",
        element: <Category />,
        loader: categoryLoader,
      },
      {
      /* Other categories */
        path: "/:category",
        element: <Category />,
        loader: categoryLoader,
      },
      {
        /* Item details page */
        path: "/:category/product/:slug",
        element: <Details />,
        loader: detailsLoader,
      },
    ],
  },
]);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

With this, we have established the routing structure for the app:

  • Homepage (/);
  • Category page (/:category);
  • Product details page (/:category/product/:slug).

And depending on which route we are on, the app renders a Layout component. That’s all we need as far as setting up the routes that we’ll use to transition between views. Now, we can start working on our first transition: between two category pages.

Transition Between Category Pages

We’ll start by implementing the transition between category pages. The transition performs a crossfade animation between views. The only part of the UI that does not participate in the transition is the bottom border of the category filter menu, which provides a visual indication for the active category filter and moves between the formerly active category filter and the currently active category filter that we will eventually register as a transition element.

Since we’re using react-router, we get its web-based routing solution, react-router-dom, baked right in, giving us access to the DOM bindings — or router components we need to keep the UI in sync with the current route as well as a component for navigational links. That’s also where we gain access to the View Transitions API implementation.

Specifically, we will use the component for navigation links (Link) with the unstable_viewTransition prop that tells the react-router to run the View Transitions API when switching page contents.

import { Link, useLocation } from "react-router-dom";
/* Other imports */

const NavLink = ({ slug, title, id }) => {
  const { pathname } = useLocation();
  /* Check if the current nav link is active */
  const isMatch = slug === "/" ? pathname === "/" : pathname.includes(slug);
  return (
    <li key={id}>
      <Link
        className={isMatch ? "nav__link nav__link--current" : "nav__link"}
        to={slug}
        unstable_viewTransition
      >
        {title}
      </Link>
    </li>
  );
};

const Nav = () => {
  return 
    <nav className={"nav"}>
      <ul className="nav__list">
        {categories.items.map((item) => (
          <NavLink {...item} />
        ))}
      </ul>
    </nav>
  );
};

That is literally all we need to register and run the default crossfading view transition! That’s again because react-router-dom is giving us access to the View Transitions API and does the heavy lifting to abstract the process of setting transitions on elements and views.

Creating The Transition Elements

We only have one UI element that gets its own transition and a name for it, and that’s the visual indicator for the actively selected product category filter in the app’s navigation. While the app transitions between category views, it runs another transition on the active indicator that moves its position from the origin category to the destination category.

I know that I had earlier described that visual indicator as a bottom border, but we’re actually going to establish it as a standard HTML horizontal rule (<hr>) element and conditionally render it depending on the current route. So, basically, the <hr> element is fully removed from the DOM when a view transition is triggered, and we re-render it in the DOM under whatever NavLink component represents the current route.

We want this transition only to run if the navigation is visible, so we’ll use the react-intersection-observer helper to check if the element is visible and, if it is, assign it a viewTransitionName in an inline style.

import { useInView } from "react-intersection-observer";
/* Other imports */

const NavLink = ({ slug, title, id }) => {
  const { pathname } = useLocation();
  const isMatch = slug === "/" ? pathname === "/" : pathname.includes(slug);
  return (
    <li key={id}>
      <Link
        ref={ref}
        className={isMatch ? "nav__link nav__link--current" : "nav__link"}
        to={slug}
        unstable_viewTransition
      >
        {title}
      </Link>
      {isMatch && (
        <hr
          style={{
            viewTransitionName: inView ? "marker" : "",
          }}
          className="nav__marker"
        />
      )}
    </li>
  );
};

First, let’s take a look at our Card component used in the category views. Once again, react-router-dom makes our job relatively easy, thanks to the unstable_useViewTransitionState hook. The hook accepts a URL string and returns true if there is an active page transition to the target URL, as well as if the transition is using the View Transitions API.

That’s how we’ll make sure that our active image remains a transition element when navigating between a category view and a product view.

import { Link, unstable_useViewTransitionState } from "react-router-dom";
/* Other imports */

const Card = ({ author, category, slug, id, title }) => {
  /* We'll use the same URL value for the Link and the hook */
  const url = /${category}/product/${slug};

  /* Check if the transition is running for the item details pageURL */
  const isTransitioning = unstable_useViewTransitionState(url);

  return (
    <li className="card">
      <Link unstable_viewTransition to={url} className="card__link">
        <figure className="card__figure">
          <img
            className="card__image"
            style=}}
              /* Apply the viewTransitionName if the card has been clicked on */
              viewTransitionName: isTransitioning ? "item-image" : "",
            }}
            src={/assets/$&#123;category&#125;/${id}-min.jpg}
            alt=""
          />
         {/* ... */}
        </figure>
        <div className="card__deco" />
      </Link>
    </li>
  );
};

export default Card;

We know which image in the product view is the transition element, so we can apply the viewTransitionName directly to it rather than having to guess:

import {
  Link,
  useLoaderData,
  unstable_useViewTransitionState,
} from "react-router-dom";
/* Other imports */

const Details = () => {
  const data = useLoaderData();
  const { id, category, title, author } = data;
  return (
    <>
      <section className="item">
        {/* ... */}
        <article className="item__layout">
          <div>
              <img
                style={{viewTransitionName: "item-image"}}
                className="item__image"
                src={/assets/${category}/${id}-min.jpg}
                alt=""
              />
          </div>
          {/* ... */}
        </article>
      </section>
    </>
  );
};

export default Details;

We’re on a good track but have two issues that we need to tackle before moving on to the final transitions.

One is that the Card component’s image (.card__image) contains some CSS that applies a fixed one-to-one aspect ratio and centering for maintaining consistent dimensions no matter what image file is used. Once the user clicks on the Card — the .card-image in a category view — it becomes an .item-image in the product view and should transition into its original state, devoid of those extra styles.


/* Card component image */
.card__image {
  object-fit: cover;
  object-position: 50% 50%;
  aspect-ratio: 1;
  /* ... */
}

/* Product view image */
.item__image {
 /* No aspect-ratio applied */
 /* ... */
}

Jake has recommended using React’s flushSync function to make this work. The function forces synchronous and immediate DOM updates inside a given callback. It’s meant to be used sparingly, but it’s okay to use it for running the View Transition API as the target component re-renders.

// Assigns view-transition-name to the image before transition runs
const [isImageTransition, setIsImageTransition] = React.useState(false);

// Applies fixed-positioning and full-width image styles as transition runs
const [isFullImage, setIsFullImage] = React.useState(false);

/* ... */

// State update function, which triggers the DOM update we want to animate
const toggleImageState = () => setIsFullImage((state) => !state);

// Click handler function - toggles both states.
const handleZoom = async () => {
  // Run API only if available.
  if (document.startViewTransition) {
    // Set image as a transition element.
    setIsImageTransition(true);
    const transition = document.startViewTransition(() => {
      // Apply DOM updates and force immediate re-render while.
      // View Transitions API is running.
      flushSync(toggleImageState);
    });
    await transition.finished;
    // Cleanup
    setIsImageTransition(false);
  } else {
    // Fallback 
    toggleImageState();
  }
};

/* ... */

With this in place, all we really have to do now is toggle class names and view transition names depending on the state we defined in the previous code.

import React from "react";
import { flushSync } from "react-dom";

/* Other imports */

const Details = () => {
  /* React state, click handlers, util functions... */

  return (
    <>
      <section className="item">
        {/* ... */}
        <article className="item__layout">
          <div>
            <button onClick={handleZoom} className="item__toggle">
              <img
                style={{
                  viewTransitionName:
                    isTransitioning || isImageTransition ? "item-image" : "",
                }}
                className={
                  isFullImage
                    ? "item__image item__image--active"
                    : "item__image"
                }
                src={/assets/${category}/${id}-min.jpg}
                alt=""
              />
            </button>
          </div>
          {/* ... */}
        </article>
      </section>
      <aside
        className={
          isFullImage ? "item__overlay item__overlay--active" : "item__overlay"
        }
      />
    </>
  );
};

We are applying viewTransitionName directly on the image’s style attribute. We could have used boolean variables to toggle a CSS class and set a view-transition-name in CSS instead. The only reason I went with inline styles is to show both approaches in these examples. You can use whichever approach fits your project!

Let’s round this out by refining styles for the overlay that sits behind the image when it is expanded:

.item__overlay--active {
  z-index: 2;
  display: block;
  background: rgba(0, 0, 0, 0.5);
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
}

.item__image--active {
  cursor: zoom-out;
  position: absolute;
  z-index: 9;
  top: 50%;
  left: 50%;
  transform: translate3d(-50%, -50%, 0);
  max-width: calc(100vw - 4rem);
  max-height: calc(100vh - 4rem);
}

Demo

The following demonstrates only the code that is directly relevant to the View Transitions API so that it is easier to inspect and use. If you want access to the full code, feel free to get it in this GitHub repo.

Conclusion

We did a lot of work with the View Transitions API in the second half of this brief two-part article series. Together, we implemented full-view transitions in two different contexts, one in a more traditional multi-page application (i.e., website) and another in a single-page application using React.

We started with transitions in a MPA because the process requires fewer dependencies than working with a framework in a SPA. We were able to set the default crossfade transition between two pages — a category page and a product page — and, in the process, we learned how to set view transition names on elements after the transition runs to prevent naming conflicts.

From there, we applied the same concept in a SPA, that is, an application that contains one page but many views. We took a React app for a “Museum of Digital Wonders” and applied transitions between full views, such as navigating between a category view and a product view. We got to see how react-router — and, by extension, react-router-dom — is used to define transitions bound to specific routes. We used it not only to set a crossfade transition between category views and between category and product views but also to set a view transition name on UI elements that also transition in the process.

The View Transitions API is powerful, and I hope you see that after reading this series and following along with the examples we covered together. What used to take a hefty amount of JavaScript is now a somewhat trivial task, and the result is a smoother user experience that irons out the process of moving from one page or view to another.

That said, the View Transitions API’s power and simplicity need the same level of care and consideration for accessibility as any other transition or animation on the web. That includes things like being mindful of user motion preferences and resisting the temptation to put transitions on everything. There’s a fine balance that comes with making accessible interfaces, and motion is certainly included.

References

The View Transitions API And Delightful UI Animations (Part 1)

Animations are an essential part of a website. They can draw attention, guide users on their journey, provide satisfying and meaningful feedback to interaction, add character and flair to make the website stand out, and so much more!

On top of that, CSS has provided us with transitions and keyframe-based animations since at least 2009. Not only that, the Web Animations API and JavaScript-based animation libraries, such as the popular GSAP, are widely used for building very complex and elaborate animations.

With all these avenues for making things move on the web, you might wonder where the View Transitions API fits in in all this. Consider the following example of a simple task list with three columns.

We’re merely crossfading between the two screen states, and that includes all elements within it (i.e., other images, cards, grid, and so on). The API is unaware that the image that is being moved from the container (old state) to the overlay (new state) is the same element.

We need to instruct the browser to pay special attention to the image element when switching between states. That way, we can create a special transition animation that is applied only to that element. The CSS view-transition-name property applies the name of the view transition we want to apply to the transitioning elements and instructs the browser to keep track of the transitioning element’s size and position while applying the transition.

We get to name the transition anything we want. Let’s go with active-image, which is going to be declared on a .gallery__image--active class that is a modifier of the class applied to images (.gallery-image) when the transition is in an active state:

.gallery__image--active {
  view-transition-name: active-image;
}

Note that view-transition-name has to be a unique identifier and applied to only a single rendered element during the animation. This is why we are applying the property to the active image element (.gallery__image--active). We can remove the class when the image overlay is closed, return the image to its original position, and be ready to apply the view transition to another image without worrying whether the view transition has already been applied to another element on the page.

So, we have an active class, .gallery__image--active, for images that receive the view transition. We need a method for applying that class to an image when the user clicks on that respective image. We can also wait for the animation to finish by storing the transition in a variable and calling await on the finished attribute to toggle off the class and clean up our work.

// Start the transition and save its instance in a variable
const transition = document.startViewTransition(() =&gtl /* ... */);

// Wait for the transition to finish.
await transition.finished;

/* Cleanup after transition has completed */

Let’s apply this to our example:

function toggleImageView(index) {
  const image = document.getElementById(js-gallery-image-${index});

  // Apply a CSS class that contains the view-transition-name before the animation starts.
  image.classList.add("gallery__image--active");

  const imageParentElement = image.parentElement;

  if (!document.startViewTransition) {
    // Fallback if View Transitions API is not supported.
    moveImageToModal(image);
  } else {
    // Start transition with the View Transitions API.
    document.startViewTransition(() => moveImageToModal(image));
  }

  // This click handler function is now async.
  overlayWrapper.onclick = async function () {
    // Fallback if View Transitions API is not supported.
    if (!document.startViewTransition) {
      moveImageToGrid(imageParentElement);
      return;
    }

    // Start transition with the View Transitions API.
    const transition = document.startViewTransition(() => moveImageToGrid(imageParentElement));

    // Wait for the animation to complete.
    await transition.finished;

    // Remove the class that contains the page-transition-tag after the animation ends.
    image.classList.remove("gallery__image--active");
  };
}

Alternatively, we could have used JavaScript to toggle the CSS view-transition-name property on the element in the inline HMTL. However, I would recommend keeping everything in CSS as you might want to use media queries and feature queries to create fallbacks and manage it all in one place.

// Applies view-transition-name to the image
image.style.viewTransitionName = "active-image";

// Removes view-transition-name from the image
image.style.viewTransitionName = "none";

And that’s pretty much it! Let’s take a look at our example (in Chrome) with the transition element applied.

Customizing Animation Duration And Easing In CSS

What we just looked at is what I would call the default experience for the View Transitions API. We can do so much more than a transition that crossfades between two states. Specifically, just as you might expect from something that resembles a CSS animation, we can configure a view transition’s duration and timing function.

In fact, the View Transitions API makes use of CSS animation properties, and we can use them to fully customize the transition’s behavior. The difference is what we declare them on. Remember, a view transition is not part of the DOM, so what is available for us to select in CSS if it isn’t there?

When we run the startViewTransition function, the API pauses rendering, captures the new state of the page, and constructs a pseudo-element tree:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

Each one is helpful for customizing different parts of the transition:

  • ::view-transition: This is the root element, which you can consider the transition’s body element. The difference is that this pseudo-element is contained in an overlay that sits on top of everything else on the top.
    • ::view-transition-group: This mirrors the size and position between the old and new states.
      • ::view-transition-image-pair: This is the only child of ::view-transition-group, providing a container that isolates the blending work between the snapshots of the old and new transition states, which are direct children.
        • ::view-transition-old(...): A snapshot of the “old” transition state.
        • ::view-transition-new(...): A live representation of the new transition state.

Yes, there are quite a few moving parts! But the purpose of it is to give us tons of flexibility as far as selecting specific pieces of the transition.

So, remember when we applied view-transition-name: active-image to the .gallery__image--active class? Behind the scenes, the following pseudo-element tree is generated, and we can use the pseudo-elements to target either the active-image transition element or other elements on the page with the root value.

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(active-image)
   └─ ::view-transition-image-pair(active-image)
      ├─ ::view-transition-old(active-image)
      └─ ::view-transition-new(active-image)

In our example, we want to modify both the cross-fade (root) and transition element (active-image ) animations. We can use the universal selector (*) with the pseudo-element to change animation properties for all available transition elements and target pseudo-elements for specific animations using the page-transition-tag value.

/* Apply these styles only if API is supported */
@supports (view-transition-name: none) {
  /* Cross-fade animation */
  ::view-transition-image-pair(root) {
    animation-duration: 400ms;
    animation-timing-function: ease-in-out;
  }

  /* Image size and position animation */
  ::view-transition-group(active-image) {
    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
  }
}

Accessible Animations

Of course, any time we talk about movement on the web, we also ought to be mindful of users with motion sensitivities and ensure that we account for an experience that reduces motion.

That’s what the CSS prefers-reduced-motion query is designed for! With it, we can sniff out users who have enabled accessibility settings at the OS level that reduce motion and then reduce motion on our end of the work. The following example is a heavy-handed solution that nukes all animation in those instances, but it’s worth calling out that reduced motion does not always mean no motion. So, while this code will work, it may not be the best choice for your project, and your mileage may vary.

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Final Demo

Here is the completed demo with fallbacks and prefers-reduced-motion snippet implemented. Feel free to play around with easings and timings and further customize the animations.

This is a perfect example of how the View Transitions API tracks an element’s position and dimensions during animation and transitions between the old and new snapshots right out of the box!

See the Pen Add to cart animation v2 - completed [forked] by Adrian Bece.

Conclusion

It amazes me every time how the View Transitions API turns expensive-looking animations into somewhat trivial tasks with only a few lines of code. When done correctly, animations can breathe life into any project and offer a more delightful and memorable user experience.

That all being said, we still need to be careful how we use and implement animations. For starters, we’re still talking about a feature that is supported only in Chrome at the time of this writing. But with Safari’s positive stance on it and an open ticket to implement it in Firefox, there’s plenty of hope that we’ll get broader support — we just don’t know when.

Also, the View Transitions API may be “easy,” but it does not save us from ourselves. Think of things like slow or repetitive animations, needlessly complex animations, serving animations to those who prefer reduced motion, among other poor practices. Adhering to animation best practices has never been more important. The goal is to ensure that we’re using view transitions in ways that add delight and are inclusive rather than slapping them everywhere for the sake of showing off.

In another article to follow this one, we’ll use View Transitions API to create full-page transitions in our single-page and multi-page applications — you know, the sort of transitions we see when navigating between two views in a native mobile app. Now, we have those readily available for the web, too!

Until then, go build something awesome… and use it to experiment with the View Transitions API!

References

Gatsby Headaches: Working With Media (Part 2)

Gatsby is a true Jamstack framework. It works with React-powered components that consume APIs before optimizing and bundling everything to serve as static files with bits of reactivity. That includes media files, like images, video, and audio.

The problem is that there’s no “one” way to handle media in a Gatsby project. We have plugins for everything, from making queries off your local filesystem and compressing files to inlining SVGs and serving images in the responsive image format.

Which plugins should be used for certain types of media? How about certain use cases for certain types of media? That’s where you might encounter headaches because there are many plugins — some official and some not — that are capable of handling one or more use cases — some outdated and some not.

That is what this brief two-part series is about. In Part 1, we discussed various strategies and techniques for handling images, video, and audio in a Gatsby project.

This time, in Part 2, we are covering a different type of media we commonly encounter: documents. Specifically, we will tackle considerations for Gatsby projects that make use of Markdown and PDF files. And before wrapping up, we will also demonstrate an approach for using 3D models.

Solving Markdown Headaches In Gatsby

In Gatsby, Markdown files are commonly used to programmatically create pages, such as blog posts. You can write content in Markdown, parse it into your GraphQL data layer, source it into your components, and then bundle it as HTML static files during the build process.

Let’s learn how to load, query, and handle the Markdown for an existing page in Gatsby.

Loading And Querying Markdown From GraphQL

The first step on your Gatsby project is to load the project’s Markdown files to the GraphQL data layer. We can do this using the gatsby-source-filesystem plugin we used to query the local filesystem for image files in Part 1 of this series.

npm i gatsby-source-filesystem

In gatsby-config.js, we declare the folder where Markdown files will be saved in the project:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `assets`,
        path: `${ __dirname }/src/assets`,
      },
    },
  ],
};

Let’s say that we have the following Markdown file located in the project’s ./src/assets directory:

---
title: sample-markdown-file
date: 2023-07-29
---

# Sample Markdown File

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed consectetur imperdiet urna, vitae pellentesque mauris sollicitudin at. Sed id semper ex, ac vestibulum nunc. Etiam ,



bash
lorem ipsum dolor sit

## Subsection

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed consectetur imperdiet urna, vitae pellentesque mauris sollicitudin at. Sed id semper ex, ac vestibulum nunc. Etiam efficitur, nunc nec placerat dignissim, ipsum ante ultrices ante, sed luctus nisl felis eget ligula. Proin sed quam auctor, posuere enim eu, vulputate felis. Sed egestas, tortor

This example consists of two main sections: the frontmatter and body. It is a common structure for Markdown files.

  • Frontmatter
    Enclosed in triple dashes (---), this is an optional section at the beginning of a Markdown file that contains metadata and configuration settings for the document. In our example, the frontmatter contains information about the page’s title and date, which Gatsby can use as GraphQL arguments.
  • Body
    This is the content that makes up the page’s main body content.

We can use the gatsby-transformer-remark plugin to parse Markdown files to a GraphQL data layer. Once it is installed, we will need to register it in the project’s gatsby-config.js file:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-transformer-remark`,
      options: { },
    },
  ],
};

Restart the development server and navigate to http://localhost:8000/___graphql in the browser. Here, we can play around with Gatsby’s data layer and check our Markdown file above by making a query using the title property (sample-markdown-file) in the frontmatter:

query {
  markdownRemark(frontmatter: { title: { eq: "sample-markdown-file" } }) {
    html
  }
}

This should return the following result:

{
  "data": {
    "markdownRemark": {
      "html": "<h1>Sample Markdown File</h1>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed consectetur imperdiet urna, vitae pellentesque mauris sollicitudin at."
      // etc.
    }
  },
  "extensions": {}
}

Notice that the content in the response is formatted in HTML. We can also query the original body as rawMarkdownBody or any of the frontmatter attributes.

Next, let’s turn our attention to approaches for handling Markdown content once it has been queried.

Using DangerouslySetInnerHTML

dangerouslySetInnerHTML is a React feature that injects raw HTML content into a component’s rendered output by overriding the innerHTML property of the DOM node. It’s considered dangerous since it essentially bypasses React’s built-in mechanisms for rendering and sanitizing content, opening up the possibility of cross-site scripting (XSS) attacks without paying special attention.

That said, if you need to render HTML content dynamically but want to avoid the risks associated with dangerouslySetInnerHTML, consider using libraries that sanitize HTML input before rendering it, such as dompurify.

The dangerouslySetInnerHTML prop takes an __html object with a single key that should contain the raw HTML content. Here’s an example:

const DangerousComponent = () => {
  const rawHTML = "<p>This is <em>dangerous</em> content!</p>";

  return <div dangerouslySetInnerHTML={ { __html: rawHTML } } />;
};

To display Markdown using dangerouslySetInnerHTML in a Gatsby project, we need first to query the HTML string using Gatsby’s useStaticQuery hook:

import * as React from "react";
import { useStaticQuery, graphql } from "gatsby";

const DangerouslySetInnerHTML = () => {
  const data = useStaticQuery(graphqlquery {
      markdownRemark(frontmatter: { title: { eq: "sample-markdown-file" } }) {
        html
      }
    });

  return <div></div>;
};

Now, the html property can be injected into the dangerouslySetInnerHTML prop.

import * as React from "react";
import { useStaticQuery, graphql } from "gatsby";

const DangerouslySetInnerHTML = () => {
  const data = useStaticQuery(graphqlquery {
      markdownRemark(frontmatter: { title: { eq: "sample-markdown-file" } }) {
        html
      }
    });

  const markup = { __html: data.markdownRemark.html };

  return <div dangerouslySetInnerHTML={ markup }></div>;
};

This might look OK at first, but if we were to open the browser to view the content, we would notice that the image declared in the Markdown file is missing from the output. We never told Gatsby to parse it. We do have two options to include it in the query, each with pros and cons:

  1. Use a plugin to parse Markdown images.
    The gatsby-remark-images plugin is capable of processing Markdown images, making them available when querying the Markdown from the data layer. The main downside is the extra configuration it requires to set and render the files. Besides, Markdown images parsed with this plugin only will be available as HTML, so we would need to select a package that can render HTML content into React components, such as rehype-react.
  2. Save images in the static folder.
    The /static folder at the root of a Gatsby project can store assets that won’t be parsed by webpack but will be available in the public directory. Knowing this, we can point Markdown images to the /static directory, and they will be available anywhere in the client. The disadvantage? We are unable to leverage Gatsby’s image optimization features to minimize the overall size of the bundled package in the build process.

The gatsby-remark-images approach is probably most suited for larger projects since it is more manageable than saving all Markdown images in the /static folder.

Let’s assume that we have decided to go with the second approach of saving images to the /static folder. To reference an image in the /static directory, we just point to the filename without any special argument on the path.

const StaticImage = () => {
  return <img src={ "/desert.png" } alt="Desert" />;
};

react-markdown

The react-markdown package provides a component that renders markdown into React components, avoiding the risks of using dangerouslySetInnerHTML. The component uses a syntax tree to build the virtual DOM, which allows for updating only the changing DOM instead of completely overwriting it. And since it uses remark, we can combine react-markdown with remark’s vast plugin ecosystem.

Let’s install the package:

npm i react-markdown

Next, we replace our prior example with the ReactMarkdown component. However, instead of querying for the html property this time, we will query for rawMarkdownBody and then pass the result to ReactMarkdown to render it in the DOM.

import * as React from "react";
import ReactMarkdown from "react-markdown";
import { useStaticQuery, graphql } from "gatsby";

const MarkdownReact = () => {
  const data = useStaticQuery(graphqlquery {
      markdownRemark(frontmatter: { title: { eq: "sample-markdown-file" } }) {
        rawMarkdownBody
      }
    });

  return <ReactMarkdown>{data.markdownRemark.rawMarkdownBody}</ReactMarkdown>;
};

markdown-to-jsx

markdown-to-jsx is the most popular Markdown component — and the lightest since it comes without any dependencies. It’s an excellent tool to consider when aiming for performance, and it does not require remark’s plugin ecosystem. The plugin works much the same as the react-markdown package, only this time, we import a Markdown component instead of ReactMarkdown.

npm i markdown-to-jsx
import * as React from "react";
import Markdown from "markdown-to-jsx";
import { useStaticQuery, graphql } from "gatsby";

const MarkdownToJSX = () => {
  const data = useStaticQuery(graphqlquery {
      markdownRemark(frontmatter: { title: { eq: "sample-markdown-file" } }) {
        rawMarkdownBody
      }
    });

  return <Markdown> { data.markdownRemark.rawMarkdownBody }</Markdown>;
};

We have taken raw Markdown and parsed it as JSX. But what if we don’t necessarily want to parse it at all? We will look at that use case next.

react-md-editor

Let’s assume for a moment that we are creating a lightweight CMS and want to give users the option to write posts in Markdown. In this case, instead of parsing the Markdown to HTML, we need to query it as-is.

Rather than creating a Markdown editor from scratch to solve this, several packages are capable of handling the raw Markdown for us. My personal favorite is react-md-editor.

Let’s install the package:

npm i @uiw/react-md-editor

The MDEditor component can be imported and set up as a controlled component:

import * as React from "react";
import { useState } from "react";
import MDEditor from "@uiw/react-md-editor";

const ReactMDEditor = () => {
  const [value, setValue] = useState("**Hello world!!!**");

  return <MDEditor value={ value } onChange={ setValue } />;
};

The plugin also comes with a built-in MDEditor.Markdown component used to preview the rendered content:

import * as React from "react";
import { useState } from "react";
import MDEditor from "@uiw/react-md-editor";

const ReactMDEditor = () => {
  const [value, setValue] = useState("**Hello world!**");

  return (
    <>
      <MDEditor value={value} onChange={ setValue } />
      <MDEditor.Markdown source={ value } />
    </>
  );
};

That was a look at various headaches you might encounter when working with Markdown files in Gatsby. Next, we are turning our attention to another type of file, PDF.

Solving PDF Headaches In Gatsby

PDF files handle content with a completely different approach to Markdown files. With Markdown, we simplify the content to its most raw form so it can be easily handled across different front ends. PDFs, however, are the content presented to users on the front end. Rather than extracting the raw content from the file, we want the user to see it as it is, often by making it available for download or embedding it in a way that the user views the contents directly on the page, sort of like a video.

I want to show you four approaches to consider when embedding a PDF file on a page in a Gatsby project.

Using The <iframe> Element

The easiest way to embed a PDF into your Gatsby project is perhaps through an iframe element:

import * as React from "react";
import samplePDF from "./assets/lorem-ipsum.pdf";

const IframePDF = () => {
  return <iframe src={ samplePDF }></iframe>;
};

It’s worth calling out here that the iframe element supports lazy loading (loading="lazy") to boost performance in instances where it doesn’t need to load right away.

Embedding A Third-Party Viewer

There are situations where PDFs are more manageable when stored in a third-party service, such as Drive, which includes a PDF viewer that can embedded directly on the page. In these cases, we can use the same iframe we used above, but with the source pointed at the service.

import * as React from "react";

const ThirdPartyIframePDF = () => {
  return (
    <iframe
      src="https://drive.google.com/file/d/1IiRZOGib_0cZQY9RWEDslMksRykEnrmC/preview"
      allowFullScreen
      title="PDF Sample in Drive"
    />
  );
};

It’s a good reminder that you want to trust the third-party content that’s served in an iframe. If we’re effectively loading a document from someone else’s source that we do not control, your site could become prone to security vulnerabilities should that source become compromised.

Using react-pdf

The react-pdf package provides an interface to render PDFs as React components. It is based on pdf.js, a JavaScript library that renders PDFs using HTML Canvas.

To display a PDF file on a <canvas>, the react-pdf library exposes the Document and Page components:

  • Document: Loads the PDF passed in its file prop.
  • Page: Displays the page passed in its pageNumber prop. It should be placed inside Document.

We can install to our project:

npm i react-pdf

Before we put react-pdf to use, we will need to set up a service worker for pdf.js to process time-consuming tasks such as parsing and rendering a PDF document.

import * as React from "react";
import { pdfjs } from "react-pdf";

pdfjs.GlobalWorkerOptions.workerSrc = "https://unpkg.com/pdfjs-dist@3.6.172/build/pdf.worker.min.js";

const ReactPDF = () => {
  return <div></div>;
};

Now, we can import the Document and Page components, passing the PDF file to their props. We can also import the component’s necessary styles while we are at it.

import * as React from "react";
import { Document, Page } from "react-pdf";

import { pdfjs } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import "react-pdf/dist/esm/Page/TextLayer.css";

import samplePDF from "./assets/lorem-ipsum.pdf";

pdfjs.GlobalWorkerOptions.workerSrc = "https://unpkg.com/pdfjs-dist@3.6.172/build/pdf.worker.min.js";

const ReactPDF = () => {
  return (
    <Document file={ samplePDF }>
      <Page pageNumber={ 1 } />
    </Document>
  );
};

Since accessing the PDF will change the current page, we can add state management by passing the current pageNumber to the Page component:

import { useState } from "react";

// ...

const ReactPDF = () => {
  const [currentPage, setCurrentPage] = useState(1);

  return (
    <Document file={ samplePDF }>
      <Page pageNumber={ currentPage } />
    </Document>
  );
};

One issue is that we have pagination but don’t have a way to navigate between pages. We can change that by adding controls. First, we will need to know the number of pages in the document, which is accessed on the Document component’s onLoadSuccess event:

// ...

const ReactPDF = () => {
  const [pageNumber, setPageNumber] = useState(null);
  const [currentPage, setCurrentPage] = useState(1);

  const handleLoadSuccess = ({ numPages }) => {
    setPageNumber(numPages);
  };

  return (
    <Document file={ samplePDF } onLoadSuccess={ handleLoadSuccess }>
      <Page pageNumber={ currentPage } />
    </Document>
  );
};

Next, we display the current page number and add “Next” and “Previous” buttons with their respective handlers to change the current page:

// ...

const ReactPDF = () => {
  const [currentPage, setCurrentPage] = useState(1);
  const [pageNumber, setPageNumber] = useState(null);

  const handlePrevious = () => {
    // checks if it isn't the first page
    if (currentPage > 1) {
      setCurrentPage(currentPage - 1);
    }
  };

  const handleNext = () => {
    // checks if it isn't the last page
    if (currentPage < pageNumber) {
      setCurrentPage(currentPage + 1);
    }
  };

  const handleLoadSuccess = ({ numPages }) => {
    setPageNumber(numPages);
  };

  return (
    <div>
      <Document file={ samplePDF } onLoadSuccess={ handleLoadSuccess }>
        <Page pageNumber={ currentPage } />
      </Document>
      <button onClick={ handlePrevious }>Previous</button>
      <p>{currentPage}</p>
      <button onClick={ handleNext }>Next</button>
    </div>
  );
};

This provides us with everything we need to embed a PDF file on a page via the HTML <canvas> element using react-pdf and pdf.js.

There is another similar package capable of embedding a PDF file in a viewer, complete with pagination controls. We’ll look at that next.

Using react-pdf-viewer

Unlike react-pdf, the react-pdf-viewer package provides built-in customizable controls right out of the box, which makes embedding a multi-page PDF file a lot easier than having to import them separately.

Let’s install it:

npm i @react-pdf-viewer/core@3.12.0 @react-pdf-viewer/default-layout

Since react-pdf-viewer also relies on pdf.js, we will need to create a service worker as we did with react-pdf, but only if we are not using both packages at the same time. This time, we are using a Worker component with a workerUrl prop directed at the worker’s package.

import * as React from "react";
import { Worker } from "@react-pdf-viewer/core";

const ReactPDFViewer = () => {
  return (
    <>
      <Worker workerUrl="https://unpkg.com/pdfjs-dist@3.4.120/build/pdf.worker.min.js"></Worker>
    </>
  );
};

Note that a worker like this ought to be set just once at the layout level. This is especially true if you intend to use the PDF viewer across different pages.

Next, we import the Viewer component with its styles and point it at the PDF through its fileUrl prop.

import * as React from "react";
import { Viewer, Worker } from "@react-pdf-viewer/core";

import "@react-pdf-viewer/core/lib/styles/index.css";

import samplePDF from "./assets/lorem-ipsum.pdf";

const ReactPDFViewer = () => {
  return (
    <>
      <Viewer fileUrl={ samplePDF } />
      <Worker workerUrl="https://unpkg.com/pdfjs-dist@3.6.172/build/pdf.worker.min.js"></Worker>
    </>
  );
};

Once again, we need to add controls. We can do that by importing the defaultLayoutPlugin (including its corresponding styles), making an instance of it, and passing it in the Viewer component’s plugins prop.

import * as React from "react";
import { Viewer, Worker } from "@react-pdf-viewer/core";
import { defaultLayoutPlugin } from "@react-pdf-viewer/default-layout";

import "@react-pdf-viewer/core/lib/styles/index.css";
import "@react-pdf-viewer/default-layout/lib/styles/index.css";

import samplePDF from "./assets/lorem-ipsum.pdf";

const ReactPDFViewer = () => {
  const defaultLayoutPluginInstance = defaultLayoutPlugin();

  return (
    <>
      <Viewer fileUrl={ samplePDF } plugins={ [defaultLayoutPluginInstance] } />
      <Worker workerUrl="https://unpkg.com/pdfjs-dist@3.6.172/build/pdf.worker.min.js"></Worker>
    </>
  );
};

Again, react-pdf-viewer is an alternative to react-pdf that can be a little easier to implement if you don’t need full control over your PDF files, just the embedded viewer.

There is one more plugin that provides an embedded viewer for PDF files. We will look at it, but only briefly, because I personally do not recommend using it in favor of the other approaches we’ve covered.

Why You Shouldn’t Use react-file-viewer

The last plugin we will check out is react-file-viewer, a package that offers an embedded viewer with a simple interface but with the capacity to handle a variety of media in addition to PDF files, including images, videos, PDFs, documents, and spreadsheets.

import * as React from "react";
import FileViewer from "react-file-viewer";

const PDFReactFileViewer = () => {
  return <FileViewer fileType="pdf" filePath="/lorem-ipsum.pdf" />;
};

While react-file-viewer will get the job done, it is extremely outdated and could easily create more headaches than it solves with compatibility issues. I suggest avoiding it in favor of either an iframe, react-pdf, or react-pdf-viewer.

Solving 3D Model Headaches In Gatsby

I want to cap this brief two-part series with one more media type that might cause headaches in a Gatsby project: 3D models.

A 3D model file is a digital representation of a three-dimensional object that stores information about the object’s geometry, texture, shading, and other properties of the object. On the web, 3D model files are used to enhance user experiences by bringing interactive and immersive content to websites. You are most likely to encounter them in product visualizations, architectural walkthroughs, or educational simulations.

There is a multitude of 3D model formats, including glTF OBJ, FBX, STL, and so on. We will use glTF models for a demonstration of a headache-free 3D model implementation in Gatsby.

The GL Transmission Format (glTF) was designed specifically for the web and real-time applications, making it ideal for our example. Using glTF files does require a specific webpack loader, so for simplicity’s sake, we will save the glTF model in the /static folder at the root of our project as we look at two approaches to create the 3D visual with Three.js:

  1. Using a vanilla implementation of Three.js,
  2. Using a package that integrates Three.js as a React component.

Using Three.js

Three.js creates and loads interactive 3D graphics directly on the web with the help of WebGL, a JavaScript API for rendering 3D graphics in real-time inside HTML <canvas> elements.

Three.js is not integrated with React or Gatsby out of the box, so we must modify our code to support it. A Three.js tutorial is out of scope for what we are discussing in this article, although excellent learning resources are available in the Three.js documentation.

We start by installing the three library to the Gatsby project:

npm i three

Next, we write a function to load the glTF model for Three.js to reference it. This means we need to import a GLTFLoader add-on to instantiate a new loader object.

import * as React from "react";
import * as THREE from "three";

import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const loadModel = async (scene) => {
  const loader = new GLTFLoader();
};

We use the scene object as a parameter in the loadModel function so we can attach our 3D model once loaded to the scene.

From here, we use loader.load() which takes four arguments:

  1. The glTF file location,
  2. A callback when the resource is loaded,
  3. A callback while loading is in progress,
  4. A callback for handling errors.
import * as React from "react";
import * as THREE from "three";

import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const loadModel = async (scene) => {
  const loader = new GLTFLoader();

  await loader.load(
    "/strawberry.gltf", // glTF file location
    function (gltf) {
      // called when the resource is loaded
      scene.add(gltf.scene);
    },
    undefined, // called while loading is in progress, but we are not using it
    function (error) {
      // called when loading returns errors
      console.error(error);
    }
  );
};

Let’s create a component to host the scene and load the 3D model. We need to know the element’s client width and height, which we can get using React’s useRef hook to access the element’s DOM properties.

import * as React from "react";
import * as THREE from "three";

import { useRef, useEffect } from "react";

// ...

const ThreeLoader = () => {
  const viewerRef = useRef(null);

  return <div style={ { height: 600, width: "100%" } } ref={ viewerRef }></div>; // Gives the element its dimensions
};

Since we are using the element’s clientWidth and clientHeight properties, we need to create the scene on the client side inside React’s useEffect hook where we configure the Three.js scene with its necessary complements, e.g., a camera, the WebGL renderer, and lights.

useEffect(() => {
  const { current: viewer } = viewerRef;

  const scene = new THREE.Scene();

  const camera = new THREE.PerspectiveCamera(75, viewer.clientWidth / viewer.clientHeight, 0.1, 1000);

  const renderer = new THREE.WebGLRenderer();

  renderer.setSize(viewer.clientWidth, viewer.clientHeight);

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
  scene.add(ambientLight);

  const directionalLight = new THREE.DirectionalLight(0xffffff);
  directionalLight.position.set(0, 0, 5);
  scene.add(directionalLight);

  viewer.appendChild(renderer.domElement);
  renderer.render(scene, camera);
}, []);

Now we can invoke the loadModel function, passing the scene to it as the only argument:

useEffect(() => {
  const { current: viewer } = viewerRef;

  const scene = new THREE.Scene();

  const camera = new THREE.PerspectiveCamera(75, viewer.clientWidth / viewer.clientHeight, 0.1, 1000);

  const renderer = new THREE.WebGLRenderer();

  renderer.setSize(viewer.clientWidth, viewer.clientHeight);

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
  scene.add(ambientLight);

  const directionalLight = new THREE.DirectionalLight(0xffffff);
  directionalLight.position.set(0, 0, 5);
  scene.add(directionalLight);

  loadModel(scene); // Here!

  viewer.appendChild(renderer.domElement);
  renderer.render(scene, camera);
}, []);

The last part of this vanilla Three.js implementation is to add OrbitControls that allow users to navigate the model. That might look something like this:

import * as React from "react";
import * as THREE from "three";

import { useRef, useEffect } from "react";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const loadModel = async (scene) => {
  const loader = new GLTFLoader();

  await loader.load(
    "/strawberry.gltf", // glTF file location
    function (gltf) {
      // called when the resource is loaded
      scene.add(gltf.scene);
    },
    undefined, // called while loading is in progress, but it is not used
    function (error) {
      // called when loading has errors
      console.error(error);
    }
  );
};

const ThreeLoader = () => {
  const viewerRef = useRef(null);

  useEffect(() => {
    const { current: viewer } = viewerRef;

    const scene = new THREE.Scene();

    const camera = new THREE.PerspectiveCamera(75, viewer.clientWidth / viewer.clientHeight, 0.1, 1000);

    const renderer = new THREE.WebGLRenderer();

    renderer.setSize(viewer.clientWidth, viewer.clientHeight);

    const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(0, 0, 5);
    scene.add(directionalLight);

    loadModel(scene);

    const target = new THREE.Vector3(-0.5, 1.2, 0);
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target = target;

    viewer.appendChild(renderer.domElement);

    var animate = function () {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    };
    animate();
  }, []);

  <div style={ { height: 600, width: "100%" } } ref={ viewerRef }></div>;
};

That is a straight Three.js implementation in a Gatsby project. Next is another approach using a library.

Using React Three Fiber

react-three-fiber is a library that integrates the Three.js with React. One of its advantages over the vanilla Three.js approach is its ability to manage and update 3D scenes, making it easier to compose scenes without manually handling intricate aspects of Three.js.

We begin by installing the library to the Gatsby project:

npm i react-three-fiber @react-three/drei

Notice that the installation command includes the @react-three/drei package, which we will use to add controls to the 3D viewer.

I personally love react-three-fiber for being tremendously self-explanatory. For example, I had a relatively easy time migrating the extensive chunk of code from the vanilla approach to this much cleaner code:

import * as React from "react";
import { useLoader, Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

const ThreeFiberLoader = () => {
  const gltf = useLoader(GLTFLoader, "/strawberry.gltf");

  return (
    <Canvas camera={ { fov: 75, near: 0.1, far: 1000, position: [5, 5, 5] } } style={ { height: 600, width: "100%" } }>
      <ambientLight intensity={ 0.4 } />
      <directionalLight color="white" />
      <primitive object={ gltf.scene } />
      <OrbitControls makeDefault />
    </Canvas>
  );
};

Thanks to react-three-fiber, we get the same result as a vanilla Three.js implementation but with fewer steps, more efficient code, and a slew of abstractions for managing and updating Three.js scenes.

Two Final Tips

The last thing I want to leave you with is two final considerations to take into account when working with media files in a Gatsby project.

Bundling Assets Via Webpack And The /static Folder

Importing an asset as a module so it can be bundled by webpack is a common strategy to add post-processing and minification, as well as hashing paths on the client. But there are two additional use cases where you might want to avoid it altogether and use the static folder in a Gatsby project:

  • Referencing a library outside the bundled code to prevent webpack compatibility issues or a lack of specific loaders.
  • Referencing assets with a specific name, for example, in a web manifest file.

You can find a detailed explanation of the static folder and use it to your advantage in the Gatsby documentation.

Embedding Files From Third-Party Services

Secondly, you can never be too cautious when embedding third-party services on a website. Replaced content elements, like <iframe>, can introduce various security vulnerabilities, particularly when you do not have control of the source content. By integrating a third party’s scripts, widgets, or content, a website or app is prone to potential vulnerabilities, such as iframe injection or cross-frame scripting.

Moreover, if an integrated third-party service experiences downtime or performance issues, it can directly impact the user experience.

Conclusion

This article explored various approaches for working around common headaches you may encounter when working with Markdown, PDF, and 3D model files in a Gatsby project. In the process, we leveraged several React plugins and Gatsby features that handle how content is parsed, embed files on a page, and manage 3D scenes.

This is also the second article in a brief two-part series that addresses common headaches working with a variety of media types in Gatsby. The first part covers more common media files, including images, video, and audio.

If you’re looking for more cures to Gatsby headaches, please check out my other two-part series that investigates internationalization.

See Also

How We Optimized Performance To Serve A Global Audience

I work for Bookaway, a digital travel brand. As an online booking platform, we connect travelers with transport providers worldwide, offering bus, ferry, train, and car transfers in over 30 countries. We aim to eliminate the complexity and hassle associated with travel planning by providing a one-stop solution for all transportation needs.

A cornerstone of our business model lies in the development of effective landing pages. These pages serve as a pivotal tool in our digital marketing strategy, not only providing valuable information about our services but also designed to be easily discoverable through search engines. Although landing pages are a common practice in online marketing, we were trying to make the most of it.

SEO is key to our success. It increases our visibility and enables us to draw a steady stream of organic (or “free”) traffic to our site. While paid marketing strategies like Google Ads play a part in our approach as well, enhancing our organic traffic remains a major priority. The higher our organic traffic, the more profitable we become as a company.

We’ve known for a long time that fast page performance influences search engine rankings. It was only in 2020, though, that Google shared its concept of Core Web Vitals and how it impacts SEO efforts. Our team at Bookaway recently underwent a project to improve Web Vitals, and I want to give you a look at the work it took to get our existing site in full compliance with Google’s standards and how it impacted our search presence.

SEO And Web Vitals

In the realm of search engine optimization, performance plays a critical role. As the world’s leading search engine, Google is committed to delivering the best possible search results to its users. This commitment involves prioritizing websites that offer not only relevant content but also an excellent user experience.

Google’s Core Web Vitals is a set of performance metrics that site owners can use to evaluate performance and diagnose performance issues. These metrics provide a different perspective on user experience:

  • Largest Contentful Paint (LCP)
    Measures the time it takes for the main content on a webpage to load.
  • First Input Delay (FID)
    Assesses the time it takes for a page to become interactive.
    Note: Google plans to replace this metric with another one called Interaction to Next Paint (INP) beginning in 2024.
  • Cumulative Layout Shift (CLS)
    Calculates the visual stability of a page.

While optimizing for FID and CLS was relatively straightforward, LCP posed a greater challenge due to the multiple factors involved. LCP is particularly vital for landing pages, which are predominantly content and often the first touch-point a visitor has with a website. A low LCP ensures that visitors can view the main content of your page sooner, which is critical for maintaining user engagement and reducing bounce rates.

Largest Contentful Paint (LCP)

LCP measures the perceived load speed of a webpage from a user’s perspective. It pinpoints the moment during a page’s loading phase when the primary — or “largest” — content has been fully rendered on the screen. This could be an image, a block of text, or even an embedded video. LCP is an essential metric because it gives a real-world indication of the user experience, especially for content-heavy sites.

However, achieving a good LCP score is often a multi-faceted process that involves optimizing several stages of loading and rendering. Each stage has its unique challenges and potential pitfalls, as other case studies show.

Here’s a breakdown of the moving pieces.

Time To First Byte (TTFB)

This is the time it takes for the first piece of information from the server to reach the user’s browser. You need to beware that slow server response times can significantly increase TTFB, often due to server overload, network issues, or un-optimized logic on the server side.

Download Time of HTML

This is the time it takes to download the page’s HTML file. You need to beware of large HTML files or slow network connections because they can lead to longer download times.

HTML Processing

Once a web page’s HTML file has been downloaded, the browser begins to process the contents line by line, translating code into the visual website that users interact with. If, during this process, the browser encounters a <script> or <style> tag that lacks either an async or deferred attribute, the rendering of the webpage comes to a halt.

The browser must then pause to fetch and parse the corresponding files. These files can be complex and potentially take a significant amount of time to download and interpret, leading to a noticeable delay in the loading and rendering of the webpage. This is why the async and deferred attributes are crucial, as they ensure an efficient, seamless web browsing experience.

Fetching And Decoding Images

This is the time taken to fetch, download, and decode images, particularly the largest contentful image. You need to look out for large image file sizes or improperly optimized images that can delay the fetching and decoding process.

First Contentful Paint (FCP)

This is the time it takes for the browser to render the first bit of content from the DOM. You need to beware of slow server response times, particularly render-blocking JavaScript or CSS, or slow network connections, all of which can negatively affect FCP.

Rendering the Largest Contentful Element

This is the time taken until the largest contentful element (like a hero image or heading text) is fully rendered on the page. You need to watch out for complex design elements, large media files, or slow browser rendering can delay the time it takes for the largest contentful element to render.

Understanding and optimizing each of these stages can significantly improve a website’s LCP, thereby enhancing the user experience and SEO rankings.

I know that is a lot of information to unpack in a single sitting, and it definitely took our team time to wrap our minds around what it takes to achieve a low LCP score. But once we had a good understanding, we knew exactly what to look for and began analyzing the analytics of our user data to identify areas that could be improved.

Analyzing User Data

To effectively monitor and respond to our website’s performance, we need a robust process for collecting and analyzing this data.

Here’s how we do it at Bookaway.

Next.js For Performance Monitoring

Many of you reading this may already be familiar with Next.js, but it is a popular open-source JavaScript framework that allows us to monitor our website’s performance in real-time.

One of the key Next.js features we leverage is the reportWebVitals function, a hook that allows us to capture the Web Vitals metrics for each page load. We can then forward this data to a custom analytics service. Most importantly, the function provides us with in-depth insights into our user experiences in real-time, helping us identify any performance issues as soon as they arise.

Storing Data In BigQuery For Comprehensive Analysis

Once we capture the Web Vitals metrics, we store this data in BigQuery, Google Cloud’s fully-managed, serverless data warehouse. Alongside the Web Vitals data, we also record a variety of other important details, such as the date of the page load, the route, whether the user was on a mobile or desktop device, and the language settings. This comprehensive dataset allows us to examine our website’s performance from multiple angles and gain deeper insights into the user experience.

The screenshot features an SQL query from a data table, focusing on the LCP web vital. It shows the retrieval of LCP values (in milliseconds) for specific visits across three unique page URLs that, in turn, represent three different landing pages we serve:

These values indicate how quickly major content items on these pages become fully visible to users.

Visualizing Data with Looker Studio

We visualize performance data using Google’s Looker Studio (formerly called Data Studio). By transforming our raw data into interactive dashboards and reports, we can easily identify trends, pinpoint issues, and monitor improvements over time. These visualizations empower us to make data-driven decisions that enhance our website’s performance and, ultimately, improve our users’ experience.

Looker Studio offers a few key advantages:

  • Easy-to-use interface
    Looker Studio is intuitive and user-friendly, making it easy for anyone on our team to create and customize reports.
  • Real-time data
    Looker Studio can connect directly to BigQuery, enabling us to create reports using real-time data.
  • Flexible and customizable
    Looker Studio enables us to create customized reports and dashboards that perfectly suit our needs.

Here are some examples:

This screenshot shows a crucial functionality we’ve designed within Looker Studio: the capability to filter data by specific groups of pages. This custom feature proves to be invaluable in our context, where we need granular insights about different sections of our website. As the image shows, we’re honing in on our “Route Landing Page” group. This subset of pages has experienced over one million visits in the last week alone, highlighting the significant traffic these pages attract. This demonstration exemplifies how our customizations in Looker Studio help us dissect and understand our site’s performance at a granular level.

The graph presents the LCP values for the 75th percentile of our users visiting the Route Landing Page group. This percentile represents the user experience of the “average” user, excluding outliers who may have exceptionally good or poor conditions.

A key advantage of using Looker Studio is its ability to segment data based on different variables. In the following screenshot, you can see that we have differentiated between mobile and desktop traffic.

Understanding The Challenges

In our journey, the key performance data we gathered acted as a compass, pointing us toward specific challenges that lay ahead. Influenced by factors such as global audience diversity, seasonality, and the intricate balance between static and dynamic content, these challenges surfaced as crucial areas of focus. It is within these complexities that we found our opportunity to refine and optimize web performance on a global scale.

Seasonality And A Worldwide Audience

As an international platform, Bookaway serves a diverse audience from various geographic locations. One of the key challenges that come with serving a worldwide audience is the variation in network conditions and device capabilities across different regions.

Adding to this complexity is the effect of seasonality. Much like physical tourism businesses, our digital platform also experiences seasonal trends. For instance, during winter months, our traffic increases from countries in warmer climates, such as Thailand and Vietnam, where it’s peak travel season. Conversely, in the summer, we see more traffic from European countries where it’s the high season for tourism.

The variation in our performance metrics, correlated with geographic shifts in our user base, points to a clear area of opportunity. We realized that we needed to consider a more global and scalable solution to better serve our global audience.

This understanding prompted us to revisit our approach to content delivery, which we’ll get to in a moment.

Layout Shifts From Dynamic And Static Content

We have been using dynamic content serving, where each request reaches our back-end server and triggers processes like database retrievals and page renderings. This server interaction is reflected in the TTFB metric, which measures the duration from the client making an HTTP request to the first byte being received by the client’s browser. The shorter the TTFB, the better the perceived speed of the site from the user’s perspective.

While dynamic serving provides simplicity in implementation, it imposes significant time costs due to the computational resources required to generate the pages and the latency involved in serving these pages to users at distant locations.

We recognize the potential benefits of serving static content, which involves delivering pre-generated HTML files like you would see in a Jamstack architecture. This could significantly improve the speed of our content delivery as it eliminates the need for on-the-fly page generation, thereby reducing TTFB. It also opens up the possibility for more effective use of caching strategies, potentially enhancing load times further.

As we envisage a shift from dynamic to static content serving, we anticipate it to be a crucial step toward improving our LCP metrics and providing a more consistent user experience across all regions and seasons.

In the following sections, we’ll explore the potential challenges and solutions we could encounter as we consider this shift. We’ll also discuss our thoughts on implementing a Content Delivery Network (CDN), which could allow us to fully leverage the advantages of static content serving.

Leveraging A CDN For Content Delivery

I imagine many of you already understand what a CDN is, but it is essentially a network of servers, often referred to as “edges.” These edge servers are distributed in data centers across the globe. Their primary role is to store (or “cache”) copies of web content — like HTML pages, JavaScript files, and multimedia content — and deliver it to users based on their geographic location.

When a user makes a request to access a website, the DNS routes the request to the edge server that’s geographically closest to the user. This proximity significantly reduces the time it takes for the data to travel from the server to the user, thus reducing latency and improving load times.

A key benefit of this mechanism is that it effectively transforms dynamic content delivery into static content delivery. When the CDN caches a pre-rendered HTML page, no additional server-side computations are required to serve that page to the user. This not only reduces load times but also reduces the load on our origin servers, enhancing our capacity to serve high volumes of traffic.

If the requested content is cached on the edge server and the cache is still fresh, the CDN can immediately deliver it to the user. If the cache has expired or the content isn’t cached, the CDN will retrieve the content from the origin server, deliver it to the user, and update its cache for future requests.

This caching mechanism also improves the website’s resilience to distributed denial-of-service (DDoS) attacks. By serving content from edge servers and reducing the load on the origin server, the CDN provides an additional layer of security. This protection helps ensure the website remains accessible even under high-traffic conditions.

CDN Implementation

Recognizing the potential benefits of a CDN, we decided to implement one for our landing pages. As our entire infrastructure is already hosted by Amazon Web Services (AWS), choosing Amazon AWS CloudFront as our CDN solution was an immediate and obvious choice. Its robust infrastructure, scalability, and a wide network of edge locations around the world made it a strong candidate.

During the implementation process, we configured a key setting known as max-age. This determines how long a page remains “fresh.” We set this property to three days, and for those three days, any visitor who requests a page is quickly served with the cached version from the nearest edge location. After the three-day period, the page would no longer be considered “fresh.” The next visitor requesting that page wouldn’t receive the cached version from the edge location but would have to wait for the CDN to reach our origin servers and generate a fresh page.

This approach offered an exciting opportunity for us to enhance our web performance. However, transitioning to a CDN system also posed new challenges, particularly with the multitude of pages that were rarely visited. The following sections will discuss how we navigated these hurdles.

Addressing Many Pages With Rare Visits

Adopting the AWS CloudFront CDN significantly improved our website’s performance. However, it also introduced a unique problem: our “long tail” of rarely visited pages. With over 100,000 landing pages, each available in seven different languages, we managed a total of around 700,000 individual pages.

Many of these pages were rarely visited. Individually, each accounted for a small percentage of our total traffic. Collectively, however, they made up a substantial portion of our web content.

The infrequency of visits meant that our CDN’s max-age setting of three days would often expire without a page being accessed in that timeframe. This resulted in these pages falling out of the CDN’s cache. Consequently, the next visitor requesting that page would not receive the cached version. Instead, they would have to wait for the CDN to reach our origin server and fetch a fresh page.

To address this, we adopted a strategy known as stale-while-revalidate. This approach allows the CDN to serve a stale (or expired) page to the visitor, while simultaneously validating the freshness of the page with the origin server. If the server’s page is newer, it is updated in the cache.

This strategy had an immediate impact. We observed a marked and continuous enhancement in the performance of our long-tail pages. It allowed us to ensure a consistently speedy experience across our extensive range of landing pages, regardless of their frequency of visits. This was a significant achievement in maintaining our website’s performance while serving a global audience.

I am sure you are interested in the results. We will examine them in the next section.

Performance Optimization Results

Our primary objective in these optimization efforts was to reduce the LCP metric, a crucial aspect of our landing pages. The implementation of our CDN solution had an immediate positive impact, reducing LCP from 3.5 seconds to 2 seconds. Further applying the stale-while-revalidate strategy resulted in an additional decrease in LCP, bringing it down to 1.7 seconds.

A key component in the sequence of events leading to LCP is the TTFB, which measures the time from the user’s request to the receipt of the first byte of data by the user’s browser. The introduction of our CDN solution prompted a dramatic decrease in TTFB, from 2 seconds to 1.24 seconds.

Stale-While-Revalidate Improvement

This substantial reduction in TTFB was primarily achieved by transitioning to static content delivery, eliminating the need for back-end server processing for each request, and by capitalizing on CloudFront’s global network of edge locations to minimize network latency. This allowed users to fetch assets from a geographically closer source, substantially reducing processing time.

Therefore, it’s crucial to highlight that

The significant improvement in TTFB was one of the key factors that contributed to the reduction in our LCP time. This demonstrates the interdependent nature of web performance metrics and how enhancements in one area can positively impact others.

The overall LCP improvement — thanks to stale-while-revalidate — was around 15% for the 75th percentile.

User Experience Results

The “Page Experience” section in Google Search Console evaluates your website’s user experience through metrics like load times, interactivity, and content stability. It also reports on mobile usability, security, and best practices such as HTTPS. The screenshot below illustrates the substantial improvement in our site’s performance due to our implementation of the stale-while-revalidate strategy.

Conclusion

I hope that documenting the work we did at Bookaway gives you a good idea of the effort that it takes to tackle improvements for Core Web Vitals. Even though there is plenty of documentation and tutorials about them, I know it helps to know what it looks like in a real-life project.

And since everything I have covered in this article is based on a real-life project, it’s entirely possible that the insights we discovered at Bookaway will differ from yours. Where LCP was the primary focus for us, you may very well find that another Web Vital metric is more pertinent to your scenario.

That said, here are the key lessons I took away from my experience:

  • Optimize Website Loading and Rendering.
    Pay close attention to the stages of your website’s loading and rendering process. Each stage — from TTFB, download time of HTML, and FCP, to fetching and decoding of images, parsing of JavaScript and CSS, and rendering of the largest contentful element — needs to be optimized. Understand potential pitfalls at each stage and make necessary adjustments to improve your site’s overall user experience.
  • Implement Performance Monitoring Tools.
    Utilize tools such as Next.js for real-time performance monitoring and BigQuery for storing and analyzing data. Visualizing your performance data with tools like Looker Studio can help provide valuable insights into your website’s performance, enabling you to make informed, data-driven decisions.
  • Consider Static Content Delivery and CDN.
    Transitioning from dynamic to static content delivery can greatly reduce the TTFB and improve site loading speed. Implementing a CDN can further optimize performance by serving pre-rendered HTML pages from edge servers close to the user’s location, reducing latency and improving load times.

Further Reading On SmashingMag

Recreating YouTube’s Ambient Mode Glow Effect

I noticed a charming effect on YouTube’s video player while using its dark theme some time ago. The background around the video would change as the video played, creating a lush glow around the video player, making an otherwise bland background a lot more interesting.

This effect is called Ambient Mode. The feature was released sometime in 2022, and YouTube describes it like this:

“Ambient mode uses a lighting effect to make watching videos in the Dark theme more immersive by casting gentle colors from the video into your screen’s background.”
— YouTube

It is an incredibly subtle effect, especially when the video’s colors are dark and have less contrast against the dark theme’s background.

Curiosity hit me, and I set out to replicate the effect on my own. After digging around YouTube’s convoluted DOM tree and source code in DevTools, I hit an obstacle: all the magic was hidden behind the HTML <canvas> element and bundles of mangled and minified JavaScript code.

Despite having very little to go on, I decided to reverse-engineer the code and share my process for creating an ambient glow around the videos. I prefer to keep things simple and accessible, so this article won’t involve complicated color sampling algorithms, although we will utilize them via different methods.

Before we start writing code, I think it’s a good idea to revisit the HTML Canvas element and see why and how it is used for this little effect.

HTML Canvas

The HTML <canvas> element is a container element on which we can draw graphics with JavaScript using its own Canvas API and WebGL API. Out of the box, a <canvas> is empty — a blank canvas, if you will — and the aforementioned Canvas and WebGL APIs are used to fill the <canvas> with content.

HTML <canvas> is not limited to presentation; we can also make interactive graphics with them that respond to standard mouse and keyboard events.

But SVG can also do most of that stuff, right? That’s true, but <canvas> is more performant than SVG because it doesn’t require any additional DOM nodes for drawing paths and shapes the way SVG does. Also, <canvas> is easy to update, which makes it ideal for more complex and performance-heavy use cases, like YouTube’s Ambient Mode.

As you might expect with many HTML elements, <canvas> accepts attributes. For example, we can give our drawing space a width and height:

<canvas width="10" height="6" id="js-canvas"></canvas>

Notice that <canvas> is not a self-closing tag, like an <iframe> or <img>. We can add content between the opening and closing tags, which is rendered only when the browser cannot render the canvas. This can also be useful for making the element more accessible, which we’ll touch on later.

Returning to the width and height attributes, they define the <canvas>’s coordinate system. Interestingly, we can apply a responsive width using relative units in CSS, but the <canvas> still respects the set coordinate system. We are working with pixel graphics here, so stretching a smaller canvas in a wider container results in a blurry and pixelated image.

The downside of <canvas> is its accessibility. All of the content updates happen in JavaScript in the background as the DOM is not updated, so we need to put effort into making it accessible ourselves. One approach (of many) is to create a Fallback DOM by placing standard HTML elements inside the <canvas>, then manually updating them to reflect the current content that is displayed on the canvas.

Numerous canvas frameworks — including ZIM, Konva, and Fabric, to name a few — are designed for complex use cases that can simplify the process with a plethora of abstractions and utilities. ZIM’s framework has accessibility features built into its interactive components, which makes developing accessible <canvas>-based experiences a bit easier.

For this example, we’ll use the Canvas API. We will also use the element for decorative purposes (i.e., it doesn’t introduce any new content), so we won’t have to worry about making it accessible, but rather safely hide the <canvas> from assistive devices.

That said, we will still need to disable — or minimize — the effect for those who have enabled reduced motion settings at the system or browser level.

requestAnimationFrame

The <canvas> element can handle the rendering part of the problem, but we need to somehow keep the <canvas> in sync with the playing <video>and make sure that the <canvas> updates with each video frame. We’ll also need to stop the sync if the video is paused or has ended.

We could use setInterval in JavaScript and rig it to run at 60fps to match the video’s playback rate, but that approach comes with some problems and caveats. Luckily, there is a better way of handling a function that must be called on so often.

That is where the requestAnimationFrame method comes in. It instructs the browser to run a function before the next repaint. That function runs asynchronously and returns a number that represents the request ID. We can then use the ID with the cancelAnimationFrame function to instruct the browser to stop running the previously scheduled function.

let requestId;

const loopStart = () => {
  /* ... */

  /* Initialize the infinite loop and keep track of the requestId */
  requestId = window.requestAnimationFrame(loopStart);
};

const loopCancel = () => {
  window.cancelAnimationFrame(requestId);
  requestId = undefined;
};

Now that we have all our bases covered by learning how to keep our update loop and rendering performant, we can start working on the Ambient Mode effect!

The Approach

Let’s briefly outline the steps we’ll take to create this effect.

First, we must render the displayed video frame on a canvas and keep everything in sync. We’ll render the frame onto a smaller canvas (resulting in a pixelated image). When an image is downscaled, the important and most-dominant parts of an image are preserved at the cost of losing small details. By reducing the image to a low resolution, we’re reducing it to the most dominant colors and details, effectively doing something similar to color sampling, albeit not as accurately.

Next, we’ll blur the canvas, which blends the pixelated colors. We will place the canvas behind the video using CSS absolute positioning.

And finally, we’ll apply additional CSS to make the glow effect a bit more subtle and as close to YouTube’s effect as possible.

HTML Markup

First, let’s start by setting up the markup. We’ll need to wrap the <video> and <canvas> elements in a parent container because that allows us to contain the absolute positioning we will be using to position the <canvas> behind the <video>. But more on that in a moment.

Next, we will set a fixed width and height on the <canvas>, although the element will remain responsive. By setting the width and height attributes, we define the coordinate space in CSS pixels. The video’s frame is 1920×720, so we will draw an image that is 10×6 pixels image on the canvas. As we’ve seen in the previous examples, we’ll get a pixelated image with dominant colors somewhat preserved.

<section class="wrapper">
  <video controls muted class="video" id="js-video" src="video.mp4"></video>
  <canvas width="10" height="6" aria-hidden="true" class="canvas" id="js-canvas"></canvas>
</section>
Syncing <canvas> And <video>

First, let’s start by setting up our variables. We need the <canvas>’s rendering context to draw on it, so saving it as a variable is useful, and we can do that by using JavaScript’s getCanvasContext function. We’ll also use a variable called step to keep track of the request ID of the requestAnimationFrame method.

const video = document.getElementById("js-video");
const canvas = document.getElementById("js-canvas");
const ctx = canvas.getContext("2d");

let step; // Keep track of requestAnimationFrame id

Next, we’ll create the drawing and update loop functions. We can actually draw the current video frame on the <canvas> by passing the <video> element to the drawImage function, which takes four values corresponding to the video’s starting and ending points in the <canvas> coordinate system, which, if you remember, is mapped to the width and height attributes in the markup. It’s that simple!

const draw = () => {
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
};

Now, all we need to do is create the loop that calls the drawImage function while the video is playing, as well as a function that cancels the loop.

const drawLoop = () => {
  draw();
  step = window.requestAnimationFrame(drawLoop);
};

const drawPause = () => {
  window.cancelAnimationFrame(step);
  step = undefined;
};

And finally, we need to create two main functions that set up and clear event listeners on page load and unload, respectively. These are all of the video events we need to cover:

  • loadeddata: This fires when the first frame of the video loads. In this case, we only need to draw the current frame onto the canvas.
  • seeked: This fires when the video finishes seeking and is ready to play (i.e., the frame has been updated). In this case, we only need to draw the current frame onto the canvas.
  • play: This fires when the video starts playing. We need to start the loop for this event.
  • pause: This fires when the video is paused. We need to stop the loop for this event.
  • ended: This fires when the video stops playing when it reaches its end. We need to stop the loop for this event.
const init = () => {
  video.addEventListener("loadeddata", draw, false);
  video.addEventListener("seeked", draw, false);
  video.addEventListener("play", drawLoop, false);
  video.addEventListener("pause", drawPause, false);
  video.addEventListener("ended", drawPause, false);
};

const cleanup = () => {
  video.removeEventListener("loadeddata", draw);
  video.removeEventListener("seeked", draw);
  video.removeEventListener("play", drawLoop);
  video.removeEventListener("pause", drawPause);
  video.removeEventListener("ended", drawPause);
};

window.addEventListener("load", init);
window.addEventListener("unload", cleanup);

Let’s check out what we’ve achieved so far with the variables, functions, and event listeners we have configured.

Creating A Reusable Class

Let’s make this code reusable by converting it to an ES6 class so that we can create a new instance for any <video> and <canvas> pairing.

class VideoWithBackground {
  video;
  canvas;
  step;
  ctx;

  constructor(videoId, canvasId) {
    this.video = document.getElementById(videoId);
    this.canvas = document.getElementById(canvasId);

    window.addEventListener("load", this.init, false);
    window.addEventListener("unload", this.cleanup, false);
  }

  draw = () => {
    this.ctx.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
  };

  drawLoop = () => {
    this.draw();
    this.step = window.requestAnimationFrame(this.drawLoop);
  };

  drawPause = () => {
    window.cancelAnimationFrame(this.step);
    this.step = undefined;
  };

  init = () => {
    this.ctx = this.canvas.getContext("2d");
    this.ctx.filter = "blur(1px)";

    this.video.addEventListener("loadeddata", this.draw, false);
    this.video.addEventListener("seeked", this.draw, false);
    this.video.addEventListener("play", this.drawLoop, false);
    this.video.addEventListener("pause", this.drawPause, false);
    this.video.addEventListener("ended", this.drawPause, false);
  };

  cleanup = () => {
    this.video.removeEventListener("loadeddata", this.draw);
    this.video.removeEventListener("seeked", this.draw);
    this.video.removeEventListener("play", this.drawLoop);
    this.video.removeEventListener("pause", this.drawPause);
    this.video.removeEventListener("ended", this.drawPause);
  };
    }

Now, we can create a new instance by passing the id values for the <video> and <canvas> elements into a VideoWithBackground() class:

const el = new VideoWithBackground("js-video", "js-canvas");
Respecting User Preferences

Earlier, we briefly discussed that we would need to disable or minimize the effect’s motion for users who prefer reduced motion. We have to consider that for decorative flourishes like this.

The easy way out? We can detect the user’s motion preferences with the prefers-reduced-motion media query and completely hide the decorative canvas if reduced motion is the preference.

@media (prefers-reduced-motion: reduce) {
  .canvas {
    display: none !important;
  }
}

Another way we respect reduced motion preferences is to use JavaScript’s matchMedia function to detect the user’s preference and prevent the necessary event listeners from registering.

constructor(videoId, canvasId) {
  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");

  if (!mediaQuery.matches) {
    this.video = document.getElementById(videoId);
    this.canvas = document.getElementById(canvasId);

    window.addEventListener("load", this.init, false);
    window.addEventListener("unload", this.cleanup, false);
  }
}
Final Demo

We’ve created a reusable ES6 class that we can use to create new instances. Feel free to check out and play around with the completed demo.

See the Pen Youtube video glow effect - dominant color [forked] by Adrian Bece.

Creating A React Component

Let’s migrate this code to the React library, as there are key differences in the implementation that are worth knowing if you plan on using this effect in a React project.

Creating A Custom Hook

Let’s start by creating a custom React hook. Instead of using the getElementById function for selecting DOM elements, we can access them with a ref on the useRef hook and assign it to the <canvas> and <video> elements.

We’ll also reach for the useEffect hook to initialize and clear the event listeners to ensure they only run once all of the necessary elements have mounted.

Our custom hook must return the ref values we need to attach to the <canvas> and <video> elements, respectively.

import { useRef, useEffect } from "react";

export const useVideoBackground = () => {
  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
  const canvasRef = useRef();
  const videoRef = useRef();

  const init = () => {
    const video = videoRef.current;
    const canvas = canvasRef.current;
    let step;

    if (mediaQuery.matches) {
      return;
    }

    const ctx = canvas.getContext("2d");

    ctx.filter = "blur(1px)";

    const draw = () => {
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
    };

    const drawLoop = () => {
      draw();
      step = window.requestAnimationFrame(drawLoop);
    };

    const drawPause = () => {
      window.cancelAnimationFrame(step);
      step = undefined;
    };

    // Initialize
    video.addEventListener("loadeddata", draw, false);
    video.addEventListener("seeked", draw, false);
    video.addEventListener("play", drawLoop, false);
    video.addEventListener("pause", drawPause, false);
    video.addEventListener("ended", drawPause, false);

    // Run cleanup on unmount event
    return () => {
      video.removeEventListener("loadeddata", draw);
      video.removeEventListener("seeked", draw);
      video.removeEventListener("play", drawLoop);
      video.removeEventListener("pause", drawPause);
      video.removeEventListener("ended", drawPause);
    };
  };

  useEffect(init, []);

  return {
    canvasRef,
    videoRef,
  };
};

Defining The Component

We’ll use similar markup for the actual component, then call our custom hook and attach the ref values to their respective elements. We’ll make the component configurable so we can pass any <video> element attribute as a prop, like src, for example.

import React from "react";
import { useVideoBackground } from "../hooks/useVideoBackground";

import "./VideoWithBackground.css";

export const VideoWithBackground = (props) => {
  const { videoRef, canvasRef } = useVideoBackground();

  return (
    <section className="wrapper">
      <video ref={ videoRef } controls className="video" { ...props } />
      <canvas width="10" height="6" aria-hidden="true" className="canvas" ref={ canvasRef } />
    </section>
  );
};

All that’s left to do is to call the component and pass the video URL to it as a prop.

import { VideoWithBackground } from "../components/VideoWithBackground";

function App() {
  return (
    <VideoWithBackground src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" />
  );
}

export default App;
Conclusion

We combined the HTML <canvas> element and the corresponding Canvas API with JavaScript’s requestAnimationFrame method to create the same charming — but performance-intensive — visual effect that makes YouTube’s Ambient Mode feature. We found a way to draw the current <video> frame on the <canvas>, keep the two elements in sync, and position them so that the blurred <canvas> sits properly behind the <video>.

We covered a few other considerations in the process. For example, we established the <canvas> as a decorative image that can be removed or hidden when a user’s system is set to a reduced motion preference. Further, we considered the maintainability of our work by establishing it as a reusable ES6 class that can be used to add more instances on a page. Lastly, we converted the effect into a component that can be used in a React project.

Feel free to play around with the finished demo. I encourage you to continue building on top of it and share your results with me in the comments, or, similarly, you can reach out to me on Twitter. I’d love to hear your thoughts and see what you can make out of it!

References

Useful DevTools Tips and Tricks

When it comes to browser DevTools, we all have our own preferences and personal workflows, and we pride ourselves in knowing that “one little trick” that makes our debugging lives easier.

But also — and I know this from having worked on DevTools at Mozilla and Microsoft for the past ten years — most people tend to use the same three or four DevTools features, leaving the rest unused. This is unfortunate as there are dozens of panels and hundreds of features available in DevTools across all browsers, and even the less popular ones can be quite useful when you need them.

As it turns out, I’ve maintained the DevTools Tips website for the past two years now. More and more tips get added over time, and traffic keeps growing. I recently started tracking the most popular tips that people are accessing on the site, and I thought it would be interesting to share some of this data with you!

So, here are the top 15 most popular DevTools tips from the website.

If there are other tips that you love and that make you more productive, consider sharing them with our community in the comments section!

Let’s count down, starting with…

15: Zoom DevTools

If you’re like me, you may find the text and buttons in DevTools too small to use comfortably. I know I’m not alone here, judging by the number of people who ask our team how to make them bigger!

Well, it turns out you can actually zoom into the DevTools UI.

DevTools’ user interface is built with HTML, CSS, and JavaScript, which means that it’s rendered as web content by the browser. And just like any other web content in browsers, it can be zoomed in or out by using the Ctrl+ and Ctrl- keyboard shortcuts (or Cmd+ and Cmd- on macOS).

So, if you find the text in DevTools too small to read, click anywhere in DevTools to make sure the focus is there, and then press Ctrl+ (or Cmd+ on macOS).

Chromium-based browsers such as Chrome, Edge, Brave, or Opera can also display the font used by an element that contains the text:

  • Select an element that only contains text children.
  • Open the Computed tab in the sidebar of the Elements tool.
  • Scroll down to the bottom of the tab.
  • The rendered fonts are displayed.

Note: To learn more, see “List the fonts used on a page or an element.”

12: Measure Arbitrary Distances On A Page

Sometimes it can be useful to quickly measure the size of an area on a webpage or the distance between two things. You can, of course, use DevTools to get the size of any given element. But sometimes, you need to measure an arbitrary distance that may not match any element on the page.

When this happens, one nice way is to use Firefox’s measurement tool:

  1. If you haven’t done so already, enable the tool. This only needs to be done once: Open DevTools, go into the Settings panel by pressing F1 and, in the Available Toolbox Buttons, check the Measure a portion of the page option.
  2. Now, on any page, click the new Measure a portion of the page icon in the toolbar.
  3. Click and drag with the mouse to measure distances and areas.

Note: To learn more, see “Measure arbitrary distances in the page.”

11: Detect Unused Code

One way to make a webpage appear fast to your users is to make sure it only loads the JavaScript and CSS dependencies it truly needs.

This may seem obvious, but today’s complex web apps often load huge bundles of code, even when only a small portion is needed to render the first page.

In Chromium-based browsers, you can use the Coverage tool to identify which parts of your code are unused. Here is how:

  1. Open the Coverage tool. You can use the Command Menu as a shortcut: press Ctrl+Shift+P (or Cmd+Shift+P on macOS), type “coverage” and then press Enter.)
  2. Click Start instrumenting coverage and refresh the page.
  3. Wait for the page to reload and for the coverage report to appear.
  4. Click any of the reported files to open them in the Sources tool.

The file appears in the tool along with blue and red bars that indicate whether a line of code is used or unused, respectively.

Note: To learn more, see “Detect unused CSS and JavaScript code.”

10: Change The Playback Rate Of A Video

Usually, when a video appears on a webpage, the video player that displays it also provides buttons to control its playback, including a way to speed it up or slow it down. But that’s not always the case.

In cases when the webpage makes it difficult or impossible to control a video, you can use DevTools to control it via JavaScript istead.

  1. Open DevTools.
  2. Select the <video> element in the Elements tool (called Inspector in Firefox).
  3. Open the Console tool.
  4. Type the following: $0.playbackRate = 2; and press Enter.

The $0 expression is a shortcut that refers to whatever element is currently selected in DevTools; in this case, it refers to the <video> HTML element.

By using the playbackRate property of the <video> element, you can speed up or slow down the video. Note that you could also use any of the other <video> element properties or methods, such as:

  • $0.pause() to pause the video;
  • $0.play() to resume playing the video;
  • $0.loop = true to repeat the video in a loop.

Note: To learn more, see “Speed up or slow down a video.”

9: Use DevTools In Another Language

If, like me, English isn’t your primary language, using DevTools in English might make things harder for you.

If that’s your case, know that you can actually use a translated version of DevTools that either matches your operating system, your browser, or a language of your choice.

The procedure differs per browser.

In Safari, both the browser and Web Inspector (which is what DevTools is called in Safari) inherit the language of the operating system. So if you want to use a different language for DevTools, you’ll need to set it globally by going into System preferencesLanguage & RegionApps.

In Firefox, DevTools always matches the language of the browser. So, if you want to use DevTools in, say, French, then download Firefox in French.

Finally, in Chrome or Edge, you can choose to either match the language of the browser or set a different language just for DevTools.

To make your choice:

  1. Open DevTools and press F1 to open the Settings.
  2. In the Language drop-down, choose either Browser UI language to match the browser language or choose another language from the list.

Note: To learn more, see “Use DevTools in another language.”

8: Disable Event Listeners

Event listeners can sometimes get in the way of debugging a webpage. If you’re investigating a particular issue, but every time you move your mouse or use the keyboard, unrelated event listeners are triggered, this could make it harder to focus on your task.

A simple way to disable an event listener is by selecting the element it applies to in the Elements tool (or Inspector in Firefox). Once you’ve found and selected the element, do either of the following:

  • In Firefox, click the event badge next to the element, and in the popup that appears, uncheck the listeners you want to disable.
  • In Chrome or Edge, click the Event Listeners tab in the sidebar panel, find the listener you want to remove, and click Remove.

Note: To learn more, see “Remove or disable event listeners.”

7: View Console Logs On Non-Safari Browsers On iOS

As you might know, Safari isn’t the only browser you can install and use on an iOS device. Firefox, Chrome, Edge, and others can also be used. Technically, they all run on the same underlying browser rendering engine, WebKit, so a website should more or less look the same in all of these browsers in iOS.

However, it’s possible to have bugs on other browsers that don’t replicate in Safari. This can be quite tricky to investigate. While it’s possible to debug Safari on an iOS device by attaching the device to a Mac with a USB cable, it’s impossible to debug non-Safari browsers.

Thankfully, there is a way to at least see your console logs in Chrome and Edge (and possibly other Chromium-based browsers) when using iOS:

  1. Open Chrome or Edge on your iOS device and go to the special about:inspect page.
  2. Click Start Logging.
  3. Keep this tab open and then open another one.
  4. In the new tab, go to the page you’re trying to debug.
  5. Return to the previous tab. Your console logs should now be displayed.

Note: To learn more, see “View console logs from non-Safari browsers on an iPhone.”

6: Copy Element Styles

Sometimes it’s useful to extract a single element from a webpage, maybe to test it in isolation. To do this, you’ll first need to extract the element’s HTML code via the Elements tool by right-clicking the element and choosing CopyCopy outer HTML.

Extracting the element’s styles, however, is a bit more difficult as it involves going over all of the CSS rules that apply to the element.

Chrome, Edge, and other Chromium-based browsers make this step a lot faster:

  1. In the Elements tool, select the element you want to copy styles from.
  2. Right-click the selected element.
  3. Click CopyCopy styles.
  4. Paste the result in your text editor.

You now have all the styles that apply to this element, including inherited styles and custom properties, in a single list.

Note: To learn more, see “Copy an element’s styles.”

5: Download All Images On The Page

This nice tip isn’t specific to any browser and can be run anywhere as long as you can execute JavaScript. If you want to download all of the images that are on a webpage, open the Console tool, paste the following code, and press Enter:

$$('img').forEach(async (img) => {
 try {
   const src = img.src;
   // Fetch the image as a blob.
   const fetchResponse = await fetch(src);
   const blob = await fetchResponse.blob();
   const mimeType = blob.type;
   // Figure out a name for it from the src and the mime-type.
   const start = src.lastIndexOf('/') + 1;
   const end = src.indexOf('.', start);
   let name = src.substring(start, end === -1 ? undefined : end);
   name = name.replace(/[^a-zA-Z0-9]+/g, '-');
   name += '.' + mimeType.substring(mimeType.lastIndexOf('/') + 1);
   // Download the blob using a <a> element.
   const a = document.createElement('a');
   a.setAttribute('href', URL.createObjectURL(blob));
   a.setAttribute('download', name);
   a.click();
 } catch (e) {}
});

Note that this might not always succeed: the CSP policies in place on the web page may cause some of the images to fail to download.

If you happen to use this technique often, you might want to turn this into a reusable snippet of code by pasting it into the Snippets panel, which can be found in the left sidebar of the Sources tool in Chromium-based browsers.

In Firefox, you can also press Ctrl+I on any webpage to open Page Info, then go to Media and select Save As to download all the images.

Note: To learn more, see “Download all images from the page.”

4: Visualize A Page In 3D

The HTML and CSS code we write to create webpages gets parsed, interpreted, and transformed by the browser, which turns it into various tree-like data structures like the DOM, compositing layers, or the stacking context tree.

While these data structures are mostly internal in-memory representations of a running webpage, it can sometimes be helpful to explore them and make sure things work as intended.

A three-dimensional representation of these structures can help see things in a way that other representations can’t. Plus, let’s admit it, it’s cool!

Edge is the only browser that provides a tool dedicated to visualizing webpages in 3D in a variety of ways.

  1. The easiest way to open it is by using the Command Menu. Press Ctrl+Shift+P (or Cmd+Shift+P on macOS), type “3D” and then press Enter.
  2. In the 3D View tool, choose between the three different modes: Z-Index, DOM, and Composited Layers.
  3. Use your mouse cursor to pan, rotate, or zoom the 3D scene.

The Z-Index mode can be helpful to know which elements are stacking contexts and which are positioned on the z-axis.

The DOM mode can be used to easily see how deep your DOM tree is or find elements that are outside of the viewport.

The Composited Layers mode shows all the different layers the browser rendering engine creates to paint the page as quickly as possible.

Consider that Safari and Chrome also have a Layers tool that shows composited layers.

Note: To learn more, see “See the page in 3D.”

3: Disable Abusive Debugger Statements

Some websites aren’t very nice to us web developers. While they seem normal at first, as soon as you open DevTools, they immediately get stuck and pause at a JavaScript breakpoint, making it very hard to inspect the page!

These websites achieve this by adding a debugger statement in their code. This statement has no effect as long as DevTools is closed, but as soon as you open it, DevTools pauses the website’s main thread.

If you ever find yourself in this situation, here is a way to get around it:

  1. Open the Sources tool (called Debugger in Firefox).
  2. Find the line where the debugger statement is. That shouldn’t be hard since the debugger is currently paused there, so it should be visible right away.
  3. Right-click on the line number next to this line.
  4. In the context menu, choose Never pause here.
  5. Refresh the page.

Note: To learn more, see “Disable abusive debugger statements that prevent inspecting websites.”

2: Edit And Resend Network Requests

When working on your server-side logic or API, it may be useful to send a request over and over again without having to reload the entire client-side webpage and interact with it each time. Sometimes you just need to tweak a couple of request parameters to test something.

One of the easiest ways to do this is by using Edge’s Network Console tool or Firefox’s Edit and Resend feature of the Network tool. Both of them allow you to start from an existing request, modify it, and resend it.

In Firefox:

  • Open the Network tool.
  • Right-click the network request you want to edit and then click Edit and Resend.
  • A new sidebar panel opens up, which lets you change things like the URL, the method, the request parameters, and even the body.
  • Change anything you need and click Send.

In Edge:

  • First, enable the Network Console tool by going into the Settings panel (press F1) → ExperimentsEnable Network Console.
  • Then, in the Network tool, find the request you want to edit, right-click it and then click Edit and Resend.
  • The Network Console tool appears, which lets you change the request just like in Firefox.
  • Make the changes you need, and then click Send.

Here is what the feature looks like in Firefox:

Note: To learn more, see “Edit and resend faulty network requests to debug them.”

If you need to resend a request without editing it first, you can do so too. (See: Replay a XHR request)

And the honor of being the Number One most popular DevTools tip in this roundup goes to… 🥁

1: Simulate Devices

This is, by far, the most widely viewed DevTools tip on my website. I’m not sure why exactly, but I have theories:

  • Cross-browser and cross-device testing remain, to this day, one of the most important pain points that web developers face, and it’s nice to be able to simulate other devices from the comfort of your development browser.
  • People might be using it to achieve non-dev tasks. For example, people use it to post photos on Instagram from their laptops or desktop computers!

It’s important to realize, though, that DevTools can’t simulate what your website will look like on another device. Underneath it, it is all still the same browser rendering engine. So, for example, when you simulate an iPhone by using Firefox’s Responsive Design Mode, the page still gets rendered by Firefox’s rendering engine, Gecko, rather than Safari’s rendering engine, WebKit.

Always test on actual browsers and actual devices if you don’t want your users to stumble upon bugs you could have caught.

That being said,

Simulating devices in DevTools is very useful for testing how a layout works at different screen sizes and device pixel ratios. You can even use it to simulate touch inputs and other user agent strings.

Here are the easiest ways to simulate devices per browser:

  • In Safari, press Ctrl+Cmd+R, or click Develop in the menu bar and then click Enter Responsive Design Mode.
  • In Firefox, press Ctrl+Shift+M (or Cmd+Shift+M), or use the browser menu → More toolsResponsive design mode.
  • In Chrome or Edge, open DevTools first, then press Ctrl+Shift+M (or Cmd+Shift+M), or click the Device Toolbar icon.

Here is how simulating devices looks in Safari:

Note: To learn more, see “Simulate different devices and screen sizes.”

Finally, if you find yourself simulating screen sizes often, you might be interested in using Polypane. Polypane is a great development browser that lets you simulate multiple synchronized viewports at the same time, so you can see how your website renders at different sizes at the same time.

Polypane comes with its own set of unique features, which you can also find on DevTools Tips.

Conclusion

I’m hoping you can see now that DevTools is very versatile and can be used to achieve as many tasks as your imagination allows. Whatever your debugging use case is, there’s probably a tool that’s right for the job. And if there isn’t, you may be able to find out what you need to know by running JavaScript in the Console!

If you’ve discovered cool little tips that come in handy in specific situations, please share them in the comments section, as they may be very useful to others too.

Further Reading on Smashing Magazine

5 Chrome Extensions Every Web Designer Should Try

Web designers are continually on the lookout for tools that improve their workflow and productivity. For that reason, we’re highlighting five essential Chrome extensions, covering various aspects such as website analysis, performance optimization, and accessibility. Let’s dive in.

Your Web Designer Toolbox

Unlimited Downloads: 500,000+ Web Templates, Icon Sets, Themes & Design Assets Starting at only $16.50/month!

Wappalyzer

Wappalyzer is an indispensable extension that identifies the technologies used on any website. With just a click, you can get detailed insights into the frameworks, libraries, content management systems, and more, providing valuable context when troubleshooting or researching new projects.

Lighthouse

Developed by Google, Lighthouse is a powerful tool for checking a website’s performance, accessibility, and SEO. With this extension, you can quickly generate reports that provide actionable recommendations to improve your site’s overall quality and user experience, ensuring that your project adheres to best practices.

Web Developer

The Web Developer extension equips your browser with a plethora of web design-related tools. It offers various features including DOM manipulation and CSS inspection to form control and responsive design testing. While it may seem more oriented towards developers, as a designer, understanding and using these features can facilitate a healthy collaboration with the development team.

CSSViewer

CSSViewer is a simple yet handy Chrome extension that allows you to inspect the CSS properties of any page element. By hovering over the desired element, you can instantly view its dimensions, fonts, colors, and other CSS properties, making it easier to debug and refine your designs.

Axe

The Axe extension assists in auditing your website for accessibility issues and offers practical guidance on addressing them. This tool is designed to eliminate false positive results, saving you time by focusing on genuine issues.

Bonus Pro Tip

For an additional productivity boost, consider using a Chrome extension like Tab Wrangler to automatically manage and close inactive tabs, reducing clutter and freeing up valuable system resources during your development sessions.

Exploring The Potential Of Web Workers For Multithreading On The Web

Web Workers are a powerful feature of modern web development and were introduced as part of the HTML5 specification in 2009. They were designed to provide a way to execute JavaScript code in the background, separate from the main execution thread of a web page, in order to improve performance and responsiveness.

The main thread is the single execution context that is responsible for rendering the UI, executing JavaScript code, and handling user interactions. In other words, JavaScript is “single-threaded”. This means that any time-consuming task, such as complex calculations or data processing that is executed, would block the main thread and cause the UI to freeze and become unresponsive.

This is where Web Workers come in.

Web Workers were implemented as a way to address this problem by allowing time-consuming tasks to be executed in a separate thread, called a worker thread. This enabled JavaScript code to be executed in the background without blocking the main thread and causing the page to become unresponsive.

Creating a web worker in JavaScript is not much of a complicated task. The following steps provide a starting point for integrating a web worker into your application:

  1. Create a new JavaScript file that contains the code you want to run in the worker thread. This file should not contain any references to the DOM, as it will not have access to it.
  2. In your main JavaScript file, create a new worker object using the Worker constructor. This constructor takes a single argument, which is the URL of the JavaScript file you created in step 1.
    const worker = new Worker('worker.js');
    
  3. Add event listeners to the worker object to handle messages sent between the main thread and the worker thread. The onmessage event handler is used to handle messages sent from the worker thread, while the postMessage method is used to send messages to the worker thread.
    worker.onmessage = function(event) {
      console.log('Worker said: ' + event.data);
    };
    worker.postMessage('Hello, worker!');
    
  4. In your worker JavaScript file, add an event listener to handle messages sent from the main thread using the onmessage property of the self object. You can access the data sent with the message using the event.data property.
    self.onmessage = function(event) {
      console.log('Main thread said: ' + event.data);
      self.postMessage('Hello, main thread!');
    };
    

Now let’s run the web application and test the worker. We should see messages printed to the console indicating that messages were sent and received between the main thread and the worker thread.

One key difference between Web Workers and the main thread is that Web Workers have no access to the DOM or the UI. This means that they cannot directly manipulate the HTML elements on the page or interact with the user.

Web Workers are designed to perform tasks that do not require direct access to the UI, such as data processing, image manipulation, or calculations.

Another important difference is that Web Workers are designed to run in a sandboxed environment, separate from the main thread, which means that they have limited access to system resources and cannot access certain APIs, such as the localStorage or sessionStorage APIs. However, they can communicate with the main thread through a messaging system, allowing data to be exchanged between the two threads.

Importance And Benefits Of Web Workers For Multithreading On The Web

Web Workers provide a way for web developers to achieve multithreading on the web, which is crucial for building high-performance web applications. By enabling time-consuming tasks to be executed in the background, separate from the main thread, Web Workers improve the overall responsiveness of web pages and allow for a more seamless user experience. The following are some of the importance and benefits of Web Workers for multithreading on the Web.

Improved Resource Utilization

By allowing time-consuming tasks to be executed in the background, Web Workers make more efficient use of system resources, enabling faster and more efficient processing of data and improving overall performance. This is especially important for web applications that involve large amounts of data processing or image manipulation, as Web Workers can perform these tasks without impacting the user interface.

Increased Stability And Reliability

By isolating time-consuming tasks in separate worker threads, Web Workers help to prevent crashes and errors that can occur when executing large amounts of code on the main thread. This makes it easier for developers to write stable and reliable web applications, reducing the likelihood of user frustration or loss of data.

Enhanced Security

Web Workers run in a sandboxed environment that is separate from the main thread, which helps to enhance the security of web applications. This isolation prevents malicious code from accessing or modifying data in the main thread or other Web Workers, reducing the risk of data breaches or other security vulnerabilities.

Better Resource Utilization

Web Workers can help to improve resource utilization by freeing up the main thread to handle user input and other tasks while the Web Workers handle time-consuming computations in the background. This can help to improve overall system performance and reduce the likelihood of crashes or errors. Additionally, by leveraging multiple CPU cores, Web Workers can make more efficient use of system resources, enabling faster and more efficient processing of data.

Web Workers also enable better load balancing and scaling of web applications. By allowing tasks to be executed in parallel across multiple worker threads, Web Workers can help distribute the workload evenly across multiple cores or processors, enabling faster and more efficient processing of data. This is particularly important for web applications that experience high traffic or demand, as Web Workers can help to ensure that the application can handle an increased load without impacting performance.

Practical Applications Of Web Workers

Let us explore some of the most common and useful applications of Web Workers. Whether you’re building a complex web application or a simple website, understanding how to leverage Web Workers can help you improve performance and provide a better user experience.

Offloading CPU-Intensive Work

Suppose we have a web application that needs to perform a large, CPU-intensive computation. If we perform this computation in the main thread, the user interface will become unresponsive, and the user experience will suffer. To avoid this, we can use a Web Worker to perform the computation in the background.

// Create a new Web Worker.
const worker = new Worker('worker.js');

// Define a function to handle messages from the worker.
worker.onmessage = function(event) {
  const result = event.data;
  console.log(result);
};

// Send a message to the worker to start the computation.
worker.postMessage({ num: 1000000 });

// In worker.js:

// Define a function to perform the computation.
function compute(num) {
  let sum = 0;
  for (let i = 0; i < num; i++) {
    sum += i;
  }
  return sum;
}

// Define a function to handle messages from the main thread.
onmessage = function(event) {
  const num = event.data.num;
  const result = compute(num);
  postMessage(result);
};

In this example, we create a new Web Worker and define a function to handle messages from the worker. We then send a message to the worker with a parameter (num) that specifies the number of iterations to perform in the computation. The worker receives this message and performs the computation in the background. When the computation is complete, the worker sends a message back to the main thread with the result. The main thread receives this message and logs the result to the console.

This task involves adding up all the numbers from 0 to a given number. While this task is relatively simple and straightforward for small numbers, it can become computationally intensive for very large numbers.

In the example code we used above, we passed the number 1000000 to the compute() function in the Web Worker. This means that the compute function will need to add up all the numbers from 0 to one million. This involves a large number of additional operations and can take a significant amount of time to complete, especially if the code is running on a slower computer or in a browser tab that is already busy with other tasks.

By offloading this task to a Web Worker, the main thread of the application can continue to run smoothly without being blocked by the computationally intensive task. This allows the user interface to remain responsive and ensures that other tasks, such as user input or animations, can be handled without delay.

Handling Network Requests

Let us consider a scenario where a web application needs to initiate a significant number of network requests. Performing these requests within the main thread could cause the user interface to become unresponsive and result in a poor user experience. In order to prevent this issue, we can utilize Web Workers to handle these requests in the background. By doing so, the main thread remains free to execute other tasks while the Web Worker handles the network requests simultaneously, resulting in improved performance and a better user experience.

// Create a new Web Worker.
const worker = new Worker('worker.js');

// Define a function to handle messages from the worker.
worker.onmessage = function(event) {
  const response = event.data;
  console.log(response);
};

// Send a message to the worker to start the requests.
worker.postMessage({ urls: ['https://api.example.com/foo', 'https://api.example.com/bar'] });

// In worker.js:

// Define a function to handle network requests.
function request(url) {
  return fetch(url).then(response => response.json());
}

// Define a function to handle messages from the main thread.
onmessage = async function(event) {
  const urls = event.data.urls;
  const results = await Promise.all(urls.map(request));
  postMessage(results);
};

In this example, we create a new Web Worker and define a function to handle messages from the worker. We then send a message to the worker with an array of URLs to request. The worker receives this message and performs the requests in the background using the fetch API. When all requests are complete, the worker sends a message back to the main thread with the results. The main thread receives this message and logs the results to the console.

Parallel Processing

Suppose we have a web application that needs to perform a large number of independent computations. If we perform these computations in sequence in the main thread, the user interface will become unresponsive, and the user experience will suffer. To avoid this, we can use a Web Worker to perform the computations in parallel.

// Create a new Web Worker.
const worker = new Worker('worker.js');

// Define a function to handle messages from the worker.
worker.onmessage = function(event) {
  const result = event.data;
  console.log(result);
};

// Send a message to the worker to start the computations.
worker.postMessage({ nums: [1000000, 2000000, 3000000] });

// In worker.js:

// Define a function to perform a single computation.
function compute(num) {
  let sum = 0;
  for (let i = 0; i < num; i++) {
    sum += i;
}
  return sum;
}

// Define a function to handle messages from the main thread.
onmessage = function(event) {
  const nums = event.data.nums;
  const results = nums.map(compute);
  postMessage(results);
};

In this example, we create a new Web Worker and define a function to handle messages from the worker. We then send a message to the worker with an array of numbers to compute. The worker receives this message and performs the computations in parallel using the map method. When all computations are complete, the worker sends a message back to the main thread with the results. The main thread receives this message and logs the results to the console.

Limitations And Considerations

Web workers are a powerful tool for improving the performance and responsiveness of web applications, but they also have some limitations and considerations that you should keep in mind when using them. Here are some of the most important ones:

Browser Support

Web workers are supported in all major browsers, including Chrome, Firefox, Safari, and Edge. However, there are still some other browsers that do not support web workers or may have limited support.

For a more extensive look at browser support, see Can I Use.

It is important that you check out the browser support for any feature before using them in production code and test your application thoroughly to ensure compatibility.

Limited Access To The DOM

Web workers run in a separate thread and do not have access to the DOM or other global objects in the main thread. This means you cannot directly manipulate the DOM from a web worker or access global objects like windows or documents.

To work around this limitation, you can use the postMessage method to communicate with the main thread and update the DOM or access global objects indirectly. For example, you can send data to the main thread using postMessage and then update the DOM or global objects in response to the message.

Alternatively, there are some libraries that help solve this issue. For example, the WorkerDOM library enables you to run the DOM in a web worker, allowing for faster page rendering and improved performance.

Communication Overhead

Web workers communicate with the main thread using the postMessage method, and as a result, could introduce communication overhead, which refers to the amount of time and resources required to establish and maintain communication between two or more computing systems, such as between a Web Worker and the main thread in a web application. This could result in a delay in processing messages and potentially slow down the application. To minimize this overhead, you should only send essential data between threads and avoid sending large amounts of data or frequent messages.

Limited Debugging Tools

Debugging Web Workers can be more challenging than debugging code in the main thread, as there are fewer debugging tools available. To make debugging easier, you can use the console API to log messages from the worker thread and use browser developer tools to inspect messages sent between threads.

Code Complexity

Using Web Workers can increase the complexity of your code, as you need to manage communication between threads and ensure that data is passed correctly. This can make it more difficult to write, debug, and maintain your code, so you should carefully consider whether using web workers is necessary for your application.

Strategies For Mitigating Potential Issues With Web Workers

Web Workers are a powerful tool for improving the performance and responsiveness of web applications. However, when using Web Workers, there are several potential issues that can arise. Here are some strategies for mitigating these issues:

Minimize Communication Overhead With Message Batching

Message batching involves grouping multiple messages into a single batch message, which can be more efficient than sending individual messages separately. This approach reduces the number of round-trips between the main thread and Web Workers. It can help to minimize communication overhead and improve the overall performance of your web application.

To implement message batching, you can use a queue to accumulate messages and send them together as a batch when the queue reaches a certain threshold or after a set period of time. Here’s an example of how you can implement message batching in your Web Worker:

// Create a message queue to accumulate messages.
const messageQueue = [];

// Create a function to add messages to the queue.
function addToQueue(message) {
  messageQueue.push(message);

  // Check if the queue has reached the threshold size.
  if (messageQueue.length >= 10) {
    // If so, send the batched messages to the main thread.
    postMessage(messageQueue);

    // Clear the message queue.
    messageQueue.length = 0;
  }
}

// Add a message to the queue.
addToQueue({type: 'log', message: 'Hello, world!'});

// Add another message to the queue.
addToQueue({type: 'error', message: 'An error occurred.'});

In this example, we create a message queue to accumulate messages that need to be sent to the main thread. Whenever a message is added to the queue using the addToQueue function, we check if the queue has reached the threshold size (in this case, ten messages). If so, we send the batched messages to the main thread using the postMessage method. Finally, we clear the message queue to prepare it for the next batch.

By batching messages in this way, we can reduce the overall number of messages sent between the main thread and Web Workers,

Avoid Synchronous Methods

These are JavaScript functions or operations that block the execution of other code until they are complete. Synchronous methods can block the main thread and cause your application to become unresponsive. To avoid this, you should avoid using synchronous methods in your Web Worker code. Instead, use asynchronous methods such as setTimeout() or etInterval() to perform long-running computations.

Here is a little demonstration:

// In the worker
self.addEventListener('message', (event) => {
  if (event.data.action === 'start') {
    // Use a setTimeout to perform some computation asynchronously.
    setTimeout(() => {
      const result = doSomeComputation(event.data.data);

      // Send the result back to the main thread.
      self.postMessage({ action: 'result', data: result });
    }, 0);
  }
});

Be Mindful Of Memory Usage

Web Workers have their own memory space, which can be limited depending on the user’s device and browser settings. To avoid memory issues, you should be mindful of the amount of memory your Web Worker code is using and avoid creating large objects unnecessarily. For example:

// In the worker
self.addEventListener('message', (event) => {
  if (event.data.action === 'start') {
    // Use a for loop to process an array of data.
    const data = event.data.data;
    const result = [];

    for (let i = 0; i < data.length; i++) {
      // Process each item in the array and add the result to the result array.
      const itemResult = processItem(data[i]);
      result.push(itemResult);
    }

    // Send the result back to the main thread.
    self.postMessage({ action: 'result', data: result });
  }
});

In this code, the Web Worker processes an array of data and returns the result to the main thread using the postMessage method. However, the for loop used to process the data may be time-consuming.

The reason for this is that the code is processing an entire array of data at once, meaning that all the data must be loaded into memory at the same time. If the data set is very large, this can cause the Web Worker to consume a significant amount of memory, potentially exceeding the memory limit allocated to the Web Worker by the browser.

To mitigate this issue, you can consider using built-in JavaScript methods like forEach or reduce, which can process data one item at a time and avoid the need to load the entire array into memory at once.

Browser Compatibility

Web Workers are supported in most modern browsers, but some older browsers may not support them. To ensure compatibility with a wide range of browsers, you should test your Web Worker code in different browsers and versions. You can also use feature detection to check if Web Workers are supported before using them in your code, like this:

if (typeof Worker !== 'undefined') {
  // Web Workers are supported.
  const worker = new Worker('worker.js');
} else {
  // Web Workers are not supported.
  console.log('Web Workers are not supported in this browser.');
}

This code checks if Web Workers are supported in the current browser and creates a new Web Worker if they are supported. If Web Workers are not supported, the code logs a message to the console indicating that Web Workers are not supported in the browser.

By following these strategies, you can ensure that your Web Worker code is efficient, responsive, and compatible with a wide range of browsers.

Conclusion

As web applications become increasingly complex and demanding, the importance of efficient multithreading techniques — such as Web Workers — is likely to increase. Web Workers are an essential feature of modern web development that allows developers to offload CPU-intensive tasks to separate threads, improving application performance and responsiveness. However, there are significant limitations and considerations to keep in mind when working with Web Workers, such as the lack of access to the DOM and limitations on the types of data that can be passed between threads.

To mitigate these potential issues, developers can follow strategies as mentioned earlier, such as using asynchronous methods and being mindful of the complexity of the task being offloaded.

Multithreading with Web Workers is likely to remain an important technique for improving web application performance and responsiveness in the future. While there are other techniques for achieving multithreading in JavaScript, such as using WebSockets or SharedArrayBuffer, Web Workers have several advantages that make them a powerful tool for developers.

Adopting more recent technology such as WebAssembly may open up new opportunities for using Web Workers to offload even more complex and computationally-intensive tasks. Overall, Web Workers are likely to continue to evolve and improve in the coming years, helping developers create more efficient and responsive web applications.

Additionally, many libraries and tools exist to help developers work with Web Workers. For example, Comlink and Workerize provide a simplified API for communicating with Web Workers. These libraries abstract away some of the complexity of managing Web Workers, making it easier to leverage their benefits.

Hopefully, this article has given you a good understanding of the potential of web workers for multithreading and how to use them in your own code.

Building Complex Forms In Vue

More often than not, web engineers always have causes to build out forms, from simple to complex. It is also a familiar pain in the shoe for engineers how fast codebases get incredibly messy and incongruously lengthy when building large and complex forms. Thus begging the question, “How can this be optimized?”.

Consider a business scenario where we need to build a waitlist that captures the name and email. This scenario only requires two/three input fields, as the case may be, and could be added swiftly with little to no hassle. Now, let us consider a different business scenario where users need to fill out a form with ten input fields in 5 sections. Writing 50 input fields isn’t just a tiring job for the Engineer but also a waste of great technical time. More so, it goes against the infamous “Don’t Repeat Yourself” (DRY) principle.

In this article, we will focus on learning to use the Vue components, the v-model directive, and the Vue props to build complex forms in Vue.

The v-model Directive In Vue

Vue has several unique HTML attributes called directives, which are prefixed with the v-. These directives perform different functions, from rendering data in the DOM to manipulating data.

The v-model is one such directive, and it is responsible for two-way data binding between the form input value and the value stored in the data property. The v-model works with any input element, such as the input or the select elements. Under the hood, it combines the inputted input value and the corresponding change event listener like the following:

<!-- Input element -->
<input v-model="inputValue" type="text">

<!-- Select element -->
<select v-model="selectedValue">
  <option value="">Please select the right option</option>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>

The input event is used for the <input type= "text"> element. Likewise, for the <select> … </select>, <input type= "checkbox"> and <input type= "radio">, the v-model will, in turn, match the values to a change event.

Components In Vue

Reusability is one of the core principles of Software Engineering, emphasizing on using existing software features or assets in a software project for reasons ranging from minimizing development time to saving cost.

One of the ways we observe reusability in Vue is through the use of components. Vue components are reusable and modular interfaces with their own logic and custom content. Even though they can be nested within each other just as a regular HTML element, they can also work in isolation.

Vue components can be built in two ways as follows:

  • Without the build step,
  • With the build step.

Without The Build Step

Vue components can be created without using the Vue Command Line Interface (CLI). This component creation method defines a JavaScript object in a Vue instance options property. In the code block below, we inlined a JavaScript string that Vue parses on the fly.

template: `
  <p> Vue component without the build step </p>
  `

With The Build Step

Creating components using the build step involves using Vite — a blazingly fast, lightweight build tool. Using the build step to create a Vue component makes a Single File Component (SFC), as it can cater to the file’s logic, content, and styling.

<template>
  <p> Vue component with the build step </p>
</template>

In the above code, we have the <p> tag within the HTML <template> tag, which gets rendered when we use a build step for the application.

Registering Vue Components

Creating a Vue component is the first step of reusability and modularity in Vue. Next is the registration and actual usage of the created Vue component.

Vue components allow the nesting of components within components and, even more, the nesting of components within a global or parent component.

Let’s consider that we stored the component we created using the build step in a BuildStep.vue file. To make this component available for usage, we will import it into another Vue component or a .vue, such as the root entry file. After importing this component, we can then register the component name in the components option property, thus making the component available as an HTML tag. While this HTML tag will have a custom name, the Vue engine will parse them as valid HTML and render them successfully in the browser.

<!-- App.vue -->
<template>
  <div>
    <BuildStep />
  </div>
</template>

<script>
import BuildStep from './BuildStep.vue'

export default {
  components: {
    BuildStep
  }
}
</script>

From the above, we imported the BuildStep.vue component into the App.vue file, registered it in the components option property, and then declared it within our HTML template as <BuildStep />.

Vue Props

Vue props, otherwise known as properties, are custom-made attributes used on a component for passing data from the parent component to the child component(s). A case where props can come in handy is when we need a component with different content but a constant visual layout, considering a component can have as many props as possible.

The Vue prop has a one-way data flow, i.e., from the parent to the child component. Thus, the parent component owns the data, and the child component cannot modify the data. Instead, the child component can emit events that the parent component can record.

Props Declaration In Vue

Let us consider the code block below:

<template>
  <p> Vue component {{ buildType }} the build step</p>
</template>

<script>
export default {
  props: {
    buildType: {
      type: String
    }
  }
}
</script>

We updated the HTML template with the interpolated buildType, which will get executed and replaced with the value of the props that will be passed down from the parent component.

We also added a props tag in the props option property to listen to the props change and update the template accordingly. Within this props option property, we declared the name of the props, which matches what we have in the <template> tag, and also added the props type.

The props type, which can be Strings, Numbers, Arrays, Boolean, or Objects, acts as a rule or check to determine what our component will receive.

In the example above, we added a type of String; we will get an error if we try to pass in any other kind of value like a Boolean or Object.

Passing Props In Vue

To wrap this up, we will update the parent file, i.e., the App.vue, and pass the props accordingly.

<!-- App.vue -->
<template>
  <div>
    <BuildStep buildType="with"/>
  </div>
</template>

<script>
import BuildStep from './BuildStep.vue'

export default {
  components: {
    BuildStep
  }
}
</script>

Now, when the build step component gets rendered, we will see something like the following:

Vue component with the build step

With props, we needn’t create a new component from scratch to display whether a component has a build step or not. We can again declare the <BuildStep /> component and add the relevant build type.

<!-- App..vue -->
<template>
  <div>
    <BuildStep buildType="without"/>
  </div>
</template>

Likewise, just as for the build step, when the component gets rendered, we will have the following view:

Vue component without the build step
Event Handling In Vue

Vue has many directives, which include the v-on. The v-on is responsible for listening and handling DOM events to act when triggered. The v-on directive can also be written as the @ symbol to reduce verbosity.

<button @click="checkBuildType"> Check build type </button>

The button tag in the above code block has a click event attached to a checkBuildType method. When this button gets clicked, it facilitates executing a function that checks for the build type of the component.

Event Modifiers

The v-on directive has several event modifiers that add unique attributes to the v-on event handler. These event modifiers start with a dot and are found right after the event modifier name.

<form @submit.prevent="submitData">
 ...
<!-- This enables a form to be submitted while preventing the page from being reloaded. -->
</form>

Key Modifiers

Key modifiers help us listen to keyboard events, such as enter, and page-up on the fly. Key modifiers are bound to the v-on directive like v-on:eventname.keymodifiername, where the eventname could be keyup and the modifiername as enter.

<input @keyup.enter="checkInput">

The key modifiers also offer flexibility but allow multiple key name chaining.

<input @keyup.ctrl.enter="checkInput">

Here the key names will listen for both the ctrl and the enter keyboard events before the checkInput method gets called.

The v-for Directive

Just as JavaScript provides for iterating through arrays using loops like the for loop, Vue-js also provides a built-in directive known as the v-for that performs the same function.

We can write the v-for syntax as item in items where items are the array we are iterating over or as items of items to express the similarity with the JavaScript loop syntax.

List Rendering

Let us consider rendering the types of component build steps on a page.

<template>
  <div>
    <ul>
        <li v-for="steps in buildSteps" :key="steps.id"> {{ steps.step }}</li>
      </ul>
  </div>
</template>

<script>
export default {
 data() {
   return {
     buildSteps: [
      {
       id: "step 1",
       step:'With the build step',
      },
      {
        id: "step 2",
       step:'Without the build step'
      }
    ]
   }
 }
}
</script>

In the code block above, the steps array within the data property shows the two types of build steps we have for a component. Within our template, we used the v-for directive to loop through the steps array, the result of which we will render in an unordered list.

We added an optional key argument representing the index of the item we are currently iterating on. But beyond that, the key accepts a unique identifier that enables us to track each item’s node for proper state management.

Using v-for With A Component

Just like using the v-for to render lists, we can also use it to generate components. We can add the v-for directive to the component like the following:

<BuildStep v-for="steps in buildSteps" :key="steps.id"/>

The above code block will not do much for rendering or passing the step to the component. Instead, we will need to pass the value of the step as props to the component.

<BuildStep v-for="steps in buildSteps" :key="steps.id" :buildType="steps.step" />

We do the above to prevent any tight fixation of the v-for to the component.

The most important thing to note in the different usage of the v-for is the automation of a long process. We can move from manually listing out 100 items or components to using the v-for directive and have everything rendered out within the split of a second, as the case may be.

Building A Complex Registration Form In Vue

We will combine everything we have learned about the v-model, Vue components, the Vue props, the v-for directive, and event handling to build a complex form that would help us achieve efficiency, scalability, and time management.

This form will cater to capturing students’ bio-data, which we will develop to facilitate progressive enhancement as business demands increase.

Setting Up The Vue App

We will be scaffolding our Vue application using the build step. To do this, we will need to ensure we have the following installed:

Now we will proceed to create our Vue application by running the command below:

# npm
npm init vue@latest vue-complex-form

where vue-complex-form is the name of the Vue application.

After that, we will run the command below at the root of our Vue project:

npm install

Creating The JSON File To Host The Form Data

We aim to create a form where users can fill in their details. While we can manually add all the input fields, we will use a different approach to simplify our codebase. We will achieve this by creating a JSON file called util/bio-data.json. Within each of the JSON objects, we will have the basic info we want each input field to have.

[
  {
    "id": 1,
    "inputvalue":"  ",
    "formdata": "First Name",
    "type": "text",
    "inputdata": "firstname"
  },
  {
    "id": 2,
    "inputvalue":"  ",
    "formdata": "Last Name",
    "type": "text",
    "inputdata": "lastname"
  },
]

As seen in the code block above, we created an object with some keys already carrying values:

  • id acts as the primary identifier of the individual object;
  • inputvalue will cater to the value passed into the v-model;
  • formdata will handle the input placeholder and the labels name;
  • type denotes the input type, such as email, number, or text;
  • inputdata represents the input id and name.

These keys’ values will be passed in later to our component as props. We can access the complete JSON data here.

Creating The Reusable Component

We will create an input component that will get passed the props from the JSON file we created. This input component will get iterated on using a v-for directive to create numerous instances of the input field at a stretch without having to write it all out manually. To do this, we will create a components/TheInputTemplate.vue file and add the code below:

<template>
  <div>
    <label :for="inputData">{{ formData }}</label>
    <input
      :value= "modelValue"
      :type= "type"
      :id= "inputData"
      :name= "inputData"
      :placeholder= "formData"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  </div>
 </template>

<script>
export default {
  name: 'TheInputTemplate',
  props: {
    modelValue: {
      type: String
    },
    formData: {
      type: String
    },
    type: {
      type: String
    },
    inputData: {
      type: String
    }
  },
  emits: ['update:modelValue']
}
</script>
<style>
label {
  display: inline-block;
  margin-bottom: 0.5rem;
  text-transform: uppercase;
  color: rgb(61, 59, 59);
  font-weight: 700;
  font-size: 0.8rem;
}
input {
  display: block;
  width: 90%;
  padding: 0.5rem;
  margin: 0 auto 1.5rem auto;
}
</style>

In the above code block, we achieved the following:

  • We created a component with an input field.
  • Within the input field, we matched the values that we will pass in from the JSON file to the respective places of interest in the element.
  • We also created props of modelValue, formData, type, and inputData that will be registered on the component when exported. These props will be responsible for taking in data from the parent file and passing it down to the TheInputTemplate.vue component.
  • Bound the modelValue prop to the value of the input value.
  • Added the update:modelValue, which gets emitted when the input event is triggered.

Registering The Input Component

We will navigate to our App.vue file and import the TheInputTemplate.vue component from where we can proceed to use it.

<template>
  <form class="wrapper">
    <TheInputTemplate/>
  </form>
</template>
<script>
import TheInputTemplate from './components/TheInputTemplate.vue'
export default {
  name: 'App',
  components: {
    TheInputTemplate
  }
}
</script>
<style>
html, body{
  background-color: grey;
  height: 100%;
  min-height: 100vh;
}
.wrapper {
  background-color: white;
  width: 50%;
  border-radius: 3px;
  padding: 2rem  1.5rem;
  margin: 2rem auto;
}
</style>

Here we imported the TheInputTemplate.vue component into the App.vue file, registered it in the components option property, and then declared it within our HTML template.

If we run npm run serve, we should have the following view:

At this point, there is not much to see because we are yet to register the props on the component.

Passing Input Data

To get the result we are after, we will need to pass the input data and add the props to the component. To do this, we will update our App.vue file:

<template>
  <div class="wrapper">
    <div v-for="bioinfo in biodata" :key="bioinfo.id">
      <TheInputTemplate v-model="bioinfo.inputvalue":formData= "bioinfo.formdata":type= "bioinfo.type":inputData= "bioinfo.inputdata"/>
    </div>
  </div>
<script>
//add imports here
import biodata from "../util/bio-data.json";
export default {
  name: 'App',
 //component goes here
  data: () => ({
    biodata
  })
}
</script>

From the code block above, we achieved several things:

  • We imported the bio-data JSON file we created into the App.vue file. Then we added the imported variable to the data options of the Vue script.
  • Looped through the JSON data, which we instantiated in the data options using the Vue v-for directive.
  • Within the TheInputTemplate.vue component we created, we passed in the suitable data to fill the props option.

At this point, our interface should look like the following:

To confirm if our application is working as it should, we will open up our Vue DevTools, or install one from https://devtools.vuejs.org if we do not have it in our browser yet.

When we type in a value in any of the input fields, we can see the value show up in the modelValue within the Vue Devtools dashboard.

Conclusion

In this article, we explored some core Vue fundamentals like the v-for, v-model, and so on, which we later sewed together to build a complex form. The main goal of this article is to simplify the process of building complex forms while maintaining readability and reusability and reducing development time.

If, in any case, there will be a need to extend the form, all the developer would have to do is populate the JSON files with the needed information, and voila, the form is ready. Also, new Engineers can avoid swimming in lengthy lines of code to get an idea of what is going on in the codebase.

Note: To explore more about handling events within components to deal with as much complexity as possible, you can check out this article on using components with v-model.

Further Reading on Smashing Magazine

Understanding App Directory Architecture In Next.js

Since Next.js 13 release, there’s been some debate about how stable the shiny new features packed in the announcement are. On “What’s New in Next.js 13?” we have covered the release announced and established that though carrying some interesting experiments, Next.js 13 is definitely stable. And since then, most of us have seen a very clear landscape when it comes to the new <Link> and <Image> components, and even the (still beta) @next/font; these are all good to go, instant profit. Turbopack, as clearly stated in the announcement, is still alpha: aimed strictly for development builds and still heavily under development. Whether you can or can’t use it in your daily routine depends on your stack, as there are integrations and optimizations still somewhere on the way. This article’s scope is strictly about the main character of the announcement: the new App Directory architecture (AppDir, for short).

Because the App directory is the one that keeps bringing questions due to it being partnered with an important evolution in the React ecosystem — React Server Components — and with edge runtimes. It clearly is the shape of the future of our Next.js apps. It is experimental though, and its roadmap is not something we can consider will be done in the next few weeks. So, should you use it in production now? What advantages can you get out of it, and what are the pitfalls you may find yourself climbing out of? As always, the answer in software development is the same: it depends.

What Is The App Directory Anyway?

It is the new strategy for handling routes and rendering views in Next.js. It is made possible by a couple of different features tied together, and it is built to make the most out of React concurrent features (yes, we are talking about React Suspense). It brings, though, a big paradigm shift in how you think about components and pages in a Next.js app. This new way of building your app has a lot of very welcomed improvements to your architecture. Here’s a short, non-exhaustive list:

  • Partial Routing.
    • Route Groups.
    • Parallel Routes.
    • Intercepting Routes.
  • Server Components vs. Client Components.
  • Suspense Boundaries.
  • And much more, check the features overview in the new documentation.

A Quick Comparison

When it comes to the current routing and rendering architecture (in the Pages directory), developers were required to think of data fetching per route.

  • getServerSideProps: Server-Side Rendered;
  • getStaticProps: Server-Side Pre-Rendered and/or Incremental Static Regeneration;
  • getStaticPaths + getStaticProps: Server-Side Pre-Rendered or Static Site Generated.

Historically, it hadn’t yet been possible to choose the rendering strategy on a per-page basis. Most apps were either going full Server-Side Rendering or full Static Site Generation. Next.js created enough abstractions that made it a standard to think of routes individually within its architecture.

Once the app reaches the browser, hydration kicks in, and it’s possible to have routes collectively sharing data by wrapping our _app component in a React Context Provider. This gave us tools to hoist data to the top of our rendering tree and cascade it down toward the leaves of our app.

import { type AppProps } from 'next/app';

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
        <SomeProvider>
            <Component {...pageProps} />
        </SomeProvider>
}

The ability to render and organize required data per route made this approach an almost good tool for when data absolutely needed to be available globally in the app. And while this strategy will allow data to spread throughout the app, wrapping everything in a Context Provider bundles hydration to the root of your app. It is not possible anymore to render any branches on that tree (any route within that Provider context) on the server.

Here, enters the Layout Pattern. By creating wrappers around pages, we could opt in or out of rendering strategies per route again instead of doing it once with an app-wide decision. Read more on how to manage states in the Pages Directory on the article “State Management in Next.js” and on the Next.js documentation.

The Layout Pattern proved to be a great solution. Being able to granularly define rendering strategies is a very welcomed feature. So the App directory comes in to put the layout pattern front and center. As a first-class citizen of Next.js architecture, it enables enormous improvements in terms of performance, security, and data handling.

With React concurrent features, it’s now possible to stream components to the browser and let each one handle its own data. So rendering strategy is even more granular now — instead of page-wide, it’s component-based. Layouts are nested by default, which makes it more clear to the developer what impacts each page based on the file-system architecture. And on top of all that, it is mandatory to explicitly turn a component client-side (via the “use client” directive) in order to use a Context.

Building Blocks Of The App Directory

This architecture is built around the Layout Per Page Architecture. Now, there is no _app, neither is there a _document component. They have both been replaced by the root layout.jsx component. As you would expect, that’s a special layout that will wrap up your entire application.

export function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="en">
            <body>
                {children}
            </body>
        </html>
}

The root layout is our way to manipulate the HTML returned by the server to the entire app at once. It is a server component, and it does not render again upon navigation. This means any data or state in a layout will persist throughout the lifecycle of the app.

While the root layout is a special component for our entire app, we can also have root components for other building blocks:

  • loading.jsx: to define the Suspense Boundary of an entire route;
  • error.jsx: to define the Error Boundary of our entire route;
  • template.jsx: similar to the layout, but re-renders on every navigation. Especially useful to handle state between routes, such as in or out transitions.

All of those components and conventions are nested by default. This means that /about will be nested within the wrappers of / automatically.

Finally, we are also required to have a page.jsx for every route as it will define the main component to render for that URL segment (as known as the place you put your components!). These are obviously not nested by default and will only show in our DOM when there’s an exact match to the URL segment they correspond to.

There is much more to the architecture (and even more coming!), but this should be enough to get your mental model right before considering migrating from the Pages directory to the App directory in production. Make sure to check on the official upgrade guide as well.

Server Components In A Nutshell

React Server Components allow the app to leverage infrastructure towards better performance and overall user experience. For example, the immediate improvement is on bundle size since RSC won’t carry over their dependencies to the final bundle. Because they’re rendered in the server, any kind of parsing, formatting, or component library will remain on the server code. Secondly, thanks to their asynchronous nature, Server Components are streamed to the client. This allows the rendered HTML to be progressively enhanced on the browser.

So, Server Components lead to a more predictable, cacheable, and constant size of your final bundle breaking the linear correlation between app size and bundle size. This immediately puts RSC as a best practice versus traditional React components (which are now referred to as client components to ease disambiguation).

On Server Components, fetching data is also quite flexible and, in my opinion, feels closer to vanilla JavaScript — which always smooths the learning curve. For example, understanding the JavaScript runtime makes it possible to define data-fetching as either parallel or sequential and thus have more fine-grained control on the resource loading waterfall.

  • Parallel Data Fetching, waiting for all:
import TodoList from './todo-list'

async function getUser(userId) {
  const res = await fetch(`https://<some-api>/user/${userId}`);
  return res.json()
}

async function getTodos(userId) {
  const res = await fetch(`https://<some-api>/todos/${userId}/list`);
  return res.json()
}

export default async function Page({ params: { userId } }) {
  // Initiate both requests in parallel.
  const userResponse = getUser(userId)
  const  = getTodos(username)

  // Wait for the promises to resolve.
  const [user, todos] = await Promise.all([userResponse, todosResponse])

  return (
    <>
      <h1>{user.name}</h1>
      <TodoList list={todos}></TodoList>
    </>
  )
}
  • Parallel, waiting for one request, streaming the other:
async function getUser(userId) {
  const res = await fetch(`https://<some-api>/user/${userId}`);
  return res.json()
}

async function getTodos(userId) {
  const res = await fetch(`https://<some-api>/todos/${userId}/list`);
  return res.json()
}

export default async function Page({ params: { userId } }) {
  // Initiate both requests in parallel.
  const userResponse = getUser(userId)
  const todosResponse = getTodos(userId)

  // Wait only for the user.
  const user = await userResponse

  return (
    <>
      <h1>{user.name}</h1>
            <Suspense fallback={<div>Fetching todos...</div>}>
          <TodoList listPromise={todosResponse}></TodoList>
            </Suspense>
    </>
  )
}

async function TodoList ({ listPromise }) {
  // Wait for the album's promise to resolve.
  const todos = await listPromise;

  return (
    <ul>
      {todos.map(({ id, name }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  );
}

In this case, <TodoList> receives an in-flight Promise and needs to await it before rendering. The app will render the suspense fallback component until it’s all done.

  • Sequential Data Fetching fires one request at a time and awaits for each:
async function getUser(username) {
  const res = await fetch(`https://<some-api>/user/${userId}`);
  return res.json()
}

async function getTodos(username) {
  const res = await fetch(`https://<some-api>/todos/${userId}/list`);
  return res.json()
}

export default async function Page({ params: { userId } }) {
  const user = await getUser(userId)


  return (
    <>
      <h1>{user.name}</h1>
            <Suspense fallback={<div>Fetching todos...</div>}>
            <TodoList userId={userId} />
            </Suspense>
    </>
  )
}

async function TodoList ({ userId }) {
  const todos = await getTodos(userId);

  return (
    <ul>
      {todos.map(({ id, name }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  );
}

Now, Page will fetch and wait on getUser, then it will start rendering. Once it reaches <TodoList>, it will fetch and wait on getTodos. This is still more granular than what we are used to it with the Pages directory.

Important things to note:

  • Requests fired within the same component scope will be fired in parallel (more about this at Extended Fetch API below).
  • Same requests fired within the same server runtime will be deduplicated (only one is actually happening, the one with the shortest cache expiration).
  • For requests that won’t use fetch (such as third-party libraries like SDKs, ORMs, or database clients), route caching will not be affected unless manually configured via segment cache configuration.
export const revalidate = 600; // revalidate every 10 minutes

export default function Contributors({
  params
}: {
  params: { projectId: string };
}) {
    const { projectId }  = params
    const { contributors } = await myORM.db.workspace.project({ id: projectId })

  return <ul>{*/ ... */}</ul>;
}

To point out how much more control this gives developers: when within the pages directory, rendering would be blocked until all data is available. When using getServerSideProps, the user would still see the loading spinner until data for the entire route is available. To mimic this behavior in the App directory, the fetch requests would need to happen in the layout.tsx for that route, so always avoid doing it. An “all or nothing” approach is rarely what you need, and it leads to worse perceived performance as opposed to this granular strategy.

Extended Fetch API

The syntax remains the same: fetch(route, options). But according to the Web Fetch Spec, the options.cache will determine how this API will interact with the browser cache. But in Next.js, it will interact with the framework server-side HTTP Cache.

When it comes to the extended Fetch API for Next.js and its cache policy, two values are important to understand:

  • force-cache: the default, looks for a fresh match and returns it.
  • no-store or no-cache: fetches from the remote server on every request.
  • next.revalidate: the same syntax as ISR, sets a hard threshold to consider the resource fresh.
fetch(`https://route`, { cache: 'force-cache', next: { revalidate: 60 } })

The caching strategy allows us to categorize our requests:

  • Static Data: persist longer. E.g., blog post.
  • Dynamic Data: changes often and/or is a result of user interaction. E.g., comments section, shopping cart.

By default, every data is considered static data. This is due to the fact force-cache is the default caching strategy. To opt out of it for fully dynamic data, it’s possible to define no-store or no-cache.

If a dynamic function is used (e.g., setting cookies or headers), the default will switch from force-cache to no-store!

Finally, to implement something more similar to Incremental Static Regeneration, you’ll need to use next.revalidate. With the benefit that instead of being defined for the entire route, it only defines the component it’s a part of.

Migrating From Pages To App

Porting logic from Pages directory to Apps directory may look like a lot of work, but Next.js has worked prepared to allow both architectures to coexist, and thus migration can be done incrementally. Additionally, there is a very good migration guide in the documentation; I recommend you to read it fully before jumping into a refactoring.

Guiding you through the migration path is beyond the scope of this article and would make it redundant to the docs. Alternatively, in order to add value on top of what the official documentation offers, I will try to provide insight into the friction points my experience suggests you will find.

The Case Of React Context

In order to provide all the benefits mentioned above in this article, RSC can’t be interactive, which means they don’t have hooks. Because of that, we have decided to push our client-side logic to the leaves of our rendering tree as late as possible; once you add interactiveness, children of that component will be client-side.

In a few cases pushing some components will not be possible (especially if some key functionality depends on React Context, for example). Because most libraries are prepared to defend their users against Prop Drilling, many create context providers to skip components from root to distant descendants. So ditching React Context entirely may cause some external libraries not to work well.

As a temporary solution, there is an escape hatch to it. A client-side wrapper for our providers:

// /providers.jsx
‘use client’

import { type ReactNode, createContext } from 'react';

const SomeContext = createContext();

export default function ThemeProvider({ children }: { children: ReactNode }) {
  return (
    <SomeContext.Provider value="data">
      {children}
    </SomeContext.Provider>
  );
}

And so the layout component will not complain about skipping a client component from rendering.

// app/.../layout.jsx
import { type ReactNode } from 'react';
import Providers from ‘./providers’;

export default function Layout({ children }: { children: ReactNode }) {
    return (
    <Providers>{children}</Providers>
  );
}

It is important to realize that once you do this, the entire branch will become client-side rendered. This approach will take everything within the <Providers> component to not be rendered on the server, so use it only as a last resort.

TypeScript And Async React Elements

When using async/await outside of Layouts and Pages, TypeScript will yield an error based on the response type it expects to match its JSX definitions. It is supported and will still work in runtime, but according to Next.js documentation, this needs to be fixed upstream in TypeScript.

For now, the solution is to add a comment in the above line {/* @ts-expect-error Server Component */}.

Client-side Fetch On The Works

Historically, Next.js has not had a built-in data mutation story. Requests being fired from the client side were at the developer’s own discretion to figure out. With React Server Components, this is bound for a chance; the React team is working on a use hook which will accept a Promise, then it will handle the promise and return the value directly.

In the future, this will supplant most bad cases of useEffect in the wild (more on that in the excellent talk “Goodbye UseEffect”) and possibly be the standard for handling asynchronicity (fetching included) in client-side React.

For the time being, it is still recommended to rely on libraries like React-Query and SWR for your client-side fetching needs. Be especially aware of the fetch behavior, though!

So, Is It Ready?

Experimenting is at the essence of moving forward, and we can’t make a nice omelet without breaking eggs. I hope this article has helped you answer this question for your own specific use case.

If on a greenfield project, I’d possibly take App directory for a spin and keep Page directory as a fallback or for the functionality that is critical for business. If refactoring, it would depend on how much client-side fetching I have. Few: do it; many: probably wait for the full story.

Let me know your thoughts on Twitter or in the comments below.

Further Reading On SmashingMag

The Key To Good Component Design Is Selfishness

When developing a new feature, what determines whether an existing component will work or not? And when a component doesn’t work, what exactly does that mean?

Does the component functionally not do what it’s expected to do, like a tab system that doesn’t switch to the correct panel? Or is it too rigid to support the designed content, such as a button with an icon after the content instead of before it? Or perhaps it’s too pre-defined and structured to support a slight variant, like a modal that always had a header section, now requiring a variant without one?

Such is the life of a component. All too often, they’re built for a narrow objective, then hastily extended for minor one-off variations again and again until it no longer works. At that point, a new component is created, the technical debt grows, the onboarding learning curve becomes steeper, and the maintainability of the codebase is more challenging.

Is this simply the inevitable lifecycle of a component? Or can this situation be averted? And, most importantly, if it can be averted, how?

Selfishness. Or perhaps, self-interest. Better yet, maybe a little bit of both.

Far too often, components are far too considerate. Too considerate of one another and, especially, too considerate of their own content. In order to create components that scale with a product, the name of the game is self-interest bordering on selfishness — cold-hearted, narcissistic, the-world-revolves-around-me selfishness.

This article isn’t going to settle the centuries-old debate about the line between self-interest and selfishness. Frankly, I’m not qualified to take part in any philosophical debate. However, what this article is going to do is demonstrate that building selfish components is in the best interest of every other component, designer, developer, and person consuming your content. In fact, selfish components create so much good around them that you could almost say they’re selfless.

I don’t know 🤷‍♀️ Let’s look at some components and decide for ourselves.

Note: All code examples and demos in this article will be based on React and TypeScript. However, the concepts and patterns are framework agnostic.

The Consideration Iterations

Perhaps, the best way to demonstrate a considerate component is by walking through the lifecycle of one. We’ll be able to see how they start small and functional but become unwieldy once the design evolves. Each iteration backs the component further into a corner until the design and needs of the product outgrow the capabilities of the component itself.

Let’s consider the modest Button component. It’s deceptively complex and quite often trapped in the consideration pattern, and therefore, a great example of working through.

Iteration 1

While these sample designs are quite barebones, like not showing various :hover, :focus, and disabled states, they do showcase a simple button with two color themes.

At first glance, it’s possible the resulting Button component could be as barebones as the design.

// First, extend native HTML button attributes like onClick and disabled from React.
type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
  text: string;
  theme: 'primary' | 'secondary';
}
<Button
  onClick={someFunction}
  text="Add to cart"
  theme="primary"
/>

It’s possible, and perhaps even likely, that we’ve all seen a Button component like this. Maybe we’ve even made one like it ourselves. Some of the namings may be different, but the props, or the API of the Button, are roughly the same.

In order to meet the requirements of the design, the Button defines props for the theme and text. This first iteration works and meets the current needs of both the design and the product.

However, the current needs of the design and product are rarely the final needs. When the next design iterations are created, the Add to cart button now requires an icon.

Iteration 2

After validating the UI of the product, it was decided that adding an icon to the Add to cart button would be beneficial. The designs explain, though, that not every button will include an icon.

Returning to our Button component, its props can be extended with an optional icon prop which maps to the name of an icon to conditionally render.

type ButtonProps = {
  theme: 'primary' | 'secondary';
  text: string;
  icon?: 'cart' | '...all-other-potential-icon-names';
}
<Button
  theme="primary"
  onClick={someFunction}
  text="Add to cart"
  icon="cart"
/>

Whew! Crisis averted.

With the new icon prop, the Button can now support variants with or without an icon. Of course, this assumes the icon will always be shown at the end of the text, which, to the surprise of nobody, is not the case when the next iteration is designed.

Iteration 3

The previous Button component implementation included the icon at the text’s end, but the new designs require an icon to optionally be placed at the start of the text. The single icon prop will no longer fit the needs of the latest design requirements.

There are a few different directions that can be taken to meet this new product requirement. Maybe an iconPosition prop can be added to the Button. But what if there comes a need to have an icon on both sides? Maybe our Button component can get ahead of this assumed requirement and make a few changes to the props.

The single icon prop will no longer fit the needs of the product, so it’s removed. In its place, two new props are introduced, iconAtStart and iconAtEnd.

type ButtonProps = {
  theme: 'primary' | 'secondary' | 'tertiary';
  text: string;
  iconAtStart?: 'cart' | '...all-other-potential-icon-names';
  iconAtEnd?: 'cart' | '...all-other-potential-icon-names';
}

After refactoring the existing uses of Button in the codebase to use the new props, another crisis is averted. Now, the Button has some flexibility. It’s all hardcoded and wrapped in conditionals within the component itself, but surely, what the UI doesn’t know can’t hurt it.

Up until this point, the Button icons have always been the same color as the text. It seems reasonable and like a reliable default, but let’s throw a wrench into this well-oiled component by defining a variation with a contrasting color icon.

Iteration 4

In order to provide a sense of feedback, this confirmation UI stage was designed to be shown temporarily when an item has been added to the cart successfully.

Maybe this is a time when the development team chooses to push back against the product requirements. But despite the push, the decision is made to move forward with providing color flexibility to Button icons.

Again, multiple approaches can be taken for this. Maybe an iconClassName prop is passed into the Button to have greater control over the icon’s appearance. But there are other product development priorities, and instead, a quick fix is done.

As a result, an iconColor prop is added to the Button.

type ButtonProps = {
  theme: 'primary' | 'secondary' | 'tertiary';
  text: string;
  iconAtStart?: 'cart' | '...all-other-potential-icon-names';
  iconAtEnd?: 'cart' | '...all-other-potential-icon-names';
  iconColor?: 'green' | '...other-theme-color-names';
}

With the quick fix in place, the Button icons can now be styled differently than the text. The UI can provide the designed confirmation, and the product can, once again, move forward.

Of course, as product requirements continue to grow and expand, so do their designs.

Iteration 5

With the latest designs, the Button must now be used with only an icon. This can be done in a few different approaches, yet again, but all of them require some amount of refactoring.

Perhaps a new IconButton component is created, duplicating all other button logic and styles into two places. Or maybe that logic and styles are centralized and shared across both components. However, in this example, the development team decides to keep all the variants in the same Button component.

Instead, the text prop is marked as optional. This could be as quick as marking it as optional in the props but could require additional refactoring if there’s any logic expecting the text to exist.

But then comes the question, if the Button is to have only an icon, which icon prop should be used? Neither iconAtStart nor iconAtEnd appropriately describes the Button. Ultimately, it’s decided to bring the original icon prop back and use it for the icon-only variant.

type ButtonProps = {
  theme: 'primary' | 'secondary' | 'tertiary';
  iconAtStart?: 'cart' | '...all-other-potential-icon-names';
  iconAtEnd?: 'cart' | '...all-other-potential-icon-names';
  iconColor?: 'green' | '...other-theme-color-names';
  icon?: 'cart' | '...all-other-potential-icon-names';
  text?: string;
}

Now, the Button API is getting confusing. Maybe a few comments are left in the component to explain when and when not to use specific props, but the learning curve is growing steeper, and the potential for error is increasing.

For example, without adding great complexity to the ButtonProps type, there is no stopping a person from using the icon and text props at the same time. This could either break the UI or be resolved with greater conditional complexity within the Button component itself. Additionally, the icon prop can be used with either or both of the iconAtStart and IconAtEnd props as well. Again, this could either break the UI or be resolved with even more layers of conditionals within the component.

Our beloved Button has become quite unmanageable at this point. Hopefully, the product has reached a point of stability where no new changes or requirements will ever happen again. Ever.

Iteration 6

So much for never having any more changes. 🤦

This next and final iteration of the Button is the proverbial straw that breaks the camel’s back. In the Add to cart button, if the current item is already in the cart, we want to show the quantity of which on the button. On the surface, this is a straightforward change of dynamically building the text prop string. But the component breaks down because the current item count requires a different font weight and an underline. Because the Button accepts only a plain text string and no other child elements, the component no longer works.

Would this design have broken the Button if this was the second iteration? Maybe not. The component and codebase were both much younger then. But the codebase has grown so much by this point that refactoring for this requirement is a mountain to climb.

This is when one of the following things will likely happen:

  1. Do a much larger refactor to move the Button away from a text prop to accepting children or accepting a component or markup as the text value.
  2. The Button is split into a separate AddToCart button with an even more rigid API specific to this one use case. This also either duplicates any button logic and styles into multiple places or extracts them into a centralized file to share everywhere.
  3. The Button is deprecated, and a ButtonNew component is created, splitting the codebase, introducing technical debt, and increasing the onboarding learning curve.

Neither outcome is ideal.

So, where did the Button component go wrong?

Sharing Is Impairing

What is the responsibility of an HTML button element exactly? Narrowing down this answer will shine light onto the issues facing the previous Button component.

The responsibilities of the native HTML button element go no further than:

  1. Display, without opinion, whatever content is passed into it.
  2. Handle native functionality and attributes such as onClick and disabled.

Yes, each browser has its own version of how a button element may look and display content, but CSS resets are often used to strip those opinions away. As a result, the button element boils down to little more than a functional container for triggering events.

The onus of formatting any content within the button isn’t the responsibility of the button but of the content itself. The button shouldn’t care. The button should not share the responsibility for its content.

The core issue with the considerate component design is that component props define the content and not the component itself.

In the previous Button component, the first major limitation was the text prop. From the first iteration, a limitation was placed on the content of the Button. While the text prop fit with the designs at that stage, it immediately deviated from the two core responsibilities of the native HTML button. It immediately forced the Button to be aware of and responsible for its content.

In the following iterations, the icon was introduced. While it seemed reasonable to bake a conditional icon into the Button, also doing so deviated from the core button responsibilities. Doing so limited the use cases of the component. In later iterations, the icon needed to be available in different positions, and the Button props were forced to expand to style the icon.

When the component is responsible for the content it displays, it needs an API that can accommodate all content variations. Eventually, that API will break down because the content will forever and always change.

Introducing The Me In Team

There’s an adage used in all team sports, “There’s no ‘I’ in a team.” While this mindset is noble, some of the greatest individual athletes have embodied other ideas.

Michael Jordan famously responded with his own perspective, “There’s an ‘I’ in win.” The late Kobe Bryant had a similar idea, “There’s an ‘M-E’ in [team].”

Our original Button component was a team player. It shared the responsibility of its content until it reached the point of deprecation. How could the Button have avoided such constraints by embodying a “M-E in team” attitude?

Me, Myself, And UI
When the component is responsible for the content it displays, it will break down because the content will forever and always change.

How would a selfish component design approach have changed our original Button?

Keeping the two core responsibilities of the HTML button element in mind, the structure of our Button component would have immediately been different.

// First, extend native HTML button attributes like onClick and disabled from React.
type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
  theme: 'primary' | 'secondary' | 'tertiary';
}
<Button
  onClick={someFunction}
  theme="primary"
>
  <span>Add to cart</span>
</Button>

By removing the original text prop in lieu of limitless children, the Button is able to align with its core responsibilities. The Button can now act as little more than a container for triggering events.

By moving the Button to its native approach of supporting child content, the various icon-related props are no longer required. An icon can now be rendered anywhere within the Button regardless of size and color. Perhaps the various icon-related props could be extracted into their own selfish Icon component.

<Button
  onClick={someFunction}
  theme="primary"
>
  <Icon name="cart" />
  <span>Add to cart</span>
</Button>

With the content-specific props removed from the Button, it can now do what all selfish characters do best, think about itself.

// First, extend native HTML button attributes like onClick and disabled from React.
type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
  size: 'sm' | 'md' | 'lg';
  theme: 'primary' | 'secondary' | 'tertiary';
  variant: 'ghost' | 'solid' | 'outline' | 'link'
}

With an API specific to itself and independent content, the Button is now a maintainable component. The self-interest props keep the learning curve minimal and intuitive while retaining great flexibility for various Button use cases.

Button icons can now be placed at either end of the content.

<Button
  onClick={someFunction}
  size="md"
  theme="primary"
  variant="solid"
>
  <Box display="flex" gap="2" alignItems="center">
    <span>Add to cart</span>
    <Icon name="cart" />
  </Box>
</Button>

Or, a Button could have only an icon.

<Button
  onClick={someFunction}
  size="sm"
  theme="secondary"
  variant="solid"
>
  <Icon name="cart" />
</Button>

However, a product may evolve over time, and selfish component design improves the ability to evolve along with it. Let’s go beyond the Button and into the cornerstones of selfish component design.

The Keys to Selfish Design

Much like when creating a fictional character, it’s best to show, not tell, the reader that they’re selfish. By reading about the character’s thoughts and actions, their personality and traits can be understood. Component design can take the same approach.

But how exactly do we show in a component’s design and use that it is selfish?

HTML Drives The Component Design

Many times, components are built as direct abstractions of native HTML elements like a button or img. When this is the case, let the native HTML element drive the design of the component.

Specifically, if the native HTML element accepts children, the abstracted component should as well. Every aspect of a component that deviates from its native element is something that must be learned anew.

When our original Button component deviated from the native behavior of the button element by not supporting child content, it not only became rigid but it required a mental model shift just to use the component.

There has been a lot of time and thought put into the structure and definitions of HTML elements. The wheel doesn’t need to be reinvented every time.

Children Fend For Themselves

If you’ve ever read Lord of the Flies, you know just how dangerous it can be when a group of children is forced to fend for themselves. However, in the case of selfish component design, we’ll be doing exactly that.

As shown in our original Button component, the more it tried to style its content, the more rigid and complicated it became. When we removed that responsibility, the component was able to do a lot more but with a lot less.

Many elements are little more than semantic containers. It’s not often we expect a section element to style its content. A button element is just a very specific type of semantic container. The same approach can apply when abstracting it to a component.

Components Are Singularly Focused

Think of selfish component design as arranging a bunch of terrible first dates. A component’s props are like the conversation that is entirely focused on them and their immediate responsibilities:

  • How do I look?
    Props need to feed the ego of the component. In our refactored Button example, we did this with props like size, theme, and variant.
  • What am I doing?
    A component should only be interested in what it, and it alone, is doing. Again, in our refactored Button component, we do this with the onClick prop. As far as the Button is concerned, if there’s another click event somewhere within its content, that’s the content’s problem. The Button does. not. care.
  • When and where am I going next?
    Any jet-setting traveler is quick to talk about their next destination. For components like modals, drawers, and tooltips, when and where they’re going is just as gravely important. Components like these are not always rendered in the DOM. This means that in addition to knowing how they look and what they do, they need to know when and where to be. In other words, this can be described with props like isShown and position.

Composition Is King

Some components, such as modals and drawers, can often contain different layout variations. For example, some modals will show a header bar while others do not. Some drawers may have a footer with a call to action. Others may have no footer at all.

Instead of defining each layout in a single Modal or Drawer component with conditional props like hasHeader or showFooter, break the single component into multiple composable child components.

<Modal>
  <Modal.CloseButton />
  <Modal.Header> ... </Modal.Header>
  <Modal.Main> ... <Modal.Main>
</Modal>
<Drawer>
  <Drawer.Main> ... </Drawer.Main>
  <Drawer.Footer> ... </Drawer.Footer>
</Drawer>

By using component composition, each individual component can be as selfish as it wants to be and used only when and where it’s needed. This keeps the root component’s API clean and can move many props to their specific child component.

Let’s explore this and the other keys to selfish component design a bit more.

You’re So Vain, You Probably Think This Code Is About You

Perhaps the keys of selfish design make sense when looking back at the evolution of our Button component. Nevertheless, let’s apply them again to another commonly problematic component — the modal.

For this example, we have the benefit of foresight in the three different modal layouts. This will help steer the direction of our Modal while applying each key of selfish design along the way.

First, let’s go over our mental model and break down the layouts of each design.

In the Edit Profile modal, there are defined header, main and footer sections. There’s also a close button. In the Upload Successful modal, there’s a modified header with no close button and a hero-like image. The buttons in the footer are also stretched. Lastly, in the Friends modal, the close button returns, but now the content area is scrollable, and there’s no footer.

So, what did we learn?

We learned that the header, main and footer sections are interchangeable. They may or may not exist in any given view. We also learned that the close button functions independently and is not tied to any specific layout or section.

Because our Modal can be comprised of interchangeable layouts and arrangements, that’s our sign to take a composable child component approach. This will allow us to plug and play pieces into the Modal as needed.

This approach allows us to very narrowly define the responsibilities of our root Modal component.

Conditionally render with any combination of content layouts.

That’s it. So long as our Modal is just a conditionally-rendered container, it will never need to care about or be responsible for its content.

With the core responsibility of our Modal defined, and the composable child component approach decided, let’s break down each composable piece and its role.

Component Role
<Modal> This is the entry point of the entire Modal component. This container is responsible for when and where to render, how the modal looks, and what it does, like handle accessibility considerations.
<Modal.CloseButton /> An interchangeable Modal child component that can be included only when needed. This component will work similarly to our refactored Button component. It will be responsible for how it looks, where it’s shown, and what it does.
<Modal.Header> The header section will be an abstraction of the native HTML header element. It will be little more than a semantic container for any content, like headings or images, to be shown.
<Modal.Main> The main section will be an abstraction of the native HTML main element. It will be little more than a semantic container for any content.
<Modal.Footer> The footer section will be an abstraction of the native HTML footer element. It will be little more than a semantic container for any content.

With each component and its role defined, we can start creating props to support those roles and responsibilities.

Modal

Earlier, we defined the barebones responsibility of the Modal, knowing when to conditionally render. This can be achieved using a prop like isShown. Therefore, we can use these props, and whenever it’s true, the Modal and its content will render.

type ModalProps = {
  isShown: boolean;
}
<Modal isShown={showModal}>
  ...
</Modal>

Any styling and positioning can be done with CSS in the Modal component directly. There’s no need to create specific props at this time.

Modal.CloseButton

Given our previously refactored Button component, we know how the CloseButton should work. Heck, we can even use our Button to build our CloseButton component.

import { Button, ButtonProps } from 'components/Button';

export function CloseButton({ onClick, ...props }: ButtonProps) {
  return (
    <Button {...props} onClick={onClick} variant="ghost" theme="primary" />
  )
}
<Modal>
  <Modal.CloseButton onClick={closeModal} />
</Modal>

Modal.Header, Modal.Main, Modal.Footer

Each of the individual layout sections, Modal.Header, Modal.Main, and Modal.Footer, can take direction from their HTML equivalents, header, main, and footer. Each of these elements supports any variation of child content, and therefore, our components will do the same.

There are no special props needed. They serve only as semantic containers.

<Modal>
  <Modal.CloseButton onClick={closeModal} />
  <Modal.Header> ... </Modal.Header>
  <Modal.Main> ... </Modal.Main>
  <Modal.Footer> ... </Modal.Footer>
</Modal>

With our Modal component and its child component defined, let’s see how they can be used interchangeably to create each of the three designs.

Note: The full markup and styles are not shown so as not to take away from the core takeaways.

Edit Profile Modal

In the Edit Profile modal, we use each of the Modal components. However, each is used only as a container that styles and positions itself. This is why we haven’t included a className prop for them. Any content styling should be handled by the content itself, not our container components.

<Modal>
  <Modal.CloseButton onClick={closeModal} />

  <Modal.Header>
    <h1>Edit Profile</h1>
  </Modal.Header>

  <Modal.Main>
    <div className="modal-avatar-selection-wrapper"> ... </div>
    <form className="modal-profile-form"> ... </form>
  </Modal.Main>

  <Modal.Footer>
    <div className="modal-button-wrapper">
      <Button onClick={closeModal} theme="tertiary">Cancel</Button>
      <Button onClick={saveProfile} theme="secondary">Save</Button>
    </div>
  </Modal.Footer>
</Modal>

Upload Successful Modal

Like in the previous example, the Upload Successful modal uses its components as opinionless containers. The styling for the content is handled by the content itself. Perhaps this means the buttons could be stretched by the modal-button-wrapper class, or we could add a “how do I look?” prop, like isFullWidth, to the Button component for a wider or full-width size.

<Modal>
  <Modal.Header>
    <img src="..." alt="..." />
    <h1>Upload Successful</h1>
  </Modal.Header>

  <Modal.Main>
    <p> ... </p>
    <div className="modal-copy-upload-link-wrapper"> ... </div>
  </Modal.Main>

  <Modal.Footer>
    <div className="modal-button-wrapper">
      <Button onClick={closeModal} theme="tertiary">Skip</Button>
      <Button onClick={saveProfile} theme="secondary">Save</Button>
    </div>
  </Modal.Footer>
</Modal>

Friends Modal

Lastly, our Friends modal does away with the Modal.Footer section. Here, it may be enticing to define the overflow styles on Modal.Main, but that is extending the container’s responsibilities to its content. Instead, handling those styles is better suited in the modal-friends-wrapper class.

<Modal>
  <Modal.CloseButton onClick={closeModal} />

  <Modal.Header>
    <h1>AngusMcSix's Friends</h1>
  </Modal.Header>

  <Modal.Main>
      <div className="modal-friends-wrapper">
        <div className="modal-friends-friend-wrapper"> ... </div>
        <div className="modal-friends-friend-wrapper"> ... </div>
        <div className="modal-friends-friend-wrapper"> ... </div>
      </div>
  </Modal.Main>
</Modal>

With a selfishly designed Modal component, we can accommodate evolving and changing designs with flexible and tightly scoped components.

Next Modal Evolutions

Given all that we’ve covered, let’s throw around some hypotheticals regarding our Modal and how it may evolve. How would you approach these design variations?

A design requires a fullscreen modal. How would you adjust the Modal to accommodate a fullscreen variation?

Another design is for a 2-step registration process. How could the Modal accommodate this type of design and functionality?

Recap

Components are the workhorses of modern web development. Greater importance continues to be placed on component libraries, either standalone or as part of a design system. With how fast the web moves, having components that are accessible, stable, and resilient is absolutely critical.

Unfortunately, components are often built to do too much. They are built to inherit the responsibilities and concerns of their content and surroundings. So many patterns that apply this level of consideration break down further each iteration until a component no longer works. At this point, the codebase splits, more technical debt is introduced, and inconsistencies creep into the UI.

If we break a component down to its core responsibilities and build an API of props that only define those responsibilities, without consideration of content inside or around the component, we build components that can be resilient to change. This selfish approach to component design ensures a component is only responsible for itself and not its content. Treating components as little more than semantic containers means content can change or even move between containers without effect. The less considerate a component is about its content and its surroundings, the better for everybody — better for the content that will forever change, better for the consistency of the design and UI, which in turn is better for the people consuming that changing content, and lastly, better for the developers using the components.

The key to the good component design is selfishness. Being a considerate team player is the responsibility of the developer.

Accessible Front-End Patterns For Responsive Tables (Part 2)

In Part 1, we explored general patterns of creating responsive and accessible tables depending on the design, use case, and data complexity. In this article, we’ll cover a few more complex and more specific examples, check out how we can improve performance on larger tables, and cover some JavaScript libraries that can further enhance tables with various functionalities like pagination, filtering, search, and others.

A quick note on accessibility before we start: The following examples lean more toward the design aspect of responsiveness compared to the previous article. I’ve used the same approach to accessibility as I did in the examples from the previous article. Still, as these are more complex and specific examples, further testing and adjustments might be required for these use cases, and I strongly encourage them.

That being said, let’s dive into the examples.

Working With Complex Enterprise Tables

Enterprise data tables display a large amount of complex data across lots of columns, and they rely on searching and filtering to quickly find the data we’re looking for. We’re not going to cover those actions in this article because they do not affect responsiveness and only serve to reduce the number of displayed rows.

The responsive patterns that we covered in the previous article won’t completely solve the UX issue here. The stacking and accordion pattern, for this case, might be too clunky for mobile use, and the scrolling pattern would make the table unusable and difficult to scan.

Lalatendu Satpathy suggests in his article about designing enterprise tables to use the stacking context but display only the critical data that the user will most likely want to search for.

Once users have found a row they were looking for, either by scanning, searching, or filtering, they can open up the details view by tapping the row.

Notice how we’re utilizing the limited screen space to the fullest extent for each operation — we’re showing as many data rows as possible, which contain only primary information, and then we are using an off-canvas element, a full-page element to display all data for a single row.

We’re using the recommended markup for the table element and ARIA labels that we’ve covered in the previous article, so let’s focus on the off-canvas element. First, let’s create a hidden off-canvas element and add empty elements where we’ll append row data for the row that has been clicked on.

<aside id="offcanvas" class="offcanvas" aria-hidden="true">
  <header class="offcanvas-header">
    <button tabindex="-1" onclick="closeOffcanvas()" aria-label="Return to table"><!-- ... --></button>
</header>
  <div><strong id="slot-1"></strong></div>
  <h1 id="slot-2"></h1>
  <dl>
    <dt>Available stock</dt>
    <dd id="slot-3"></dd>
   <!-- ... -->
  </dl>
</aside>

We’re using CSS to make sure that this element only displays on smaller viewports. On larger viewports, even though the off-canvas element could be activated, it won’t be displayed. Alternatively, we could have also used JavaScript’s match media element to prevent the function from running.

@media screen and (max-width: 1260px) {
  .offcanvas {
    display: block;
  }
}

Let’s move onto the row click handle function, which populates off-canvas element slots and applies an active class. We are populating the off-canvas slots by iterating over columns and using an index to target the id-s. Additionally, we are removing the aria-hidden attribute and moving the focus onto the element. We can also use focus trapping to prevent the user from leaving the off-canvas element while it’s opened.

function openAndPopulateAside() {
  if(offcanvas.classList.contains("offcanvas-active")) {
    return;
  }

  const row = window.event.target.closest("tr");
  const columns = Array.from(row.children);

  columns.forEach(function (child, i) {
    const id = `slot-${i + 1}`;
    document.getElementById(id).innerHTML = child.innerHTML;
  });

  offcanvas.classList.add("offcanvas-active");
  offcanvas.removeAttribute("aria-hidden",);
  offcanvas.querySelector("button").tabIndex = undefined;
  offcanvas.focus();
}

We also need to have a way to close the off-canvas element and undo the changes we applied when we activated the modal.

function closeOffcanvas() {
  offcanvas.setAttribute("aria-hidden", "true");
  offcanvas.classList.remove("offcanvas-active");
  offcanvas.querySelector("button").tabIndex = -1;
  document.getElementById("table-wrapper").focus();
}

In these examples, we’re relying on additional elements outside of tables (like our off-canvas element) to help us make full use of the available screen space to fully display table data. Check out the following CodePen example and see how these elements work together to improve table UX on smaller screens.

Comparing this to the previous example, the only primary column is the title & platform column. We cannot pick any other column to include for comparison since they are equally important and depend on user preference. Using a stacked column approach is also not an option, as we want users to compare the review scores to different games and between the review sites. This table is also too complex for a scrollable table, as both the primary column and table headers are equally important. It would take too much screen space if we used the fixed-column approach.

Let’s tackle this problem with the approach described in Joe Winter’s article. First, let’s focus on vertical scanning.

Let’s give users an option to choose the additional column they’ll use for comparison — their preferred review game review site. We’ll use a select element in this case, but tabs and other similar controls work well. We can store their preference in local storage if we want to keep track of user preferences and store it for future use.

<form>
  <label for="filter">Review site</label> 
  <select onchange="filterChange()" id="filter">
    <option value="1">GameSpot</option>
    <option value="2">IGN</option>
    <option value="3">Dexerto</option>
    <option value="4">GameInformer</option>
    <option value="5">VG247</option>
  </select>
</form>
const allBodyRows = document.querySelectorAll("tbody > tr");
const mainHeadCols = document.querySelectorAll("thead > tr:last-child > th");

function filterChange() {
  const value = parseInt(select.value);

  mainHeadCols.forEach(function (col, i) {
    const colIndex = i + 1;

    // Skip the first (primary column).
    if (i == 0) {
      return;
    }

    if (colIndex === 1 || colIndex === value + 1) {
      col.classList.remove("hidden");
    } else {
      col.classList.add("hidden");
    }
  });

  allBodyRows.forEach(function (row) {
    const cols = row.querySelectorAll("td");

    cols.forEach(function (col, i) {
      const colIndex = i + 1;

      if (colIndex === value) {
        col.classList.remove("hidden");
      } else {
        col.classList.add("hidden");
      }
    });
  });
}

Next, we’ll implement the same off-canvas element as we did in the previous example to cover the horizontal scanning, where we display all column data for a selected row.

We’ll use a very similar function and go through the same motions of opening, populating, and closing the off-canvas element.

function openAndPopulateAside() {
  const row = this.window.event.target.closest("tr");
  const columns = Array.from(row.children);

  columns.forEach(function (child, i) {
    const id = `slot-${i + 1}`;
    document.getElementById(id).innerHTML = child.innerHTML;
  });

  offcanvas.classList.add("offcanvas-active");
  offcanvas.removeAttribute("aria-hidden");
  offcanvas.querySelector("button").tabIndex = undefined;
  offcanvas.focus();
}

function closeOffcanvas() {
  offcanvas.setAttribute("aria-hidden", "true");
  offcanvas.classList.remove("offcanvas-active");
  offcanvas.querySelector("button").tabIndex = -1;
  document.getElementById("table-wrapper").focus();
}

We’ve improved upon the previous example by giving users an option to select an additional primary column alongside the “Title & Platform” so users can select which column will be used for comparison between the rows.

But what about more complex calendars used for planning and schedule? They can contain a variable amount of information within the cells, and scaling them down for mobile is not always viable.

We could either use the stacking pattern or scrolling pattern, but they’re not ideal for this calendar project. User needs to see their schedule for today and at least for the next day and have a general overview (a summary) for a wider timespan.

We can divide the large calendar app into two elements on the smaller screens:

  • List element: the schedule for today and the next day;
  • Table element: general, high-level, 5-day overview.

There are too many differences between the large screen and small screen views, so there is no smart way of using CSS to transform between the two. We need to duplicate the element and make sure to hide the inactive element with CSS. This will also hide it from screen readers and make the element not accessible with the keyboard.

<figure class="table-wrapper">
  <figcaption id="caption">
    <h1>Consultation schedule</h1>
  </figcaption>

  <table aria-labelledby="caption" class="table-full">
    <!-- ... -->
  <table>

  <ol class="list">
    <!-- ... -->
  </ol>

  <table aria-labelledby="caption" class="table-map">
    <!-- ... -->
  </table>
</figure>
@media screen and (min-width: 960px) {
  .table-map, .list {
    display: none;
  }
}

@media screen and (max-width: 959px) {
  .table-full {
    display: none;
  }
}

These two views can be easily generated with JavaScript or JavaScript frameworks like React and Svelte, but also with static HTML generators.

However, pagination is not an ideal fit for all tables and data types. Sometimes we just want to display the whole table and allow users to scroll the entire data table without restrictions or interruptions. What can we do if we need to display the whole table regardless of the number of rows and columns?

Virtualization

We can use virtualization. We keep the entire table data in memory but dynamically render table rows and columns that are currently visible to the user. We update the state while the user is scrolling and interacting with the table, all the while maintaining the illusion that every row and column is present by changing inner dimensions to compensate for missing elements.

This can be seen in the example below, where we render out only a handful of rows in DOM out of a total of 100,000 rows! Notice the inlined height style attribute on the second tr element.

The same approach can be used for large lists and various other HTML elements. There are some specialized virtualization libraries like Clusterize.js if you’re looking to implement just that in your project, but many popular JavaScript table libraries like Tabulator and component libraries support this out of the box.

If you want to read more about the effectiveness of table virtualization, Robert Cooper of Basedash published a case study on how table virtualization introduced significant improvements to their React project.

The root cause of the problem was that we were trying to render the entire table at once, even if most of the data for the table was off the screen/viewport. Also, the React code for rendering a single table cell was quite inefficient, so when we needed to render thousands of table cells on initial load, all those inefficiencies compounded. (…)

Overall, after implementing both virtualization and improvements to our table cell, we were able to speed up table load times by 4-5x in most cases and over 10x in extreme cases. All while increasing the default page size from 50 rows to 100.

Depending on the approach you choose, either by implementing virtualization yourself or by using an existing library, make sure to test if the solution is accessible for your use-case — both for keyboard navigation and for assistive devices.

CSS approach

Interestingly enough, there is a non-JS way to optimize table render performance. We can try to apply CSS contain: strict to the table element to signal that the massive table won’t affect the style or layout of any other elements on the page.

This is exactly how Johan Isaksson improved the performance (on his machine) of Google Search Console, which wasn’t using virtualization at the time, after experiencing issues browsing a table with 500 rows (which resulted in over 16,000 DOM elements being rendered).

However, this is not a universal and perfect solution, and depending on your use case, it might cause visual bugs, especially if you are dealing with a dynamic table that can be filtered, search, and reordered.

As the “strictest” of the containment values, this value should be used with careful consideration. This is due to the dimension requirements it imposes on the contained element. With these requirements, this containment value does offer the most potential performance benefits of containment.

If you are working with dynamic tables, which is often the case with enterprise data, you’d want to either use pagination or virtualization, depending on the design and use case, to create fully optimized complex data tables that perform optimally.

JavaScript Libraries For Enhancing Tables

Additional table features like searching, filtering, ordering, and others can improve table UX even on smaller screens by allowing users to easily scan the table and quickly find the information that they’re looking for. There are so many JavaScript-based solutions out there, both specialized and as part of a larger UI components library, and I’d like to highlight some of them here.

Tabulator is a zero-dependency vanilla JavaScript library for enhancing tables with a plethora of aforementioned functionalities and more. It also features separate NPM packages for React, Angular, and Vue. If you are working on a project that heavily features tables and requires lots of features and interactions, Tabulator can do a lot of heavy lifting for you.

As for the framework-specific libraries, I’ve only used react-table, which worked wonderfully on the projects I’ve worked on. It’s fully implemented with React hooks, so it’s fully customizable and doesn’t enforce any markup, design, or HTML structure.

As for table virtualization specifically, Clusterize.js is a solid vanilla JS solution that works well and has been recently updated in the last year at the time of writing. As for the framework-specific library, there is react-virtualized, but it hasn’t been updated for a while so make sure to test if it fits your use case before committing to using it on your project.

Keep in mind that you should always consult Bundlephobia to see package size and dependencies, and make sure to check out the package repository to see if the package is currently being maintained and if the issues raised are being actively addressed.

Conclusion

Creating responsive and accessible tables requires a careful and thoughtful approach, so the table remains usable even on smaller screen sizes. In this article, we’ve covered some highly specific use cases and approaches like an enterprise data table and a calendar. Large & complex data tables may introduce performance issues due to the DOM tree growing too large, so we need to use either pagination or table virtualization to avoid the potential issues. In conclusion, make sure that, regardless of the design and use case, your tables are responsive, usable, accessible, and performant on various types of devices and screen sizes.

References

Document Object Model (DOM) Geometry: A Beginner’s Introduction And Guide

If you’ve been working with JavaScript for a while, you may be fairly familiar with DOM (Document Object Model) and CSSOM (CSS Object Model) scripting. Beyond the interfaces defined by the DOM and CSSOM specifications, a subset of methods and properties are specified in the CSSOM View Module, providing an API for determining and manipulating DOM element geometry to render interesting user interfaces on the web.

Prerequisites:

  • A refresher on Coordinates System;
  • An understanding of CSS Layout and Positioning;
  • Writing callbacks in JavaScript;
  • Some patience.

Table of Contents:

The CSSOM View Module

The CSS Object Model (CSSOM) is a set of APIs allowing CSS manipulation from JavaScript. Just like the DOM provides the interface for manipulating the HTML, the CSSOM allows authors to read and manipulate CSS.

The CSSOM View is a module of CSS that contains a bunch of properties and methods, all bundled up to provide authors with a separate interface for obtaining information about the visual view of elements. The properties in this module are predominantly read-only and are calculated each time they are accessed — live values.

Currently, the CSSOM View Module is only a working draft and under revision in the W3C’s Table of Specification. Its essence, therefore, is to define these interfaces, both already existing and new, in a manner that can be compatible across browsers.

Why Do Geometry Methods and Properties Matter At All?

From my perspective, there are a few reasons to try understanding and using the CSSOM View properties and methods.

First, it is not that the everyday user interface requires movable components to achieve its most basic user stories. Unless you’re building a game interface, you may not always need to make stuff movable on your website. Geometry properties are useful despite these because the ability to programmatically manipulate the visual view of DOM elements gives developers more superpowers for implementing dynamic user interfaces.

Kanban boards are implemented because components can be dragged and dropped at relevant sections. More content is loaded as users scroll to the bottom of a document because scroll position values are readable. So, while it may not seem immediately obvious, it is through knowing the accurate size and position information of elements that these features are achievable.

Second, when viewing HTML documents in a web browser, DOM Elements are rendered in visual shapes, so they have a corresponding visual representation made viewable/visual by browsers. Accessing the live visual properties of these DOM elements through the CSSOM View properties and methods gives an advantage over the regular CSS properties. And then you ask how:

  1. After setting the width and height properties of HTML elements in CSS, the CSS box-sizing property finally sets how an element’s total width and height are calculated. This creates an error-prone JavaScript if the value of our box-sizing changes.
  2. Second, there’s hardly any way to read an exact numeric value of an element’s width set to auto. And sometimes, we need the width in exact pixels and sizes.

Finally, it just seems way more flexible and useful to have a set of read-only lives values that can be relied on when writing some other code that manipulates the elements based on the current live values.

Element Node Geometry

Offsets

Coordinates specified using the “offset” model use the top-left corner of the element being examined or on which an event has occurred.
MDN

Unlike other properties in the CSSOM View, offset properties are only available to HTMLElement nodes derived from the Element node. As such, you cannot read the offset properties of an SVGElement because they don’t exist.

Offset Left and Top

Using the read-only properties offsetLeft and offsetTop gives the x/y coordinates of an element relative to its offsetParent. The offsetLeft property returns the distance of the outer left border of the current element relative to the inner left border of the offsetParent while the offsetTop property returns the distance of the outer top border of the current element relative to the inner top border of the offsetParent.

Offset Parent

The offsetParent of any element is its nearest ancestor element which has a CSS position property that is not static, a <td>, <th>, or <table> element or at the base, the <body> element.

Offset Width and Height

These read-only properties provide the full outer size of element nodes. The offsetWidth is determined by calculating the total size of an element’s vertical borders, padding, and content, including any scrollbars that may exist. The offsetHeight is calculated in the same way using an element’s horizontal borders, padding, and content height.

Clients

Client Left and Top

In the most basic sense of it, these read-only properties give the size in pixels of an element’s left border width and the top-border width, respectively. In a deeper sense, however, the value of the clientLeft and clientTop properties of an element gives the relative coordinates of the inner side (outer padding) of that element from its outer side (outer border).

So, where a document has a right-to-left writing direction and left vertical scrollbars, the clientLeft will return coordinate values, including the size of the scrollbar. This is because the scrollbar displays between the inner side (outer padding) of that element from its outer side (outer border).

Client Width and Height

The read-only clientWidth and clientHeight properties of an element return the size of the area inside the element’s borders. The clientWidth property will return the size of an element’s content width and its vertical padding without the scroll bar. If there is no padding, then the clientWidth is just the size of that element’s content width. This is the same for the clientHeight property, which will return the size of an element’s content height plus horizontal padding, and in the absence of any padding, it will return just the content height as the clientHeight.

Scrolls

Scroll Left and Top

An element with no overflowing content on its x-axis or y-axis will return 0 when its scrollLeft and scrollTop properties are queried, respectively. An element’s scrollLeft property returns the distance in pixels that an element’s content is scrolled horizontally, while the scrollTop property gives the distance in pixels that an element’s content is scrolled vertically.

The pixels returned by the scrollLeft and scrollTop properties of an element are not always viewable in the scrollable viewport or client area due to the scrolling. The pixels can be viewed as representing the size of the area that has been scrolled away either to the left or to the top.

The scrollLeft and scrollTop properties are read-write properties, so their values can be manipulated.

Note: The scrollLeft and scrollTop properties may not always return whole numbers and can return floating point values.

Scroll Width and Height

The scrollWidth property of an element calculates its clientWidth plus the entire overflowing content on its left and right side, while the scrollHeight property calculates an element’s clientHeight plus the entire overflowing content on the element’s top and bottom side.

This is why if an element has no overflowing content on its x or y axes, its scrollWidth and scrollHeight properties will return the same values, respectively, as its clientWidth and clientHeight properties.

MDN explains the scrollWidth and scrollHeight property values as:

“… Equal to the minimum width or height the element would require in order to fit all the content in the viewport without using a horizontal or vertical scrollbar.”
Window and Document Geometry
The Window interface represents a window containing a DOM document; the document property points to the DOM document loaded in that window.

The geometry properties of the document loaded in the window and the window itself are relevant for several reasons. Sometimes we need to read the width of the entire viewport and the entire height of our document, other times, we even want to scroll a page to some definite extent and whatnot. Well, of course, the properties to read the relevant values and information are not left out in the CSSOM View Module.

Because there’s a root <html> element (labeled as Document.documentElement in the DOM) that defines the whole HTML document, we can also get the various height, width, and position properties of the HTML document by querying the root element.

Window Width and Height

The properties for calculating the width and height of the window are divided into inner and outer width and height properties. To calculate the outer width and height of the window, the outerWidth and outerHeight read-only properties are used, and they respectively return the width and height of the whole browser window.

To obtain the inner width and height of the window, the innerWidth and innerHeight properties are used. What is returned is the width and height (including scroll bars) of the entire viewport where the document is visible.

You may need to obtain the inner — viewport width or height of the window without the scrollbar and borders and, in such cases, use the clientWidth or clientHeight on the Document.documentElement, which is the root element representing the document.

Document Width and Height

We never set borders, padding, or margin values on the root element itself. Still, on elements contained in the Document, using the scrollWidth and scrollHeight properties on the root element Document.documentElement will return the document’s entire width and height.

Window and Document Scroll Values

Scroll Left and Top

As explored in the Element Node Geometry section, the scrollLeft and scrollTop properties return in pixels the size of the left or top scrolled away area of an element.

Thus, to determine the left or top scroll state of a document, using the scrollLeft and scrollTop properties on the Document.documentElement will return values representing the size of the part of the Document that has been scrolled away and is not visible in the window’s viewport.

The scroll state values of a document can alternatively and more preferably be obtained using the window.pageXOffset and window.pageYOffset values.

Window and Document Scroll Methods

We can programmatically scroll the page in response to certain user interactions using scroll methods defined in the CSSOM View Module. Let’s consider them.

The scroll() and scrollTo() Methods

These two window methods are basically the same methods and allow you to scroll the page to specific (x, y) coordinates in the Document. The coordinates values represent an absolute position from the top and left corners of the document itself.

To simply visualize this, let’s run this code:

window.scrollTo(0, 500); 
//Scrolls the page vertically to 500 pixels from the page’s origin (0, 0).

window.scrollTo(0, 500);
//Page stays at the same point.

After running window.scrollTo(0, 500) the first time, an attempt to run it a second time does nothing because the page is already at an absolute position of 500 pixels from the Document’s origin on its y-axis.

The scroll() and scrollTo() methods define x and y parameters for corresponding arguments representing the number of pixels along the horizontal and vertical axes, respectively, that you want the page scrolled to or a dictionary of options containing top, left, and behavior values.

The behavior value determines how the scroll occurs. It could be "smooth", which gives a smooth scrolling effect, or "auto", which makes the scrolling like a quick jump to the specified coordinates.

The scrollBy() Method

This is a relative scroll method. It scrolls the page relative to its current position and does not regard the Document origin whatsoever.

To examine this method, let’s use the code example from the scroll() and scrollTo() methods section:

window.scrollTo(0, 500); 
//Scrolls the page 500 pixels from the current position, say (0, 0), to (0, 500).

window.scrollTo(0, 500);
//Scrolls the page another 500 pixels from the current position to (0, 1000).
Coordinates

Coordinate systems are the bane of how positions of elements are defined in the CSSOM View methods and properties.

When specifying the location of a pixel in a graphics context, its position is defined relative to a fixed point in the context. This fixed point is called the origin. The position is specified as the number of pixels offset from the origin along each dimension of the context.

The CSSOM uses standard coordinate systems, and these are generally only different in terms of where their origin is located.

Window and Document Coordinates

While the CSSOM uses four standard coordinate systems, the client and page coordinate systems are the most used in the CSSOM View Module. The dimensions or positions of elements are usually defined relative to either the document or the viewport.

Client Coordinates (Window Relative)

I found no better description of client coordinates than the one from MDN:

The “client” coordinate system uses as its origin the top-left corner of the viewport or browsing context in which the event occurred. This is the entire viewing area in which the document is presented. Scrolling is not a factor.

Client coordinates values are similar to using position: fixed in CSS and are calculated from the view port’s top left edge.

Page Coordinates (Document Relative)

The “page” coordinate system gives the position of a pixel relative to the top-left corner of the entire Document in which the pixel is located. That means that a given point in an element within the document will keep the same coordinates in the page model unless the element moves (either directly by changing its position or indirectly by adding or resizing other content).

Page coordinates values are similar to using position: absolute in CSS and are calculated from the Document’s top left edge. The page-relative position of an element will always stay the same regardless of scrolling, while its window-relative position will depend on the document scrolling.

Element Coordinates

The Element.getBoundingClientRect() Method

This method returns an object called a DOMRect object whose properties are window-relative pixel positions and dimensions of an element. This is the one method you turn to when you need to manipulate an element relative to the viewport.

You should note that in certain cases, the returned DOMRect object does not always hold the same property values or dimensions for the same element. This is specifically true whenever transforms (skew, rotate, scale) are added to an element.

The reason for this is pretty logical:

In case of transforms, the offsetWidth and offsetHeight returns the element's layout width and height, while getBoundingClientRect() returns the rendering width and height. As an example, if the element has width: 100px; and transform: scale(0.5); the getBoundingClientRect() will return 50 as the width, while offsetWidth will return 100.
MDN

You can visualize this by clicking the display button in this pen below:

See the Pen DOM Rect Properties [forked] by Pearl Akpan.

The object returned by the getBoundingClientRect() method holds six dimension properties of the element the method was called on. These properties are:

  • x and y properties return the x and y coordinates of the element’s origin relative to the window;
  • top and bottom properties return the y coordinates for the top and bottom edge of the element’s box;
  • left and right properties return x coordinates for the left and right edge of the element’s box;
  • height and width properties return the entire width and height of the element as if the element is set to box-sizing: border-box.

Mouse and Pointer Events Coordinates

All mouse or pointer event objects have coordinate properties that define both window-relative and document-relative coordinates where the mouse or pointer event occurs.

The window-relative coordinates for mouse events are stored in the clientX and clientY properties which denote the x and y coordinates, respectively.

On the other hand, the document-relative coordinates for mouse and pointer events are stored in the event object’s pageX and pageY properties for the x and y coordinates, respectively.

Use Cases

The APIs in the CSSOM View Module combine the most foundational yet useful methods and properties for accessing geometry properties of DOM Elements as rendered in the browser. Because these properties are live, they are more reliable in specific cases than their CSS values. But how can these APIs be used to create real-life user interface features?

We’d examine four everyday user interface solutions used in everyday modern websites and web apps that can be created using these APIs.

In this section, we will focus solely on the JavaScript code for implementing these user interface solutions, not the CSS nor HTML.

Scroll-to-top Component

The scroll-to-top button allows a user to quickly return to the top of the page with little effort. The CSSOM View API provides a simple method to achieve this with its scrollTo() and duplicate scroll() methods.

Here’s an implementation of the scroll-to-top button:

See the Pen Scroll-To-Top [forked] by Pearl Akpan.

To achieve this, we need to create a scroll-to-top button. In our js file, we add a "click" event listener to this button:

scrollToTop.addEventListener("click", (e) => {
  window.scrollTo({left: 0, top: 0, behavior: "smooth"});
});

Then we register a handler for this event which executes (handles) what happens when this button is clicked. The code in the event handler calls the window’s scrollTo() method, with values that define the top of the page and the behaviour for the scroll.

For user experience, it’d definitely serve no use to see a scroll-to-top button if a user is already at the top of the page:

document.addEventListener("scroll", (e)=> {
  if(window.pageYOffset >= 500) {
      scrollToTop.style.display = "block";
  } else {
    scrollToTop.style.display = "none";
  }
});

The code above displays the scroll-to-top button only when the user has scrolled some distance by using the window.pageYOffset value to determine how far the page has been scrolled. If the page has been scrolled up to 500 pixels to the top, the scroll-to-top component becomes visible, and if not, it stays invisible.

Infinite Scrolling

With its implementation in popular social media, infinite scrolling allows users to scroll down a page; more content automatically and continuously loads at the bottom, eliminating the user’s need to click the next page.

Can you guess how the browser knows to load more content as a user scrolls down the page? How does one determine when a user has reached the bottom?

We know that document.scrollHeight gives the total height of a document, document.clientHeight gives the size of the viewable screen or viewport, and document.scrollTop or window.pageYOffset gives the size of the part of the document that has been scrolled away to the top. Could we take an intuitive guess that if document.scrollTop + document.clientHeight >= document.scrollHeight, then the user has reached the bottom of the page? I think so.

See the Pen Infinite Scroll - [forked] by Pearl Akpan.

This pen uses the infinite scrolling technique to load cards on a page until they reach their maximum count. At its most basic form, it imitates how e-commerce websites display search results for products. Let’s break down how this is achieved.

We use HTML and CSS to define the form and style of the cards container and the styles each element with the class of card should have. In our pen, we hard-code the first set of cards with HTML.

First, we get and assign to constants the following:

  • card container element, which is the parent element for all the cards;
  • status element that displays the current number of cards loaded.

We set a maximum on the number of cards that should be loaded on the page, and we also have a calculated value for the number of cards to be added to the page per load. So, we define a totalCardsNo and cardLoadAmount constants to store the values for the maximum number of cards and the number of cards to be added:

const cardContainer = document.querySelector("main"); 
const currentCardStats = document.querySelector(".currentCardNo"); 
const cardLoadAmount = 9; 
const totalCardsNo = 90; 
let lastIndex;

We need to write a test that checks when a user is at the bottom of the page and loads the cards. By our earlier guess, if document.scrollTop + document.clientHeight >= document.scrollHeight, then our user is at the bottom of the page.

In our pen, we add a scroll event listener to our document and use an arrow function to handle the scroll event. This function “handles” all card loading-related actions but does this only when the user is truly at the bottom of that page, that is, the condition document.scrollTop + document.clientHeight >= document.scrollHeight returns true:

document.addEventListener("scroll", (e) => { 
  if (document.documentElement.scrollTop + document.documentElement.clientHeight >= document.documentElement.scrollHeight) { 
    const children = cardContainer.children; 
    lastIndex = children.length; 
  } 
});

Once the user is at the bottom of the page, we initialize a constant, children, to hold an HTMLCollection of all the currently loaded cards, which are the current children of the cardContainer. The length of children also represents the index (not an array-like index) of the last card, and we store that value in the lastIndex variable.

In our code, we use the value of the lastIndex to know whether to load more cards or whether we’ve reached the totalCardsNo after which we can no longer load cards. If the value of lastIndex is less than totalCardNo, we load more cards:

if(lastIndex < totalCardsNo) { 
  for(let i = 1; i <= cardLoadAmount; i++) { 
    const tile = document.createElement("div"); 
    tile.classList.add("card");
    tile.textContent = `${lastIndex + i}`; 
    cardContainer.appendChild(tile); 
  } 
  currentCardStats.textContent = `${children.length}`; 
} else { 
    return; 
}

This second condition is contained in the first condition, and when it returns false, the event handler function adds no cards.

Animate on Scroll

One of the cooler features in websites and landing pages is components or elements that animate as the page is scrolled to a certain position (usually where the element should be visible) in a document.

Here’s a final result of a page with elements that animate on scroll. Let’s walk through how this is achieved:

See the Pen Animate on Scroll [forked] by Pearl Akpan.

Because the idea of animating an element on scroll depends on when the element becomes visible, we need a test to figure out when an element has become visible, that is, has entered the viewport.

Our visible context is the viewport — the window; we need viewport-relative coordinates of an element. Remember,

The Element.getBoundingClientRect() method returns a DOMRect object providing information about the size of an element and its position relative to the viewport.

In a vertically scrolled page, an element is visible when its getBoundingClientRect().top value is less than the viewport’s height. In our pen, this is the condition we test to decide when to animate our element.

To start, we first get and store all the elements we will be animating in an array:

const animatingElements = Array.from(document.getElementsByClassName("item"));

Our test is summarised in this conditional below. There’s a slight addition to the simple condition that tests if the value of the element’s getBoundingClientRect().top is less than the height of the viewport (defined in document.documentElement.clientHeight).

We want the animation or transition on an element also to be visible, so adding 50 to an element’s getBoundingClientRect().top value sets a condition for the element to be animated when it is at least 50 pixels visible in the viewport:

if(el.getBoundingClientRect().top + 50 < document.documentElement.clientHeight) {
  el.classList.add("animated");
}

In our CSS, we create a class .animated, a utility class for an animation set to run once. Applying this class to an element runs the animation on it:

document.addEventListener("scroll", (e) => {
  animatingElements.forEach((el) => {
    if(el.getBoundingClientRect().top + 50 < document.documentElement.clientHeight) {
      el.classList.add("animated");
    } else {
      return;
    }
  });
});

Now we add a scroll event listener to our document and register a handler that checks if the element is 50 pixels visible on the viewport. If the condition returns true, the animated class is added to the visible element once and for all.

Range Slider

While they may vary in implementation and use, range sliders are one of the more common web components. They are input controls that allow users to select a value or change a state from a control or sliding bar.

Take a look at the final result of this pen, where I implement a basic slider:

See the Pen Range Slider [forked] by Pearl Akpan.

We use HTML and CSS to define and style the elements designated with the classes .track and .thumb, respectively. A “drag” technique is the major implementation in sliders because the thumb is dragged within the track, which defines some sort of range.

So, we are getting and assigning the .track and .thumb elements to constants. Then we declare but don’t initialize the variables draggable and shiftX, to be used later:

const thumb = document.querySelector(".thumb");
const slider = document.querySelector(".track");
let draggable;
let x;

In its most basic sequence, drag-and-drop is achieved by:

  1. Moving the pointer to the object.
  2. Pressing and holding down the button on the mouse or other pointing device to “grab” the object (defined as a “pointerdown” event).
  3. “Dragging” the object to the desired location by moving the pointer to that location is defined as a “pointermove” event).
  4. “Drop” the object by releasing the button defined as a “pointerup” event).

These sequences of actions are each defined in UI Events, specifically, the mouse and pointer events. Because as we saw in the Mouse and Pointer Events Coordinates section, these mouse events have document-relative and window-relative coordinate properties, and we would use them to create a drag-and-drop algorithm for our slider.

Events need handlers, so we declare handlers for the pointerdown, pointermove, and pointerup events each. The first handler we declare is a prepDrag function for the pointerdown event. The pointerdown event fires whenever the mouse or pointer is pressed down on the element that has a listener for the event:

function prepDrag(event) {
  draggable = event.target;
  x = event.clientX - draggable.getBoundingClientRect().left;
  document.addEventListener("pointermove", startDrag);
  document.addEventListener("pointerup", endDrag);
}

The role of this handler is to prepare the element for moving. For instance, if the element was statically positioned, to prepare the element for a moving or “dragging” event, the prepDrag handler will have to set the element’s position to absolute or relative to be able to manipulate the element’s position through it’s top, left values.

Initialising the globally declared draggable and x in the prepDrag handler’s local scope makes the values accessible to the other handlers which will be executed in that scope.

Lastly, in this handler, we add the pointermove and pointerup event listeners to the document and not the thumb element. The reason is that the mousemove event triggers often, but not for every pixel. As such, it can cause unintended drag-and-drop responses. Adding the event listener to the document is a more reliable way to catch the mousemove event.

The second function, startDrag, handles the pointermove event, and it executes all the logic that determines how the thumb element moves and its positioning by manipulating its top and left style values:

function startDrag(event) {
   if (event.clientX < track.offsetLeft || event.clientX > slider.getBoundingClientRect().right){
    return;
  }
    draggable.style.left = event.clientX - shiftX - track.getBoundingClientRect().left +  'px';
}

We want to constrain the dragging of the thumb to the boundaries of the track, such that even if the pointer is moved out of the track while pressed down, the thumb doesn’t get dragged out too.

This is implemented by manipulating the left style value of the draggable only when the mouse event’s clientX property is within the width of the track. Thus, while the pointer is pressed down and moving, the draggable element’s left position style only changes if the mouse event’s clientX value is not less than the track’s offsetLeft value nor greater than the track’s getBoundingClientRect().right value.

The last function, endDrag, handles the pointerup event. It removes the pointermove and pointerup event listeners from the document:

function endDrag() {
  document.removeEventListener("pointermove", startDrag);
  document.removeEventListener("pointerup", endDrag);
}

Since these events are set to initiate in a continuous sequence, it makes sense that their handlers don’t continue to run once the pointerdown event (which begins the sequence) ends:

thumb.addEventListener("pointerdown", prepDrag);

Finally, we add a pointerdown event listener to the thumb element to register a handler for the very first event we listen for.

Conclusion

The use cases covered in this article merely scratch the surface of what is achievable with CSSOM View Module API.

When the heavy DOM manipulation is not considered, I believe the methods and properties in this API give us a lot of tools to customize the geometric properties of web components to suit various interface needs.

Futuristic CSS

I run the yearly State of CSS survey, asking developers about the CSS features and tools they use or want to learn. The survey is actually open right now, so go take it!

The goal of the survey is to help anticipate future CSS trends, and the data is also used by browser vendors to inform their roadmap.

This year, Lea Verou pitched in as lead survey designer to help select which CSS features to include. But even though we added many new and upcoming features (some of which, like CSS nesting, aren’t even supported yet), some features were so far off, far-fetched, and futuristic (or just plain made-up!) that we couldn’t in good conscience include them in the survey.

But it’s fun to speculate. So today, let’s take a look at some CSS features that might one day make their way to the browser… or not!

CSS Toggles

The CSS checkbox hack has been around for over ten years, and it still remains the only way to achieve any kind of “toggle effect” in pure CSS (I actually used it myself recently for the language switcher on this page).

But what if we had actual toggles, though? What if you could handle tabs, accordions, and more, all without writing a single line of JavaScript code?

That’s exactly what Tab Atkins and Miriam Suzanne’s CSS Toggles proposal wants to introduce. The proposal is quite complex, and the number of details and edge cases involved makes it clear that this will be far from trivial for browser vendors to implement. But hey, one can dream, and in fact, an experimental implementation recently appeared in Chrome Canary!

CSS Switch Function

A major trend in recent years — not only in CSS but in society at large — has been recognizing that we’ve often done a poor job of serving the needs of a diverse population. In terms of web development, this translates into building websites that can adapt not only to different devices and contexts but also to different temporary or permanent disabilities such as color blindness or motion sickness.

The result is that we often need to target these different conditions in our code and react to them, and this is where Miriam Suzanne’s switch proposal comes in:

.foo {
  display: grid;
  grid-template-columns: switch(
    auto /
     (available-inline-size > 1000px) 1fr 2fr 1fr 2fr /
     (available-inline-size > 500px) auto 1fr /
   );
}

While the initial proposal focuses on testing available-inline-size as a way to set up grid layouts, one can imagine the same switch syntax being used for many other scenarios as well, as a complement to media and container queries.

Intrinsic Typography

Intrinsic typography is a technique coined by Scott Kellum, who developed the type-setting tool Typetura. In a nutshell, it means that instead of giving the text a specific size, you let it set its own size based on the dimensions of the element containing it:

Instead of sizing and spacing text for each component at every breakpoint, the text is given instructions to respond to the areas it is placed in. As a result, intrinsic typography enables designs to be far more flexible, adapting to the area in which it is placed, with far less code.

This goes beyond what the already quite useful Utopia Type Scale Calculator can offer, as it only adapts based on viewport dimensions — not container dimensions.

The only problem with Typetura is that it currently requires a JavaScript library to work. As is often the case, though, one can imagine that if this approach proves popular, it’ll make its way to native CSS sooner or later.

We can already achieve a lot of this today (or pretty soon, at least) with container query units, which lets you reference a container’s size when defining units for anything inside it.

Sibling Functions

It’s common in Sass to write loops when you want to style a large number of items based on their position in the DOM. For example, to progressively indent each successive item in a list, you could do the following:

@for $i from 1 through 10 {
  ul:nth-child(#{$i}) {
    padding-left: #{$i * 5px}
  }
}

This would then generate the equivalent of 10 CSS declarations. The obvious downside here is that you end up with ten lines of code! Also, what if your list has more than ten elements?

An elegant solution currently in the works is the sibling-count() and sibling-index() functions. Using sibling-index(), the previous example would become:

ul > li {
  padding-left: calc(sibling-index() * 5px); 
}

It’s an elegant solution to a common need!

CSS Patterns

A long, long time ago, I made a little tool called Patternify that would let you draw patterns and export them to base64 code to be dropped inline in your CSS code. My concept was to let you use patterns inside CSS but with CSS Doodle. Yuan Chuan had the opposite idea: what if you used CSS to create the patterns?

Now pure-CSS pattern-making has been around for a while (and recently got more elaborate with the introduction of conic gradients), but Yuan Chuan definitely introduced some key new concepts, starting with the ability to randomize patterns or easily specify a grid.

Obviously, CSS Doodle is probably far more intricate than a native pattern API would ever need to be, but it’s still fun to imagine what we could do with just a few more pattern-focused properties. The @image proposal might be a step in that direction, as it gives you tools to define or modify images right inside your CSS code.

Native HTML/CSS Charts

Now we’re really getting into wild speculation. In fact, as far as I know, no one else has ever submitted a proposal or even blogged about this. But as someone who spends a lot of their time working on data visualizations, I think native HTML/CSS charts would be amazing!

Now, most charts you’ll come across on the web will be rendered using SVG or sometimes Canvas. In fact, this is the approach we use for the surveys through the DataViz library Nivo.

The big problem with this, though, is that neither SVG nor Canvas are really responsive. You can scale them down proportionally, but you can’t have the same fine-grained control that something like CSS Grid offers.

That’s why some have tried to lay out charts using pure HTML and CSS, like charting library Charts.css.

The problem here becomes that once you go past simple blocky bar charts, you need to use a lot of hacks and complex CSS code to achieve what you want. It can work, and libraries like Charts.css do help a lot, but it’s not easy by any means.

That’s why I think having native chart elements in the browser could be amazing. Maybe something like:

<linechart>
  <series id=”series_a”>
    <point x=”0” y=”2”/>
    <point x=”1” y=”4”/>
    <point x=”2” y=”6”/>
  </series>
  <series id=”series_b”>
    <point x=”0” y=”6”/>
    <point x=”1” y=”4”/>
    <point x=”2” y=”2”/>
  </series>
</linechart>

You would then be able to control the chart’s spacing, layout, colors, and so on by using good old CSS — including media and container queries, to make your charts look good in every situation.

Of course, this is something that’s already possible through web components, and many are experimenting in this direction. But you can’t beat the simplicity of pure HTML/CSS.

And Also…

Here are a couple more quick ones just to keep you on your toes:

Container Style Queries

You might already know that container queries let you define an element’s style based on the width or height of its containing element. Container style queries let you do the same, but based on that container’s — you guessed it — style, and there’s actually already an experimental implementation for it in Chrome Canary.

As Geoff Graham points out, this could take the form of something like:

.posts {
  container-name: posts;
}

@container posts (background-color: #f8a100) {
  /* Change styles when `posts` container has an orange background */
  .post {
    color: #fff;
  }
}

This is a bit like :has(), if :has() lets you select based on styles and not just DOM properties and attributes, which, now that I think about it, might be another cool feature too!

Random Numbers

People have tried to simulate a random number generator in CSS for a long time (using the “Cicada Principle” technique and other hacks), but having true built-in randomness would be great.

A CSS random number generator would be useful not just for pattern-making but for any time you need to make a design feel a little more organic. There is a fairly recent proposal that suggests a syntax for this, so it’ll be interesting to see if we ever get CSS randomness!

Grid Coordinates Selector

What if you could target grid items based on their position in a grid or flexbox layout, either by styling a specific row or column or even by targeting a specific item via its x and y coordinates?

It might seem like a niche use case at first, but as we use Grid and Subgrid more and more, we might also need new ways of targeting specific grid items.

Better Form Styling

Styling form inputs has traditionally been such a pain that many UI libraries decide to abstract away the native form input completely and recreate it from scratch using a bunch of divs. As you might imagine, while this might result in nicer-looking forms, it usually comes at the cost of accessibility.

And while things have been getting better, there’s certainly still a lot we could improve when it comes to forming input styling and styling native widgets in general. The new <selectmenu> element proposal is already a great start in that direction.

Animating To Auto

We’ve all run into this: you want to animate an element’s height from 0 to, well, however big it needs to be to show its contents, and that’s when you realize CSS doesn’t let you animate or transition to auto.

There are workarounds, but it would still be nice to have this fixed at the browser level. For this to happen, we’ll also need to be able to use auto inside calc, for example calc(auto / 2 + 200px / 2).

Predicting The Future

Now let’s be real for a second: the chances of any of these features being implemented (let alone supported in all major browsers) are slim, at least if we’re looking at the next couple of years.

But then again, people thought the same about :has() or native CSS nesting, and it does look like we’re well on our way to being able to use those two — and many more — in our code sooner than later.

So let’s touch base again five years from now and see how wrong I was. Until then, I’ll keep charting the course of CSS through our yearly surveys. And I hope you’ll help us by taking this year’s survey!

Thanks to Lea Verou and Bramus Van Damme for their help with this article.

Named Element IDs Can Be Referenced as JavaScript Globals

Did you know that DOM elements with IDs are accessible in JavaScript as global variables? It’s one of those things that’s been around, like, forever but I’m really digging into it for the first time.

If this is the first time you’re hearing about it, brace yourself! We can see it in action simply by adding an ID to an element in HTML:

<div id="cool"></div>

Normally, we’d define a new variable using querySelector("#cool") or getElementById("cool") to select that element:

var el = querySelector("#cool");

But we actually already have access to #cool without that rigamorale:

So, any id — or name attribute, for that matter — in the HTML can be accessed in JavaScript using window[ELEMENT_ID]. Again, this isn’t exactly “new” but it’s really uncommon to see.

As you may guess, accessing the global scope with named references isn’t the greatest idea. Some folks have come to call this the “global scope polluter.” We’ll get into why that is, but first…

Some context

This approach is outlined in the HTML specification, where it’s described as “named access on the Window object.”

Internet Explorer was the first to implement the feature. All other browsers added it as well. Gecko was the only browser at the time to not support it directly in standards mode, opting instead to make it an experimental feature. There was hesitation to implement it at all, but it moved ahead in the name of browser compatibility (Gecko even tried to convince WebKit to move it out of standards mode) and eventually made it to standards mode in Firefox 14.

One thing that might not be well known is that browsers had to put in place a few precautionary measures — with varying degrees of success — to ensure generated globals don’t break the webpage. One such measure is…

Variable shadowing

Probably the most interesting part of this feature is that named element references don’t shadow existing global variables. So, if a DOM element has an id that is already defined as a global, it won’t override the existing one. For example:

<head>
  <script>
    window.foo = "bar";
  </script>
</head>
<body>
  <div id="foo">I won't override window.foo</div>
  <script>
    console.log(window.foo); // Prints "bar"
  </script>
</body>

And the opposite is true as well:

<div id="foo">I will be overridden :(</div>
<script>
  window.foo = "bar";
  console.log(window.foo); // Prints "bar"
</script>

This behavior is essential because it nullifies dangerous overrides such as <div id="alert" />, which would otherwise create a conflict by invalidating the alert API. This safeguarding technique may very well be the why you — if you’re like me — are learning about this for the first time.

The case against named globals

Earlier, I said that using global named elements as references might not be the greatest idea. There are lots of reasons for that, which TJ VanToll has covered nicely over at his blog and I will summarize here:

  • If the DOM changes, then so does the reference. That makes for some really “brittle” (the spec’s term for it) code where the separation of concerns between HTML and JavaScript might be too much.
  • Accidental references are far too easy. A simple typo may very well wind up referencing a named global and give you unexpected results.
  • It is implemented differently in browsers. For example, we should be able to access an anchor with an id — e.g. <a id="cool"> — but some browsers (namely Safari and Firefox) return a ReferenceError in the console.
  • It might not return what you think. According to the spec, when there are multiple instances of the same named element in the DOM — say, two instances of <div class="cool"> — the browser should return an HTMLCollection with an array of the instances. Firefox, however, only returns the first instance. Then again, the spec says we ought to use one instance of an id in an element’s tree anyway. But doing so won’t stop a page from working or anything like that.
  • Maybe there’s a performance cost? I mean, the browser’s gotta make that list of references and maintain it. A couple of folks ran tests in this StackOverflow thread, where named globals were actually more performant in one test and less performant in a more recent test.

Additional considerations

Let’s say we chuck the criticisms against using named globals and use them anyway. It’s all good. But there are some things you might want to consider as you do.

Polyfills

As edge-case-y as it may sound, these types of global checks are a typical setup requirement for polyfills. Check out the following example where we set a cookie using the new CookieStore API, polyfilling it on browsers that don’t support it yet:

<body>
  <img id="cookieStore"></img>
  <script>
    // Polyfill the CookieStore API if not yet implemented.
    // https://developer.mozilla.org/en-US/docs/Web/API/CookieStore
    if (!window.cookieStore) {
      window.cookieStore = myCookieStorePolyfill;
    }
    cookieStore.set("foo", "bar");
  </script>
</body>

This code works perfectly fine in Chrome, but throws the following error in Safari.:

TypeError: cookieStore.set is not a function

Safari lacks support for the CookieStore API as of this writing. As a result, the polyfill is not applied because the img element ID creates a global variable that clashes with the cookieStore global.

JavaScript API updates

We can flip the situation and find yet another issue where updates to the browser’s JavaScript engine can break a named element’s global references.

For example:

<body>
  <input id="BarcodeDetector"></input>
  <script>
    window.BarcodeDetector.focus();
  </script>
</body>

That script grabs a reference to the input element and invokes focus() on it. It works correctly. Still, we don’t know how long it will continue to work.

You see, the global variable we’re using to reference the input element will stop working as soon as browsers start supporting the BarcodeDetector API. At that point, the window.BarcodeDetector global will no longer be a reference to the input element and .focus() will throw a “window.BarcodeDetector.focus is not a function” error.

Conclusion

Let’s sum up how we got here:

  • All major browsers automatically create global references to each DOM element with an id (or, in some cases, a name attribute).
  • Accessing these elements through their global references is unreliable and potentially dangerous. Use querySelector or getElementById instead.
  • Since global references are generated automatically, they may have some side effects on your code. That’s a good reason to avoid using the id attribute unless you really need it.

At the end of the day, it’s probably a good idea to avoid using named globals in JavaScript. I quoted the spec earlier about how it leads to “brittle” code, but here’s the full text to drive the point home:

As a general rule, relying on this will lead to brittle code. Which IDs end up mapping to this API can vary over time, as new features are added to the web platform, for example. Instead of this, use document.getElementById() or document.querySelector().

I think the fact that the HTML spec itself recommends to staying away from this feature speaks for itself.


Named Element IDs Can Be Referenced as JavaScript Globals originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Things I Wish I Had Known About Angular When I Started

I’ve been using Angular since version 2, and it has come a long way since those days to what it is right now. I’ve worked on various Angular projects over the years, yet I keep finding new things. It goes to say how massive the framework is. Here are some things I wish I had known about Angular when I started so you don’t have to learn it the hard way.

Modularize Your Application

Angular has detailed documentation outlining the recommended approach to structure your application. Angular also provides a CLI to help scaffold your application that adheres to their recommendations.

I’ve had my fair share of mistakes when it comes to structuring the application. As you follow tutorials, you’re guided through where you should put your files and which modules the components or services belong to. However, when you venture beyond the tutorial, you sometimes end up with a structure that doesn’t scale well. This could lead to issues down the road.

Below are some mistakes I’ve made that came back and bit me.

Split Your Components Into Modules

The release of Standalone Components in Angular 14 makes NgModules no longer a requirement when creating components. You can choose not to use modules for your components, directives, and pipes. However, you could still follow the folder structure outlined below, omitting the module files.

Initially, I put all the components into the default module you get when creating a new Angular app. As the application grew, I ended up with a lot of components in the same module. They were separate components and didn’t have any need to be in the same module.

Split your components into separate modules, so you can import and load only the required modules. The common approach is to divide your application into the following modules:

  • Core module for singleton services and components that are used once at the app level (example: navigation bar and footer).
  • Feature modules for each feature — code related to the specific functionality of your application. For example, a simple e-commerce application could have a feature module for products, carts, and orders.
  • Shared module for the module that is referenced across different parts of the application. These can include components, directives, and pipes.

Dividing the application into separate modules helps partition your application into smaller, more focused areas. It creates clear boundaries between the different types of modules and each feature module. This separation helps maintain and scale the application as different teams can work on separate parts with a lower risk of breaking another part of the application.

Lazy Load Your Routes

This is a result of my first mistake of putting everything in a single module. Because all the components were in the same module, I couldn’t lazy load the modules. All the modules were imported at the root level, eventually affecting the initial load time. After separating your components into modules, lazy load your routes, so the modules only get loaded when you navigate to the route that requires them.

Single Responsibility

This applies to all types of files in an Angular app. I’ve let my service and component files grow beyond their scope, which made them difficult to work with. The general rule is to keep each component/service/pipe/directive performing a specific set of tasks. If a component is trying to do more than what it was initially made for, it might be worth refactoring and splitting it into several smaller components. This will make testing and maintenance a lot easier.

Use The Angular CLI

You’ve probably used the ng serve command either directly in your command line or through a script in your package.json file. This is one of Angular CLI’s commands. However, the CLI comes with more handy commands that can speed up your development especially when it comes to initializing and scaffolding.

Initially, I did most of these manually as I didn’t understand how to use the CLI except for starting and stopping the local server. I would create component files manually, add the boilerplate code, and add them to the right modules. This was okay for smaller projects but became a tedious task as the project grew. That’s when I learned how to use the CLI and use it to automate most of the manual work I do. For example, instead of creating all the boilerplate for a card component, the following command will create them for you:

ng g c card

You can use the CLI by installing it globally via npm using the command below:

npm install -g @angular/cli

To view the available commands, execute the code below:

ng help

Most projects have custom configurations that are project-specific, and you have to do some modifications to the code generated by the CLI. Angular provides an elegant solution for these scenarios, such as schematics. A schematic is a template-based code generator — a set of instructions to generate or modify code for your project. Similar to Angular CLI, your custom schematics are packaged and can be installed via npm in whichever project needs it.

Path Aliases And Barrel Exports

As I was learning Angular, I tried to keep my project neat by putting all the services into a services folder, models in a models folder, and so on. However, after some time, I end up with a growing list of import statements like this:

import { UserService } from '../../services/user.service';
import { RolesService } from '../../services/roles.service';

Typescript path alias can help simplify your import statements. To setup path aliases, open your tsconfig.json and add the desired path name and its actual path:

{
 "compilerOptions": {
 "paths": {
 "@services/*": ["src/app/services/*"],
 }
 }
}

Now the import statements above can be re-written as:

import { UserService } from '@services/user.service';
import { RolesService } from '@services/roles.service';

An added benefit of using path aliases is that it allows you to move your files around without having to update your imports. You’d have to update them if you were using relative paths.

This can be further simplified by using barrel exports. Barrels are a handy way to export multiple files from a single folder (think of it as a proxy for your files). Add an index.ts in the services folder with the following contents:

export * from './user.service';
export * from './roles.service';

Now, update the tsconfig.json to point to the index.ts file instead of the asterisk (*).

{
 "compilerOptions": {
 "paths": {
 "@services": ["src/app/services/index.ts"],
 }
 }
}

The import statements can now be further simplified into:

import { UserService, RolesService } from '@services';
Embrace Typescript’s Features

I started by learning JavaScript, so I wasn’t used to the type system and the other features that TypeScript offers. My exposure to TypeScript was through Angular, and it was overwhelming to learn both a new language (although it’s a superset of JavaScript, some differences trip me up every time) and a new framework. I often find TypeScript slowing me down instead of helping me with the development. I avoided using TypeScript features and overused the any type in my project.

However, as I got more acquainted with the framework, I began to understand the benefits of TypeScript when used correctly. TypeScript offers a lot of useful features that improve the overall developer experience and make the code you write cleaner. One of the benefits of using TypeScript that I’ve grown accustomed to is the IntelliSense or autocomplete it provides in your IDE. Their type safety and static type checking have also helped catch potential bugs at compile time that could have snuck in.

The nice thing about TypeScript is its flexible configuration. You can toggle their settings easily via their tsconfig.json as per your project’s needs. You can change these settings again if you decide on a different setting. This allows you to set the rules as loose or strict as you’d like.

Improve Performance By Using trackBy

Performance is crucial for applications, and Angular provides various ways to optimize your applications. This is often a problem that you won’t run into at the beginning as you are probably working with small data sets and a limited number of components. However, as your application grows and the number of components being rendered grows and becomes increasingly complex, you’ll start to notice some performance degradation. These performance degradations are usually in the form of slowness in the app: slow to respond, load, or render and stuttering in the UI.

Identifying the source of these problems is an adventure on its own. I’ve found that most of the performance issues I’ve run into in the applications are UI related (this doesn’t mean that other parts of the application don’t affect performance). This is especially prominent when rendering components in a loop and updating an already rendered component. This usually causes a flash in the component when the components are updated.

Under the hood, when a change occurs in these types of components, Angular needs to remove all the DOM elements associated with the data and re-create them with the updated data. That is a lot of DOM manipulations that are expensive.

A solution I’ve found to fix this issue is to use the trackBy function whenever you’re rendering components using the ngFor directive (especially when you’re frequently updating the rendered components).

The ngFor directive needs to uniquely identify items in the iterable to correctly perform DOM updates when items in the iterable are reordered, new items are added, or existing items are removed. For these scenarios, it is desirable only to update the elements affected by the change to make the updates more efficient. The trackBy function lets you pass in a unique identifier to identify each component generated in the loop, allowing Angular to update only the elements affected by the change.

Let’s look at an example of a regular ngFor that creates a new div for each entry in the users array.

@Component({
 selector: 'my-app',
 template: `
 <div *ngFor="let user of users">
 {{ user.name }}
 </div>
 `,
})

export class App {
 users = [
 {id: 1, name: 'Will'},
 {id: 2, name: 'Mike'},
 {id: 3, name: 'John'},
 ]
}

Keeping most of the code the same, we can help Angular keep track of the items in the template by adding the trackBy function and assigning it to a function that returns the unique identifier for each entry in the array (in our case, the user’s id).

@Component({
 selector: 'my-app',
 template: `
 <div *ngFor="let user of users; trackBy: trackByFn">
 {{ user.name }}
 </div>
 `,
})

export class App {
 users = [
 {id: 1, name: 'Will'},
 {id: 2, name: 'Mike'},
 {id: 3, name: 'John'},
 ]
 trackByFn(index, item) {
 return item.id;
 }
}
Use Pipes For Data Transformations

Data transformations are inevitable as you render data in your templates. My initial approach to this was to:

  • Bind the template to a function that accepts the data as the input:
interface User {
 firstName: string,
 middleName: string,
 lastName: string
}
@Component({
 selector: 'my-app',
 template: `
 <h1>{{ formatDisplayName(user) }}</h1>
 `,
})

export class App {
 user: User = {
 firstName: 'Nick',
 middleName: 'Piberius',
 lastName: 'Wilde'
 }
 formatDisplayName(user: User): string {
 return `${user.firstName} ${user.middleName.substring(0,1)}. ${user.lastName}`; 
 }
}
  • Create a new variable, assign the formatted data to the variable, and bind the new variable in the template:
interface User {
 firstName: string,
 middleName: string,
 lastName: string
}
@Component({
 selector: 'my-app',
 template: `
 <h1>{{ displayName }}</h1>
 `,
})

export class App {
 user: User = {
 firstName: 'Nick',
 middleName: 'Piberius',
 lastName: 'Wilde'
 }
 displayName = `${this.user.firstName} ${this.user.middleName.substring(0,1)}. ${this.user.lastName}`; 
}

Neither approach was clean nor performant and wasn’t what Angular recommends to perform data transformations. For these scenarios, angular recommends using pipes. Pipes are functions specifically designed to be used in templates.

Angular provides built-in pipes for common data transformations such as internationalization, date, currency, decimals, percentage, and upper and lower case strings. In addition, Angular also lets you create custom pipes that can be reused throughout your application.

The data transformation above can be re-written using a pipe as follows:

@Pipe({name: 'displayName'})
export class DisplayNamePipe implements PipeTransform {
 transform(user: User): string {
 return `${user.firstName} ${user.middleName.substring(0,1)}. ${user.lastName}`; 
 }
}

The pipe can then be used in the template by using the pipe (|) character followed by the pipe name.

@Component({
 selector: 'my-app',
 template: `
 <h1>{{ user | displayName }}</h1>
 `,
})

export class App {
 user: User = {
 firstName: 'Nick',
 middleName: 'Piberius',
 lastName: 'Wilde'
 }
}
Improve Performance With OnPush Change Detection

Angular applications are made up of a tree of components that rely on their change detectors to keep the view and their corresponding models in sync. When Angular detects a change in the model, it immediately updates the view by walking down the tree of change detectors to determine if any of them have changed. If the change detector detects the change, it will re-render the component and update the DOM with the latest changes.

There are two change detection strategies provided by Angular:

  • Default
    The change detection cycle runs on every event that occurs inside the component.
  • OnPush
    The change detection cycle only runs when a component’s event handler is triggered, an async pipe is used in the template, a new value is emitted, and when any of the component’s input reference changes.

In addition to the reduced number of change detection cycles and its performance boost, the restrictions imposed by using the OnPush change detection strategy also make you architect your app better by pushing you to create more modular components that utilize one of the three recommended ways mentioned above to update the DOM.

RxJS Is Your Friend

RxJS is a JavaScript library that uses observables for reactive programming. While RxJS isn’t exclusively used in Angular, it plays a big role in the Angular ecosystem. Angular’s core features, such as Routing, HttpClient, and FormControl, leverage observables by default.

RxJS is a part of Angular that has been largely unexplored for me as I was learning the framework. I’ve avoided using it unless I had to. It was a new concept, and I found it quite hard to wrap my head around it. I’ve worked with JavaScript Promises, but observables and streams are a new paradigm for me.

After working for a while with Angular, I eventually took the time to learn and understand RxJS and try to use them in my projects. It wasn’t long before I realized the numerous benefits of RxJS that I’ve been missing out on all this time. RxJS, with its large collection of chainable operators, excels in handling async tasks.

I’ve been using RxJS with Angular for a few years now, and my experience has been nothing less than positive. The set of operators RxJS offers is really handy. They seem to have an operator (or a chain of operators) for every use case. Commonly used operators include:

  • map: passes each source value through a transformation function to get corresponding output values.
  • tap: modify the outside state when the observable emits a new value without altering the stream.
  • switchMap: maps each value to an Observable, then flattens all of these inner Observables.
  • filter: emits a value from the source if it passes a criterion function.
  • combineLatestWith: create an observable that combines the latest values from all passed observables and the source into an array and emits them.
Learn How To Spot And Prevent Memory Leaks

Memory leaks are one of the worst types of issues you run into — hard to find, debug, and often hard to solve. This might not be a concern initially, but it becomes crucial when your application reaches a certain size. Common symptoms of memory leaks are degrading performance the longer the app is being used or the same events being fired multiple times. Two of the most common source of memory leaks I’ve run into are:

1. Subscriptions That Are Not Cleaned Up

Unlike the async pipe, listening to an observable using the subscribe method won’t get cleaned up automatically. You will have to manually clean up the subscriptions by calling unsubscribe on the subscription or using the takeUntil operator.

The example below shows a memory leak introduced by listening to the route params observable. Every new instance of MyComponent creates a new subscription which will continue to run even after the component is destroyed.

export class MyComponent {
 constructor(private route: ActivatedRoute){
 this.route.params.subscribe((params) => {
 // Do something
 });
 }
}

As mentioned above, you can fix the memory leak by either calling unsubscribe or using the takeUntil operator.

  • Fixing the memory leak using the unsubscribe method:
export class MyComponent {
 private routeSubscription;
 constructor(private route: ActivatedRoute){
 this.routeSubscription = this.route.params.subscribe((params) => {
 // Do something
 });

 }
 ngOnDestroy() {
 this.routeSubscription.unsubcribe();
 }
}
  • Fixing the memory leak using the takeUntil operator:
export class MyComponent {
 private componentDestroyed$ = new Subject<boolean>();
 constructor(private route: ActivatedRoute){
 this.route.params.pipe(
 takeUntil(this.componentDestroyed$)
 ).subscribe((params) => {
 // Do something
 });

 }
 ngOnDestroy() {
 this.componentDestroyed$.next(true);
 this.componentDestroyed$.complete();
 }
}

2. Event Listeners That Are Not Cleaned Up

Another common source of memory leaks is event listeners that aren’t unregistered when no longer used. For example, the scroll event listener in the code below gets instantiated on every new instance of MyComponent and continuously runs even after the component is destroyed unless you unregister it.

export class MyComponent {
 constructor(private renderer: Renderer2) {}
 ngOnInit() {
 this.renderer.listen(document.body, 'scroll', () => {
 // Do something
 });
 }
}

To fix this and stop listening to the event after the component is destroyed, assign it to a variable and unregister the listener on the ngOnDestroy lifecycle method.

export class MyComponent {
 private listener;
 constructor(private renderer: Renderer2) {}
 ngOnInit() {
 this.listener = this.renderer.listen(
 document.body,
 ‘scroll’,
 () => {
 // Do something
 });

 }
 ngOnDestroy() {
 this.listener();
 }
}
Consider Using A State Management Library (If Applicable)

State management is another part of the stack that you don’t usually think about until you need it. Most small and simple applications don’t need any external state management library. However, as the project grows and managing your application’s state gets more complicated, it might be time to re-think if the project could benefit from implementing more robust state management.

There is no correct solution for state management as every project’s requirements are different. Luckily, there are a few state management libraries for Angular that offer different features. These are a few of the commonly used state management libraries in the Angular ecosystem:

Wrapping Up

If you’ve just started to learn Angular and it hasn’t quite clicked yet, be patient! It will eventually start to make sense, and you’ll see what the framework has to offer. I hope my personal experience can help you accelerate your learning and avoid the mistakes I’ve made.

Building A Retro Draggable Web Component With Lit

Back in the 90s, my first operating system was Windows. Now in the 2020s, I work primarily on building web applications using the browser. Over the years, the browser’s transformed into a wonderful and powerful tool that supports a wide world of rich applications. Many of these applications, with their complex interfaces and breadth of capabilities, would make even the hardiest turn-of-the-millennium programs blush.

Native browser features like web components are being adopted and used across the web by multinational companies and individual developers alike.

In case you’re wondering if anyone is using Web Components:

- GitHub
- YouTube
- Twitter (embedded tweets)
- SalesForce
- ING
- Photoshop web app
- Chrome devtools
- the complete Firefox UI
- Apple Music web client

— Danny Moerkerke (@dannymoerkerke) August 5, 2022

So, why not embrace the technology of the present by paying homage to the interfaces of the past?

In this article, I hope to teach you just that by replicating the iconic broken window effect.

We’ll be using web components, the browser’s native component model, to build out this interface. We’ll also use the Lit library, which simplifies the native web component APIs.

A lot of the concepts I talk about here are lessons I’ve learnt from building A2k, a UI library designed to help you create retro UI with modern tooling.

In this article, we’ll cover:

  • the basics of creating web components using Lit;
  • how to easily customize your component’s behavior using Lit’s built-in tools;
  • how to encapsulate reusable functionality;
  • how to dispatch and respond to events using advanced data flow methods.

It’s worth knowing your core HTML, CSS, and some basic JavaScript to follow along with this tutorial, but no framework-specific knowledge is required.

Getting Started

You can follow allow along in the browser using StackBlitz.

Once StackBlitz finishes setting up, you should see the following in the browser window:

Note: If you don’t want to use StackBlitz, you can clone the repo and run the instructions inside of the README.md file. You can also use the Lit VSCode for syntax highlighting and features.

Next, open up the project in your editor of choice. Let’s have a quick look to see what our starter code looks like.

index.html

We have a very barebones HTML file that does little more than import some CSS and a JavaScript file.

You may have also spotted a brand new element, the a2k-window element. You won’t have seen this before because this is the custom element we’ll be building ourselves. Since we haven’t created and registered this component yet, the browser will fall back to display the inner HTML content.

The Various .js Files

I’ve added a little boilerplate for some of the components and functions, but we’ll fill in the gaps over the course of this article(s). I’ve imported all of the necessary first and third-party code we’ll use throughout this article.

Bonus: Fonts

I’ve also added some retro fonts for fun! It’s a wonderful MS-2000-inspired font created by Lou. You can download it and use it in your own projects if you’re looking to inject a little millennium flavor into your designs.

Part 1: Building Our First Web Component

Writing Our Markup

The first thing we want to do is get a convincing-looking window element going. With just a few lines of code, we’ll have the following.

Let’s start by jumping into our a2k-window.js file. We’ll write a little boilerplate to get our component up and running.

We’ll need to define a class that extends Lit’s LitElement base class. By extending from LitElement, our class gets the ability to manage reactive states and properties. We also need to implement a render function on the class that returns the markup to render.

A really basic implementation of a class will look like this:

class A2kWindow extends LitElement {
  render() {
    return html`
      <div id="window">
        <slot></slot>
      </div>
    `;
  }
}

There are two things worth noting:

  • We can specify an element ID which is then encapsulated within the web component. Just like the top-level document, duplicate IDs are not allowed within the same component, but other web components or external DOM elements can use the same ID.
  • The slot element is a handy tool that can render custom markup passed down from the parent. For those familiar with React, we can liken it to a React portal that renders where you set the children prop. There’s more that you can do with it, but that’s beyond the scope of this article.

Writing the above doesn’t make our web component available in our HTML. We’ll need to define a new custom element to tell the browser to associate this definition with the a2k-window tag name. Underneath our component class, write the following code:

customElements.define("a2k-window", A2kWindow);

Now let’s jump back to our browser. We should expect to see our new component render to the page, but…

Even though our component has been rendered, we see some plain unstyled content. Let’s go ahead and add some more HTML and CSS:

class A2kWindow extends LitElement {
  static styles = css`
    :host {
      font-family: var(--font-primary);
    }

    #window {
      width: min(80ch, 100%);
    }

        #panel {
      border: var(--border-width) solid var(--color-gray-400);
      box-shadow: 2px 2px var(--color-black);
      background-color: var(--color-gray-500);
    }

    #draggable {
      background: linear-gradient(
        90deg,
        var(--color-blue-100) 0%,
        var(--color-blue-700) 100%
      );
      user-select: none;
    }

    #draggable p {
      font-weight: bold;
      margin: 0;
      color: white;
      padding: 2px 8px;
    }

    [data-dragging="idle"] {
      cursor: grab;
    }

    [data-dragging="dragging"] {
      cursor: grabbing;
    }
  `;

  render() {
    return html`
      <div id="window">
        <div id="panel">
          <slot></slot>
        </div>
      </div>
    `;
  }
}

There are a couple of things worth noting in the above code:

  • We define the styles scoped to this custom element via the static styles property. Due to how styles encapsulation works, our component won’t be affected by any external styles. However, we can use the CSS variables we’ve added in our styles.css to apply styles from an external source.
  • I’ve added some styles for DOM elements that don’t exist just yet, but we’ll add them soon.

A note on styles: Styling in Shadow DOM is a topic too large to delve into in this article. To learn more about styling in Shadow DOM, you can refer to the Lit documentation.

If you refresh, you should see the following:

Which is starting to look more like our Windows-inspired web component. 🙌

Pro tip: If you’re not seeing the browser apply the changes you’re expecting. Open up the browser’s dev tools. The browser might have some handy error messages to help you work out where things are failing.

Making Our Web Component Customizable

Our next step is to create the heading for our window component. A core feature of web components is HTML element properties. Instead of hardcoding the text content of our window’s heading, we can make it a property input on the element. We can use Lit to make our properties reactive, which triggers lifecycle methods when changed.

To do this, we need to do three things:

  1. Define the reactive properties,
  2. Assign a default value,
  3. Render the value of the reactive property to the DOM.

First off, we need to specify the reactive properties we want to enable for our component:

class A2kWindow extends LitElement {
  static styles = css`...`;

  static properties = {
    heading: {},
  };

  render() {...}
}

We’ll do this by specifying the static properties object on our class. We then specify the names of the properties we want, along with some options passed through as an object. Lit’s default options handle string property conversion by default. This means we don’t need to apply any options and can leave heading as an empty object.

Our next step is to assign a default value. We’ll do this within the component’s constructor method.

class A2kWindow extends LitElement {
  static styles = css`...`;

  static properties = {...};

    constructor() {
    super();

    this.heading = "Building Retro Web Components with Lit";
  }

  render() {...}
}

Note: Don’t forget to call super()!

And finally, let’s add a little more markup and render the value to the DOM:

class A2kWindow extends LitElement {
  static styles = css`...`;

  static properties = {...};

    constructor() {...}

    render() {
    return html`
      <div id="window">
        <div id="panel">
          <div id="draggable">
            <p>${this.heading}</p>
          </div>
          <slot></slot>
        </div>
      </div>
    `;
  }
}

With that done, let’s jump back to our browser and see how everything looks:

Very convincing! 🙌

Bonus

Apply a custom heading to the a2k-element from the index.html file.

Brief breather 😮‍💨

It’s wonderful to see how easily we can build UI from 1998 with modern primitives in 2022!

And we haven’t even gotten to the fun parts yet! In the next sections, we’ll look into using some of Lit’s intermediate concepts to create drag functionality in a way that’s reusable across custom components.

Part 2: Making Our Component Draggable

This is where things get a little tricky! We’re moving into some intermediate Lit territory, so don’t sweat if not everything makes perfect sense.

Before we start writing the code, let’s have a quick rundown of the concepts we’ll be playing with.

Directives

As you’ve seen, when writing our HTML templates in Lit, we write them inside the html literals tag. This allows us to use JavaScript to alter the behavior of our templates. We can do things like evaluating expressions:

html`<p>${this.heading}</p>`

We can return specific templates under certain conditions:

html`<p>
${this.heading ? this.heading : “Please enter a heading”}
</p>`

There will be times when we’ll need to step out of the normal rendering flow of Lit’s rendering system. You might want to render something at a later time or extend Lit’s template functionality. This can be achieved through the use of directives. Lit has a handful of built-in directives.

We’ll use the styleMap directive, which allows us to apply styles directly to an element via a JavaScript object. The object is then transformed into the element’s inline styles. This will come in handy as we adjust the position of our window element since the element’s position is managed by CSS properties. In short, styleMap turns:

const top = this.top // a variable we could get from our class, a function, or anywhere

styleMap({
    position: "absolute",
    left: "100px",
    top
})

into

"position: absolute; top: 50px; left: 100px;"

Using styleMap makes it easy to use variables to change styles.

Controllers

Lit has a number of handy ways to compose complex components from smaller, reusable pieces of code.

One way is to build components from lots of smaller components. For example, an icon button that looks like this:

The markup may have the following markup:

class IconButton extends LitElement {
    render() {
        return html`
            <a2k-button>
                <a2k-icon icon="windows-icon"></a2k-icon>
                <slot></slot>
            </a2k-button>
        `
    }
}

In the above example, we’re composing our IconButton out of two pre-existing web components.

Another way to compose complex logic is by encapsulating specific state and behavior into a class. Doing so allows us to decouple specific behaviors from our markup. This can be done through the use of controllers, a cross-framework way to share logic that can trigger re-renders in a component. They also have the benefit of hooking into the component’s lifecycle.

Note: Since controllers are cross-framework, they can be used in React and Vue with small adapters.

With controllers, we can do some cool things, like managing the drag state and position of its host component. Interestingly enough, that’s exactly what we plan to do!

While a controller might sound complicated, if we analyse its skeleton, we’ll be able to make sense of what it is and what it does.

export class DragController {
    x = 0;
    y = 0;
    state = "idle"

    styles = {...}

  constructor(host, options) {
    this.host = host;
    this.host.addController(this);
  }

  hostDisconnected() {...}

  onDragStart = (pointer, ev) => {...};

  onDrag = (_, pointers) => {...};
}

We begin by initialising our controller by registering it with the host component and storing a reference to the host. In our case, the host element will be our a2k-window component.

Once we’ve done that, we can hook into our host’s lifecycle methods, like hostConnected, hostUpdate, hostUpdated, hostDisconnected, and so on, to run drag-specific logic. In our case, we’ll only need to hook into hostDisconnected for clean-up purposes.

Finally, we can add our own methods and properties to our controller that will be available to our host component. Here we’re defining a few private methods that will get called during the drag actions. We’re also defining a few properties that our host element can access.

When onDrag and onDragStart functions are invoked, we update our styles property and request that our host component re-renders. Since our host component turns this style object into inline CSS (via the styleMap directive), our component will apply the new styles.

If this sounds complicated, hopefully, this flowchart better visualises the process.

Writing Our Controller

Arguably the most technical part of the article, let’s wire up our controller!

Let’s begin by completing the initialisation logic of our controller:

export class DragController {
    x = 0;
    y = 0;
    state = "idle";

  styles = {
    position: "absolute",
    top: "0px",
    left: "0px",
  };

  constructor(host, options) {
        const {
      getContainerEl = () => null,
      getDraggableEl = () => Promise.resolve(null),
    } = options;

    this.host = host;
    this.host.addController(this);
    this.getContainerEl = getContainerEl;

    getDraggableEl().then((el) => {
      if (!el) return;

      this.draggableEl = el;
      this.init();
    });
  }

    init() {...}

  hostDisconnected() {...}

  onDragStart = (pointer) => {...};

  onDrag = (_, pointers) => {...};
}

The main difference between this snippet and the skeleton from earlier is the addition of the options argument. We allow our host element to provide callbacks that give us access to two different elements: the container and the draggable element. We’ll use these elements later on to calculate the correct position styles.

For reasons I’ll touch on later, getDraggableEl is a promise that returns the draggable element. Once the promise resolves, we store the element on the controller instance, and we’ll fire off the initialize function, which attaches the drag event listeners to the draggable element.

init() {
  this.pointerTracker = new PointerTracker(this.draggableEl, {
    start: (...args) => {
      this.onDragStart(...args);
      this.state = "dragging";
      this.host.requestUpdate();
      return true;
    },
    move: (...args) => {
      this.onDrag(...args);
    },
    end: (...args) => {
      this.state = "idle";
      this.host.requestUpdate();
    },
  });
}

We’ll use the PointerTracker library to track pointer events easily. It’s much more pleasant to use this library than to write the cross-browser, cross-input mode logic to support pointer events.

PointerTracker requires two arguments, draggableEl, and an object of functions that act as the event handlers for the dragging events:

  • start: gets invoked when the pointer is pressed down on draggableEl;
  • move: gets invoked when dragging draggableEl around;
  • end: gets invoked when we release the pointer from draggableEl.

For each, we’re either updating the dragging state, invoking our controller’s callback, or both. Our host element will use the state property as an element attribute, so we trigger this.host.requestUpdate to ensure the host re-renders.

Like with the draggableEl, we assign a reference to the pointerTracker instance to our controller to use later.

Next, let’s start adding logic to the class’s functions. We’ll start with the onDragStart function:

onDragStart = (pointer, ev) => {
  this.cursorPositionX = Math.floor(pointer.pageX);
  this.cursorPositionY = Math.floor(pointer.pageY);
};

Here we’re storing the cursor’s current position, which we’ll use in the onDrag function.

onDrag = (_, pointers) => {
    this.calculateWindowPosition(pointers[0]);
};

When the onDrag function is called, it’s provided a list of the active pointers. Since we’ll only cater for one window being dragged at a time, we can safely just access the first item in the array. We’ll then send that through to a function that determines the new position of the element. Strap in because it’s a little wild:

calculateWindowPosition(pointer) {
  const el = this.draggableEl;
  const containerEl = this.getContainerEl();

  if (!el || !containerEl) return;

  const oldX = this.x;
  const oldY = this.y;

  //JavaScript’s floats can be weird, so we’re flooring these to integers.
  const parsedTop = Math.floor(pointer.pageX);
  const parsedLeft = Math.floor(pointer.pageY);

  //JavaScript’s floats can be weird, so we’re flooring these to integers.
  const cursorPositionX = Math.floor(pointer.pageX);
  const cursorPositionY = Math.floor(pointer.pageY);

  const hasCursorMoved =
    cursorPositionX !== this.cursorPositionX ||
    cursorPositionY !== this.cursorPositionY;

  // We only need to calculate the window position if the cursor position has changed.
  if (hasCursorMoved) {
    const { bottom, height } = el.getBoundingClientRect();
    const { right, width } = containerEl.getBoundingClientRect();

    // The difference between the cursor’s previous position and its current position.
    const xDelta = cursorPositionX - this.cursorPositionX;
    const yDelta = cursorPositionY - this.cursorPositionY;

    // The happy path - if the element doesn’t attempt to go beyond the browser’s boundaries.
    this.x = oldX + xDelta;
    this.y = oldY + yDelta;

    const outOfBoundsTop = this.y < 0;
    const outOfBoundsLeft = this.x < 0;
    const outOfBoundsBottom = bottom + yDelta > window.innerHeight;
    const outOfBoundsRight = right + xDelta >= window.innerWidth;

    const isOutOfBounds =
      outOfBoundsBottom ||
      outOfBoundsLeft ||
      outOfBoundsRight ||
      outOfBoundsTop;

    // Set the cursor positions for the next time this function is invoked.
    this.cursorPositionX = cursorPositionX;
    this.cursorPositionY = cursorPositionY;

    // Otherwise, we force the window to remain within the browser window.
    if (outOfBoundsTop) {
      this.y = 0;
    } else if (outOfBoundsLeft) {
      this.x = 0;
    } else if (outOfBoundsBottom) {
      this.y = window.innerHeight - height;
    } else if (outOfBoundsRight) {
      this.x = Math.floor(window.innerWidth - width);
    }

    this.updateElPosition();
    // We trigger a lifecycle update.
    this.host.requestUpdate();
  }
}

updateElPosition(x, y) {
    this.styles.transform = translate(${this.x}px, ${this.y}px);
}

It’s certainly not the prettiest code, so I’ve tried my best to annotate the code to clarify what’s going on.

To summarize:

  • When the function gets invoked, we check to see that both the draggableEl and containerEl are available.
  • We then access the element’s position and the cursor’s position.
  • We then calculate whether the cursor’s moved. If it hasn’t, we do nothing.
  • We set the new x and y position of the element.
  • We determine whether or not the element tries to break the window’s bounds.
    • If it does, then we update the x or y position to bring the element back within the confines of the window.
  • We update this.styles with the new x and y values.
  • We then trigger the host’s update lifecycle function, which causes our element to apply the styles.

Review the function several times to ensure you’re confident about what it does. There’s a lot going on, so don’t sweat if it doesn’t soak in straight away.

The updateElPosition function is a small helper in the class to apply the styles to the styles property.

We also need to add a little clean-up to ensure that we stop tracking if our component happens to disconnect while being dragged.

hostDisconnected() {
  if (this.pointerTracker) {
    this.pointerTracker.stop();
  }
}

Finally, we need to jump back to our a2k-window.js file and do three things:

  • initialize the controller,
  • apply the position styles,
  • track the drag state.

Here’s what these changes look like:

class A2kWindow extends LitElement {
  static styles = css`...`;

  static properties = {...};

  constructor() {...}

  drag = new DragController(this, {
    getContainerEl: () => this.shadowRoot.querySelector("#window"),
        getDraggableEl: () => this.getDraggableEl(),
  });

    async getDraggableEl() {
        await this.updateComplete;
        return this.shadowRoot.querySelector("#draggable");
    }

  render() {
    return html`
      <div id="window" style=${styleMap(this.drag.styles)}>
        <div id="panel">
          <div id="draggable" data-dragging=${this.drag.state}>
            <p>${this.heading}</p>
          </div>
          <slot></slot>
        </div>
      </div>
    `;
  }
}

We’re using this.shadowRoot.querySelector(selector) to query our shadow DOM. This allows us controller to access DOM elements across shadow DOM boundaries.

Because we plan to dispatch events from our dragging element, we should wait until after rendering has completed, hence the await this.updateComplete statement.

Once this is all completed, you should be able to jump back into the browser and drag your component around, like so:

(Large preview) Part 3: Creating The Broken Window Effect

Our component is pretty self-contained, which is great. We could use this window element anywhere on our site and drag it without writing any additional code.

And since we’ve created a reusable controller to handle all of the drag functionality, we can add that behavior to future components like a desktop icon.

Now let’s start building out that cool broken window effect when we drag our component.

We could bake this behavior into the window element itself, but it’s not really useful outside of a specific use case, i.e., making a cool visual effect. Instead, we can get our drag controller to emit an event whenever the onDrag callback is invoked. This means that anyone using our component can listen to the drag event and do whatever they want.

To create the broken window effect, we’ll need to do two things:

  • dispatch and listen to the drag event;
  • add the broken window element to the DOM.

Dispatching and listening to events in Lit

Lit has a handful of different ways to handle events. You can add event listeners directly within your templates, like so:

handleClick() {
    console.log("Clicked");
}

render() {
    html`<button @click="${this.handleClick}">Click me!</button>`
}

We’re defining the function that we want to fire on button click and passing it through to the element which will be invoked on click. This is a perfectly viable option, and it’s the approach I’d use if the element and callback are located close together.

As I mentioned earlier, we won’t be baking the broken window behavior into the component, as passing down event handlers through a number of different web components would become cumbersome. Instead, we can leverage the native window event object to have a component dispatch an event and have any of its ancestors listen and respond. Have a look at the following example:

// Event Listener
class SpecialListener extends LitElement {
    constructor() {
        super()

        this.specialLevel = '';
        this.addEventListener('special-click', this.handleSpecialClick)
    }

    handleSpecialClick(e) {
        this.specialLevel = e.detail.specialLevel;
    }

    render() {
        html`<div>
            <p>${this.specialLevel}</p>
            <special-button>
        </div>`
    }
}

// Event Dispatcher
class SpecialButton extends LitElement {
    handleClick() {
        const event = new CustomEvent("special-click", {
      bubbles: true,
      composed: true,
      detail: {
                specialLevel: 'high',
            },
    });

        this.dispatchEvent(event);
    }

    render() {
        html`<button @click="${this.handleClick}">Click me!</button>`
    }
}

Note: Don’t forget to check out the MDN resources if you need a refresher on native DOM Events.

We have two components, a listener and a dispatcher. The listener is a component that adds an event listener to itself. It listens to the special-click event and outputs the value the event sends through.

Our second component, SpecialButton, is a descendant of SpecialListener. It’s a component that dispatches an event on click. The code inside of the handleClick method is interesting, so let’s understand what’s going on here:

  • We create an event object by creating an instance of CustomEvent.
  • The first argument of CustomEvent is the name of the event we want to dispatch. In our case, it’s special-click.
  • The second argument of CustomEvent is the options argument. Here we’re setting three options: bubbles, composed, and detail.
  • Setting bubbles to true allows our event to flow up the DOM tree to the component’s ancestors.
  • Setting composed to true allows our event to propagate outside our element’s shadow root.
  • Finally, we dispatch our event by firing off this.dispatchEvent(event).

Once this happens, the listener will react to the event by invoking the handleSpecialClick callback.

Let’s go ahead and dispatch events from our drag controller. We’ll want to create an instance of CustomEvent with an event name of window-drag. We’ll want to set the composed and bubbles options to true.

We’ll then create the detail option with a single property: containerEl. Finally, we’ll want to dispatch the event.

Go ahead and try to implement this logic inside of the onDrag function.

Hint: We’ll want to dispatch the event from our dragging element. Don’t forget that we saved a reference to the element on the controller’s instance.

Before I go ahead and spoil the answer, let’s get our listener set up. That way, we’ll be able to determine whether we’ve wired up our event dispatcher correctly.

Jump into the script.js file and add the following lines:

function onWindowDrag() {
    console.log('dragging');
}

window.addEventListener('window-drag', onWindowDrag);

You can now jump into your browser, drag your element, and view the logs in the console.

You can check your solution against mine below:

onDrag = (_, pointers) => {
  this.calculateWindowPosition(pointers[0]);

    const event = new CustomEvent("window-drag", {
      bubbles: true,
      composed: true,
      detail: {
        containerEl: this.getContainerEl(),
      },
    });

  this.draggableEl.dispatchEvent(event);
};

Great! The only thing left to do is add the broken window element to the DOM every time we receive a drag event.

We’ll need to create a new broken window component that looks like the following:

Our broken window should look a little more than our regular window without any content. The markup for the component is going to be very straightforward. We’ll have nested divs, each responsible for different aspects of the element:

  • The outer-most div will be responsible for positioning.
  • The middle div will be responsible for appearance.
  • The inner-most div will be responsible for width and height.

Here’s the entire code for our broken window. Hopefully, by this point, nothing in the snippet below should be new to you:

export class BrokenWindow extends LitElement {
  static properties = {
    height: {},
    width: {},
    top: {},
    left: {},
  };

  static styles = css`
    #outer-container {
      position: absolute;
      display: flex;
    }

    #middle-container {
      border: var(--border-width) solid var(--color-gray-400);
      box-shadow: 2px 2px var(--color-black);
      background-color: var(--color-gray-500);
    }
  `;

  render() {
    return html`
      <div
        style=${styleMap({
          transform: `translate(${this.left}px, ${this.top}px)`,
        })}
        id="outer-container"
      >
        <div id="middle-container">
          <div
            style=${styleMap({
              width: `${this.width}px`,
              height: `${this.height}px`,
            })}
          ></div>
        </div>
      </div>
    `;
  }
}

window.customElements.define("a2k-broken-window", BrokenWindow);

Once you’ve created the component, we can check that it’s working correctly by adding the following to our index.html file:

<a2k-broken-window top="100" left="100" width="100" height="100"></a2k-broken-window>

If you see the following in your browser, then congratulations! Your broken window is working perfectly.

Bonus

You may have noticed that both our a2k-window component and our a2k-broken-window component share a lot of the same styles. We can leverage one of Lit’s composition techniques to abstract out the repeated markup and styles into a separate component, a2k-panel. Once we’ve done that, we can reuse a2k-panel in our window components.

I won’t give away the answer here, but if you want to give it a shot, the Lit documentation will help if you get stuck.

Rendering Our Broken Window On Drag

We’re at the last stop on our retro web component journey.

To create our broken window effect, we only need to do a handful of things:

  • Listen to the window-drag event;
  • Get access to the container’s styles;
  • Create a new a2k-broken-window element;
  • Set the top, left, height, width attributes to our new element;
  • Insert the broken window into the DOM.

Let’s jump into our script.js file:

function onWindowDrag(e) {
    ...
}

window.addEventListener("window-drag", onWindowDrag);

We’re listening to the window-drag event and setting up a callback that receives the event object when invoked.

function onWindowDrag(e) {
    const { containerEl } = e.detail;
  const { width, top, left, height } = containerEl.getBoundingClientRect();
}

window.addEventListener("window-drag", onWindowDrag);

The above bit of code is doing two things:

  • Accessing the containerEl from the detail object.
  • We’re then using the containerEl’s getBoundingClientRect function to get the element’s CSS properties.
function onWindowDrag(e) {
  const { containerEl } = e.detail;
  const { width, top, left, height } = containerEl.getBoundingClientRect();

  const newEl = document.createElement("a2k-broken-window");

  newEl.setAttribute("width", width);
  newEl.setAttribute("top", top);
  newEl.setAttribute("left", left);
  newEl.setAttribute("height", height);
}

Here we’re imperatively creating our broken window element and applying our styles. For anyone familiar with writing HTML with JavaScript (or even jQuery), this shouldn’t be a foreign concept. Now we’ll add our component to the DOM.

We need to be very specific about where we want to place the element. We can’t just append it to the body; otherwise, it’ll cover our main window element.

We also can’t write it as the first element of body; otherwise, the oldest window will appear above the newer windows.

One solution is to add our component into the DOM just before our container element. All the JavaScript devs out there might be eager to write their own script to manage this but luckily the window has the perfect function for us:

containerEl.insertAdjacentElement("beforebegin", newEl);

The above is a very handy function that gives us control over where an element gets added. This script inserts our new element before our container element.

Our finished script looks like this:

function onWindowDrag(e) {
  const { containerEl } = e.detail;
  const { width, top, left, height } = containerEl.getBoundingClientRect();

  const newEl = document.createElement("a2k-broken-window");

  newEl.setAttribute("width", width);
  newEl.setAttribute("top", top);
  newEl.setAttribute("left", left);
  newEl.setAttribute("height", height);

  containerEl.insertAdjacentElement("beforebegin", newEl);
}

window.addEventListener("window-drag", onWindowDrag);

Jump back to the browser and start dragging your window. You should now be seeing your cool window effect!

If your script isn’t working, then don’t worry! Open up your console and see if you can debug the problem(s). You can even run through the code snippets above and ensure everything’s been copied correctly.

Bonus

We’ve made a cool draggable effect by listening to the drag events and writing some custom logic inside the handlers.

But Microsoft did this 20 years ago. I’d love to see what cool effects the creative Smashing community can whip up instead! Here’s me having a little fun:

(Large preview)

Please bombard my Twitter with what you’ve created using this article. 😄

Conclusion

Thanks for making it to the end! We covered a lot of ground. I hope it’s helped you get comfortable writing web components with the wonderful Lit library. Most importantly, I hope you’ve enjoyed joining me in building something fun.

The draggable window is part of my web component UI library, A2k, which you can use in your own projects. You can give it a whirl by heading over to the GitHub repo.

If you’d like to support the project, you can follow me on Twitter for updates or leave the repo a GitHub star.

I would also love to offer a shout-out to Elliott Marquez, Lit Developer at Google, for being a technical reviewer.

Migration From jQuery to Next.js: A Guide

jQuery has served developers well for many years. However, libraries (like React) and Frameworks (like Next.js) are now bringing us more modern features to help with our code's performance and maintainability. This guide will show you how to rewrite your jQuery site using Next.js to take advantage of all these new features, such as client-side routing for smoother transitions and the ability to separate code into components to make it more reusable.

Getting started

The easiest way to get started with a Next.js is to run npx create-next-app. This will scaffold a project for you. However, to understand what this command does, we’ll create our application from scratch.

First, we’ll create our Next.js project using npm init. You can proceed with the default settings, as we will change them later. Then, we want to install React and Next.js using:

npm install react react-dom next

Next up, we can open the package.json file and replace the default scripts with:

"scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
}

This allows you to run npm run dev to start the development server; npm run build to build your application; and npm run start to start a server of that built application.

To add pages — like you would index.html with jQuery — create a directory named pages and create a file named index.jsx in it. Inside this file, place the following code:

export default function Index() {
  return <h1>Hello World</h1> ;
}

Now, by running npm run start and navigating to localhost:3000, you should see a h1 tag displayed. The name of this function isn’t important, so you can call it whatever you want. However, don’t use an anonymous arrow function, as this will prevent fast refresh from working.

CSS

In jQuery, you can specify CSS by page, importing different stylesheets for different pages. This is also possible in Next.js using the next/head component and a link tag the same way as jQuery. Anyhow, there are more performance-friendly ways to to this in Next.js.

Global Stylesheet

The first way is with a global stylesheet. To do so, we need to create a custom App by making the file _app.js inside the pages directory. The starting point for this file is as follows:

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

At the top of this file, you can add an import statement and import any CSS file you want. For example, if you created a separate folder at the root level called styles and put main.css in it, then you would add:

import "../styles/main.css"

Now, whatever you put inside this file will be applied throughout your application.

CSS Modules

The next option is CSS modules — which allows you to specify CSS anywhere in your application. They will create unique class names from the classes you provide, so you can use a same class name in multiple places in your application’s code.

Expanding the initial hello world example, you could create a file index.module.css in the same directory and then write the import:

import styles from "./index.module.css"

Afterwards, if you were to define a heading class in the CSS file, you could do the following:

export default function Index() {
  return <h1 className={styles.heading}>Hello World</h1> ;
}

and those styles will be applied only to that element.

Styled JSX

The final built-in option is styled JSX. This is most similar to including a <style> tag at the top of your page to define some styles. Simply add jsx to the <style> tag, and use a template string inside, like this:

<style jsx>{`
  .heading {
      font-weight: 700
  `}</style>

This option has the advantage of being changeable at runtime. For instance, if you wanted to supply the font weight in your component props, you could do:

<style jsx>{`
  .heading{
      font-weight: ${props.fontWeight}
  `}</style>

The one disadvantage of this method is that it introduces additional runtime JavaScript into your application, increasing the size by 12kb (3kb gzipped).

Events

In jQuery, you might have events set up to respond to DOM elements. To give you an idea, you might want to execute code when a p tag is clicked and do so like this:

$( "p" ).click(function() {
    console.log( "You clicked a paragraph!" );
});

Instead, React uses event handlers — which you might have seen in HTML — like onclick. Note that React uses camelCase instead, and so onclick should be referenced as onClick. Therefore, rewriting this small example into React would look like this:

export default function Index() {
  function clickParagraph(){
    console.log("You clicked a paragraph!");
  }
  return <p onClick={clickParagraph}>Hello World</p>;
}

Each method comes with its advantages and disadvantages. In jQuery, it is easy to have something happen for all paragraphs, whereas in React, you have to specify per paragraph. However, for larger codebases, having to specify makes it easy to see what will happen with the interaction with any element, where you may have forgotten about the jQuery function.

Effects

Effects are used in jQuery to show and hide content. You might have something like this already:

$( "p" ).hide();

In React, this behavior is implemented using conditional rendering. You can see this by combining it with the replacement for events we just saw:

import {useState} from "react"
export default function Index() {
  const [show, setShow] = useState(true);
  function clickButton(){
    setShow(false)
  }
  return (
    <div>
      <h1>Hello world</h1>
      {show && <button onClick={clickButton}>Click me</button>}
    </div>
  )
}

When you click this button, it will change the value of show to false and so, the statement won’t render anything. This can be expanded with the conditional operator to show one thing or another, depending on the value like this:

show ? <p>Show this if show is true</p> : <p>Show this if show is false</p>
Data Fetching

In jQuery, Ajax is used for external data fetching without reloading. In React, this can be done by using the useEffect hook. For this example, we’ll fetch the exchange rate from a public API when the page loads:

import { useState, useEffect } from "react";
export default function Index() {
  const [er, setEr] = useState(true);
  useEffect(async () => {
    const result = await fetch("https://api.exchangerate.host/latest");
    const exchangerate = await result.json();
    setEr(exchangerate.rates["GBP"]);
  }, []);
  return (
    <div>
      <h1>Hello world</h1>
      <p>Exchange rate: {er}</p>
    </div> 
  );
}

useEffect takes in a function and a dependency array. The function does the data fetching, using async as the fetch API asynchronously. We can then set any state we want in there, and it will be updated on the page. The dependency array determines which value changes will run the function. In this case, it’s set to an empty array which means that the function will only run when the page first loads.

Beyond this, Next.js also provides options for fetching data on the server or at build time. For build time data fetching, the function getStaticProps can be used. This function provides an improvement in performance as the data can be provided with the page — rather than waiting on an external service. To use it, create this function in a page as it doesn’t work in components.

export async function getStaticProps() {
  return {
    props: {},
  }
}

You can perform any data fetching you want before the return, and after that, pass the data through to the page under props — then, the data is provided to the page and can be accessed under the props.

By replacing the function name from getStaticProps to getServerSideProps, the function will be called on every request, giving you the flexibility to use Node.js functions if needed. It also allows you to make many data requests on the server and to process them to reduce the bandwidth used by the client.

You also have the option of a middle ground between the two called Incremental Static Regeneration. This option will generate a static page in the same way as getStaticProps, but it allows you to specify a revalidation period — which will regenerate the page when a request comes in at most as often as the period you specify. To do this, alongside props, you should also include a revalidate key with the time in seconds you want.

Objects into DOM elements

With jQuery, you have to be careful with which method you use for turning an object into DOM elements. The most common example of this is to create a list of items because, with jQuery, a loop over items would add each to the DOM one by one. With React, the virtual DOM is used to create diffs of the new state from the current one. This means that despite adding items in a loop, they are added to the real DOM as one operation.

This is done using the map function in JavaScript, where you can map each item to some JSX.

export default function Index() {
  const fruits = ["Apple", "Orange", "Pear"];
  return (
    <div>
      <h1>Hello world</h1>
      <ul>
        {fruits.map((fruit) => (
          <li key={fruit}>{fruit}</li>
        ))}
      </ul>
    </div>
  );
}

Notice that the element inside the map needs a key prop. This is used in the diffing process discussed above, making it easy for React to distinguish between each element, so each of these should be unique.

Deffereds

The use of deferreds in jQuery can be replaced with the native JavaScript promise functionality. The syntax for deffereds was designed to mirror the functionality of promises, so the syntax should be familiar and not require too much alteration. One example of where deffereds might be used is in data fetching. If you do this with the fetch method in JavaScript, then you can add a .then to the end of the fetch as it returns a promise. This code will only run when the fetch is completed, and so the data (or an error) will be present. You can see this functionality in use here:

fetch("example.com")
.then((response) => {
  console.log(response)
})
.catch((error) => {
console.error(error)
})

This will fetch example.com and log the fetched response unless an error occurs — in this case it will be logged as an error.

In addition to this syntax, the newer async/await syntax can also be used. These require a function defined as a`sync`, in the same way as you might export a function. You can declare it like so:

async function myFunction(){
  return
}

Inside this function, you can call further async functions by placing await in front of them, for example:

async function myFunction(){
  const data = await fetch("example.com")
  return data
}

This code will return a promise that will resolve when the data is fetched, so it needs to be called inside an asynchronous function to await the result. However, in order to also catch errors, you will need to write a conditional to check the response status — if data.ok isn’t true, an error should be thrown. Then, you can wrap these away statements in a try catch block, rather than using .catch. You can read more about these methods in this article.

Improvements

Routing

Next.js uses file system routing, which is very similar to using different .html pages in a traditional website. However, this system also offers features beyond that, providing dynamic routes and allowing one page to be accessed under a range of urls.

For example, if you have a blog, you might keep all your files under /blog/*, creating a file [slug].jsx inside the blog folder — which will allow that content to be served for all pages under blog. Then, you can use the router in Next.js to find which route has been navigated to, like so:

const router = useRouter()
const { slug } = router.query

API routes

API routes allow you to also write your backend inside your Next.js application. To use these routes, create an api folder in your pages directory — now, any files created inside it will run on the server rather than the client, as with the rest of the pages.

To get started with these, you need to export a default function from the file, and this can take two parameters. The first will be the incoming request, and the second will let you create the response. A basic API route can be written like this:

export default function handler(request, response) {
  response.status(200).json({ magazine: 'Smashing' })
}
Limitations

jQuery UI

You may use jQuery UI in your application for user interface, but React doesn’t provide an official UI library like this. Nevertheless, a range of alternatives has been produced. Two of the most popular are Reach UI and React Aria. Both of these alternatives focus very strongly on Accessibility, ensuring that the project you create is usable by a bigger range of users.

Animation

While you can use conditional rendering instead of effects, this doesn’t provide all the same functionality, as you can’t do things such as fading content out. One library that helps to provide this functionality is React Transition Group — which allows you to define entering and exiting transitions.

Conclusion

Moving from jQuery to Next.js is a large undertaking, especially for big code bases. However, this migration allows you to use newer concepts (such as data fetching at build time) and sets you up to have simple migration paths to new versions of React and Next.js — along with the features they bring.

React can help you better organize your code (which is particularly important for large codebases) and brings a substantial performance improvement through the use of a virtual DOM. Overall, I believe that migrating from jQuery to Next.js is worth the effort, and I hope that if you decide to migrate, you enjoy all the features React and Next.js have to offer.

Further Reading on Smashing Magazine