> All in One 586: March 2025

Ads

Monday, March 31, 2025

Partly Cloudy/Wind today!



With a high of F and a low of 36F. Currently, it's 45F and Clear/Wind outside.

Current wind speeds: 24 from the Southeast

Pollen: 0

Sunrise: March 31, 2025 at 06:36PM

Sunset: April 1, 2025 at 07:15AM

UV index: 0

Humidity: 73%

via https://ift.tt/ZjifLR2

April 1, 2025 at 10:02AM

Worlds Collide: Keyframe Collision Detection Using Style Queries

A friend DMs Lee Meyer a CodePen by Manuel Schaller containing a pure CSS simulation of one of the world’s earliest arcade games, Pong, with both paddles participating automatically, in an endless loop. The demo reminds Lee of an arcade machine in attract mode awaiting a coin, and the iconic imagery awakens muscle memory from his misspent childhood, causing him to search his pocket in which he finds the token a spooky shopkeeper gave him last year at the CSS tricks stall in the haunted carnival. The token gleams like a power-up in the light of his laptop, which has a slot he never noticed. He feeds the token into the slot, and the CodePen reloads itself. A vertical range input and a life counter appear, allowing him to control the left paddle and play the game in Chrome using a cocktail of modern and experimental CSS features to implement collision detection in CSS animations. He recalls the spooky shopkeeper’s warning that playing with these features has driven some developers to madness, but the shopkeeper’s voice in Lee’s head whispers: “Too late, we are already playing.”

CSS collision detection: Past and present

So, maybe the experience of using modern CSS to add collision detection and interactivity to an animation wasn’t as much like a screenplay sponsored by CSS as I depicted in the intro above — but it did feel like magic compared to what Alex Walker had to go through in 2013 to achieve a similar effect. Hilariously, he describes his implementation as “a glittering city of hacks built on the banks of the ol’ Hack River. On the Planet Hack.“ Alex’s version of CSS Pong cleverly combines checkbox hacks, sibling selectors, and :hover, whereas the CodePen below uses style queries to detect collisions. I feel it’s a nice illustration of how far CSS has come, and a testament to increased power and expressiveness of CSS more than a decade later. It shows how much power we get when combining new CSS features — in this case, that includes style queries, animatable custom properties, and animation timelines. The future CSS features of inline conditionals and custom functions might be able to simplify this code more.

Collision detection with style queries

Interactive CSS animations with elements ricocheting off each other seems more plausible in 2025 and the code is somewhat sensible. While it’s unnecessary to implement Pong in CSS, and the CSS Working Group probably hasn’t been contemplating how to make that particular niche task easier, the increasing flexibility and power of CSS reinforce my suspicion that one day it will be a lifestyle choice whether to achieve any given effect with scripting or CSS.

The demo is a similar number of lines of CSS to Alex’s 2013 implementation, but it didn’t feel much like a hack. It’s a demo of modern CSS features working together in the way I expected after reading the instruction booklet. Sometimes when reading introductory articles about the new features we are getting in CSS, it’s hard to appreciate how game-changing they are till you see several features working together. As often happens when pushing the boundaries of a technology, we are going to bump up against the current limitations of style queries and animations. But it’s all in good fun, and we’ll learn about these CSS features in more detail than if we had not attempted this crazy experiment.

It does seem to work, and my 12-year-old and 7-year-old have both playtested it on my phone and laptop, so it gets the “works on Lee’s devices” seal of quality. Also, since Chrome now supports controlling animations using range inputs, we can make our game playable on mobile, unlike the 2013 version, which relied on :hover. Temani Afif provides a great explanation of how and why view progress timelines can be used to style anything based on the value of a range input.

Using style queries to detect if the paddle hit the ball

The ball follows a fixed path, and whether the player’s paddle intersects with the ball when it reaches our side is the only input we have into whether it continues its predetermined bouncy loop or the screen flashes red as the life counter goes down till we see the “Game Over” screen with the option to play again.

This type of interactivity is what game designers call a quick time event. It’s still a game for sure, but five months ago, when I was young and naive, I mused in my article on animation timelines that the animation timeline feature could open the door for advanced games and interactive experiences in CSS. I wrote that a video game is just a “hyper-interactive animation.” Indeed, the above experiment shows that the new features in CSS allow us to respond to user input in sophisticated ways, but the demo also clarifies the difference between the kind of interactivity we can expect from the current incarnation of CSS versus scripting. The above experiment is more like if Pong were a game inside the old-school arcade game Dragon’s Lair, which was one giant quick time event. It only works because there are limited possible outcomes, but they are certainly less limited than what we used to be able to achieve in CSS.

Since we know collision detection with the paddle is the only opportunity for the user to have a say in what happens next, let’s focus on that implementation. It will require more mental gymnastics than I would like, since container style queries only allow for name-value pairs with the same syntax as feature queries, meaning we can’t use “greater than” or “less than” operators when comparing numeric values like we do with container size queries which follow the same syntax as @media size queries.

The workaround below allows us to create style queries based on the ball position being in or out of the range of the paddle. If the ball hits our side, then by default, the play field will flash red and temporarily unpause the animation that decrements the life counter (more on that later). But if the ball hits our side and is within range of the paddle, we leave the life-decrementing animation paused, and make the field background green while the ball hits the paddle. Since we don’t have “greater than” or “less than” operators in style queries, we (ab)use the min() function. If the result equals the first argument then that argument is less than or equal to the second; otherwise it’s greater than the second argument. It’s logical but made me wish for better comparison operators in style queries. Nevertheless, I was impressed that style queries allow the collision detection to be fairly readable, if a little more verbose than I would like.

body {
  --int-ball-position-x: round(down, var(--ball-position-x));
  --min-ball-position-y-and-top-of-paddle: min(var(--ball-position-y) + var(--ball-height), var(--ping-position));
  --min-ball-position-y-and-bottom-of-paddle: min(var(--ball-position-y), var(--ping-position) + var(--paddle-height));
}

@container style(--int-ball-position-x: var(--ball-left-boundary)) {
  .screen {
    --lives-decrement: running;
      
    .field {
      background: red;
    }
  }
}

@container style(--min-ball-position-y-and-top-of-paddle: var(--ping-position)) and style(--min-ball-position-y-and-bottom-of-paddle: var(--ball-position-y)) and style(--int-ball-position-x: var(--ball-left-boundary)) {
  .screen {
    --lives-decrement: paused;

    .field {
      background: green;
    }
  }
}

Responding to collisions

Now that we can style our playing field based on whether the paddle hits the ball, we want to decrement the life counter if our paddle misses the ball, and display “Game Over” when we run out of lives. One way to achieve side effects in CSS is by pausing and unpausing keyframe animations that run forwards. These days, we can style things based on custom properties, which we can set in animations. Using this fact, we can take the power of paused animations to another level.


body {
  animation: ball 8s infinite linear, lives 80ms forwards steps(4) var(--lives-decrement);
  --lives-decrement: paused;        
}

.lives::after {
   content: var(--lives);
}

@keyframes lives {
  0% {
    --lives: "3";
  }
  25% {
    --lives: "2";
  }
  75% {
    --lives: "1";
  }
  100% {
    --lives: "0";
  }
}

@container style(--int-ball-position-x: var(--ball-left-boundary)) {
  .screen {
    --lives-decrement: running;
      
    .field {
      background: red;
    }
  }
}

@container style(--min-ball-position-y-and-top-of-paddle: var(--ping-position)) and style(--min-ball-position-y-and-bottom-of-paddle: var(--ball-position-y)) and style(--int-ball-position-x: 8) {
  .screen {
    --lives-decrement: paused;
    
    .field {
      background: green;
    }
  }
}

@container style(--lives: '0') {
  .field {
     display: none;
  }
  
  .game-over {
     display: flex;
  }
}

So when the ball hits the wall and isn’t in range of the paddle, the lives-decrementing animation is unpaused long enough to let it complete one step. Once it reaches zero we hide the play field and display the “Game Over” screen. What’s fascinating about this part of the experiment is that it shows that, using style queries, all properties become indirectly possible to control via animations, even when working with non-animatable properties. And this applies to properties that control whether other animations play. This article touches on why play state deliberately isn’t animatable and could be dangerous to animate, but we know what we are doing, right?

Full disclosure: The play state approach did lead to hidden complexity in the choice of duration of the animations. I knew that if I chose too long a duration for the life-decrementing counter, it might not have time to proceed to the next step while the ball was hitting the wall, but if I chose too short a duration, missing the ball once might cause the player to lose more than one life.

I made educated guesses of suitable durations for the ball bouncing and life decrementing, and I expected that when working with fixed-duration predictable animations, the life counter would either always work or always fail. I didn’t expect that my first attempt at the implementation intermittently failed to decrement the life counter at the same point in the animation loop. Setting the durations of both these related animations to multiples of eight seems to fix the problem, but why would predetermined animations exhibit unpredictable behavior?

Forefeit the game before somebody else takes you out of the frame

I have theories as to why the unpredictability of the collision detection seemed to be fixed by setting the ball animation to eight seconds and the lives animation to 80 milliseconds. Again, pushing CSS to its limits forces us to think deeper about how it’s working.

  1. CSS appears to suffer from timer drift, meaning if you set a keyframes animation to last for one second, it will sometimes take slightly under or over one second. When there is a different rate of change between the ball-bouncing and life-losing, it would make sense that the potential discrepancy between the two would be pronounced and lead to unpredictable collision detection. When the rate of change in both animations is the same, they would suffer about equally from timer drift, meaning the frames still synchronize predictably. Or at least I’m hoping the chance they don’t becomes negligible.
  2. Alex’s 2013 version of Pong uses translate3d() to move the ball even though it only moves in 2D. Alex recommends this whenever possible “for efficient animation rendering, offloading processing to the GPU for smoother visual effects.” Doing this may have been an alternative fix if it leads to more precise animation timing. There are tradeoffs so I wasn’t willing to go down that rabbit hole of trying to tune the animation performance in this article — but it could be an interesting focus for future research into CSS collision detection.
  3. Maybe style queries take a varying amount of time to kick in, leading to some form of a race condition. It is possible that making the ball-bouncing animation slower made this problem less likely.
  4. Maybe the bug remains lurking in the shadows somewhere. What did I expect from a hack I achieved using a magic token from a spooky shopkeeper? Haven’t I seen any eighties movie ever?

Outro

You finish reading the article, and feel sure that the author’s rationale for his supposed fix for the bug is hogwash. Clearly, Lee has been driven insane by the allure of overpowering new CSS features, whereas you respect the power of CSS, but you also respect its limitations. You sit down to spend a few minutes with the collision detection CodePen to prove it is still broken, but then find other flaws in the collision detection, and you commence work on a fork that will be superior. Hey, speaking of timer drift, how is it suddenly 1 a.m.? Only a crazy person would stay up that late playing with CSS when they have to work the next day. “Madness,” repeats the spooky shopkeeper inside your head, and his laughter echoes somewhere in the night.

Roll the credits

This looping Pong CSS animation by Manuel Schaller gave me an amazing basis for adding the collision detection. His twitching paddle animations help give the illusion of playing against a computer opponent, so forking his CodePen let me focus on implementing the collision detection rather than reinventing Pong.

This author is grateful to the junior testing team, comprised of his seven-year-old and twelve-year-old, who declared the CSS Pong implementation “pretty cool.” They also suggested the green and red flashes to signal collisions and misses.

The intro and outro for this article were sponsored by the spooky shopkeeper who sells dangerous CSS tricks. He also sells frozen yoghurt, which he calls froghurt.


Worlds Collide: Keyframe Collision Detection Using Style Queries originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/qhlNAVw
via IFTTT

Sunday, March 30, 2025

Rain Early today!



With a high of F and a low of 31F. Currently, it's 38F and Cloudy outside.

Current wind speeds: 10 from the Northeast

Pollen: 0

Sunrise: March 30, 2025 at 06:38PM

Sunset: March 31, 2025 at 07:14AM

UV index: 0

Humidity: 89%

via https://ift.tt/QzRewyU

March 31, 2025 at 10:02AM

Saturday, March 29, 2025

Snow today!



With a high of F and a low of 27F. Currently, it's 36F and Rain Shower outside.

Current wind speeds: 14 from the Northeast

Pollen: 0

Sunrise: March 29, 2025 at 06:39PM

Sunset: March 30, 2025 at 07:13AM

UV index: 0

Humidity: 91%

via https://ift.tt/LeCfJ4Z

March 30, 2025 at 10:02AM

Friday, March 28, 2025

Mostly Cloudy today!



With a high of F and a low of 40F. Currently, it's 59F and Clear outside.

Current wind speeds: 7 from the Northeast

Pollen: 0

Sunrise: March 28, 2025 at 06:41PM

Sunset: March 29, 2025 at 07:12AM

UV index: 0

Humidity: 28%

via https://ift.tt/mJuhEdn

March 29, 2025 at 10:02AM

Automated Visual Regression Testing With Playwright

Comparing visual artifacts can be a powerful, if fickle, approach to automated testing. Playwright makes this seem simple for websites, but the details might take a little finessing.

Recent downtime prompted me to scratch an itch that had been plaguing me for a while: The style sheet of a website I maintain has grown just a little unwieldy as we’ve been adding code while exploring new features. Now that we have a better idea of the requirements, it’s time for internal CSS refactoring to pay down some of our technical debt, taking advantage of modern CSS features (like using CSS nesting for more obvious structure). More importantly, a cleaner foundation should make it easier to introduce that dark mode feature we’re sorely lacking so we can finally respect users’ preferred color scheme.

However, being of the apprehensive persuasion, I was reluctant to make large changes for fear of unwittingly introducing bugs. I needed something to guard against visual regressions while refactoring — except that means snapshot testing, which is notoriously slow and brittle.

In this context, snapshot testing means taking screenshots to establish a reliable baseline against which we can compare future results. As we’ll see, those artifacts are influenced by a multitude of factors that might not always be fully controllable (e.g. timing, variable hardware resources, or randomized content). We also have to maintain state between test runs, i.e. save those screenshots, which complicates the setup and means our test code alone doesn’t fully describe expectations.

Having procrastinated without a more agreeable solution revealing itself, I finally set out to create what I assumed would be a quick spike. After all, this wouldn’t be part of the regular test suite; just a one-off utility for this particular refactoring task.

Fortunately, I had vague recollections of past research and quickly rediscovered Playwright’s built-in visual comparison feature. Because I try to select dependencies carefully, I was glad to see that Playwright seems not to rely on many external packages.

Setup

The recommended setup with npm init playwright@latest does a decent job, but my minimalist taste had me set everything up from scratch instead. This do-it-yourself approach also helped me understand how the different pieces fit together.

Given that I expect snapshot testing to only be used on rare occasions, I wanted to isolate everything in a dedicated subdirectory, called test/visual; that will be our working directory from here on out. We’ll start with package.json to declare our dependencies, adding a few helper scripts (spoiler!) while we’re at it:

{
  "scripts": {
    "test": "playwright test",
    "report": "playwright show-report",
    "update": "playwright test --update-snapshots",
    "reset": "rm -r ./playwright-report ./test-results ./viz.test.js-snapshots || true"
  },
  "devDependencies": {
    "@playwright/test": "^1.49.1"
  }
}

If you don’t want node_modules hidden in some subdirectory but also don’t want to burden the root project with this rarely-used dependency, you might resort to manually invoking npm install --no-save @playwright/test in the root directory when needed.

With that in place, npm install downloads Playwright. Afterwards, npx playwright install downloads a range of headless browsers. (We’ll use npm here, but you might prefer a different package manager and task runner.)

We define our test environment via playwright.config.js with about a dozen basic Playwright settings:

import { defineConfig, devices } from "@playwright/test";

let BROWSERS = ["Desktop Firefox", "Desktop Chrome", "Desktop Safari"];
let BASE_URL = "http://localhost:8000";
let SERVER = "cd ../../dist && python3 -m http.server";

let IS_CI = !!process.env.CI;

export default defineConfig({
  testDir: "./",
  fullyParallel: true,
  forbidOnly: IS_CI,
  retries: 2,
  workers: IS_CI ? 1 : undefined,
  reporter: "html",
  webServer: {
    command: SERVER,
    url: BASE_URL,
    reuseExistingServer: !IS_CI
  },
  use: {
    baseURL: BASE_URL,
    trace: "on-first-retry"
  },
  projects: BROWSERS.map(ua => ({
    name: ua.toLowerCase().replaceAll(" ", "-"),
    use: { ...devices[ua] }
  }))
});

Here we expect our static website to already reside within the root directory’s dist folder and to be served at localhost:8000 (see SERVER; I prefer Python there because it’s widely available). I’ve included multiple browsers for illustration purposes. Still, we might reduce that number to speed things up (thus our simple BROWSERS list, which we then map to Playwright’s more elaborate projects data structure). Similarly, continuous integration is YAGNI for my particular scenario, so that whole IS_CI dance could be discarded.

Capture and compare

Let’s turn to the actual tests, starting with a minimal sample.test.js file:

import { test, expect } from "@playwright/test";

test("home page", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot();
});

npm test executes this little test suite (based on file-name conventions). The initial run always fails because it first needs to create baseline snapshots against which subsequent runs compare their results. Invoking npm test once more should report a passing test.

Changing our site, e.g. by recklessly messing with build artifacts in dist, should make the test fail again. Such failures will offer various options to compare expected and actual visuals:

Failing test with slightly different screenshots side by side

We can also inspect those baseline snapshots directly: Playwright creates a folder for screenshots named after the test file (sample.test.js-snapshots in this case), with file names derived from the respective test’s title (e.g. home-page-desktop-firefox.png).

Generating tests

Getting back to our original motivation, what we want is a test for every page. Instead of arduously writing and maintaining repetitive tests, we’ll create a simple web crawler for our website and have tests generated automatically; one for each URL we’ve identified.

Playwright’s global setup enables us to perform preparatory work before test discovery begins: Determine those URLs and write them to a file. Afterward, we can dynamically generate our tests at runtime.

While there are other ways to pass data between the setup and test-discovery phases, having a file on disk makes it easy to modify the list of URLs before test runs (e.g. temporarily ignoring irrelevant pages).

Site map

The first step is to extend playwright.config.js by inserting globalSetup and exporting two of our configuration values:

export let BROWSERS = ["Desktop Firefox", "Desktop Chrome", "Desktop Safari"];
export let BASE_URL = "http://localhost:8000";

// etc.

export default defineConfig({
  // etc.
  globalSetup: require.resolve("./setup.js")
});

Although we’re using ES modules here, we can still rely on CommonJS-specific APIs like require.resolve and __dirname. It appears there’s some Babel transpilation happening in the background, so what’s actually being executed is probably CommonJS? Such nuances sometimes confuse me because it isn’t always obvious what’s being executed where.

We can now reuse those exported values within a newly created setup.js, which spins up a headless browser to crawl our site (just because that’s easier here than using a separate HTML parser):

import { BASE_URL, BROWSERS } from "./playwright.config.js";
import { createSiteMap, readSiteMap } from "./sitemap.js";
import playwright from "@playwright/test";

export default async function globalSetup(config) {
  // only create site map if it doesn't already exist
  try {
    readSiteMap();
    return;
  } catch(err) {}

  // launch browser and initiate crawler
  let browser = playwright.devices[BROWSERS[0]].defaultBrowserType;
  browser = await playwright[browser].launch();
  let page = await browser.newPage();
  await createSiteMap(BASE_URL, page);
  await browser.close();
}

This is fairly boring glue code; the actual crawling is happening within sitemap.js:

  • createSiteMap determines URLs and writes them to disk.
  • readSiteMap merely reads any previously created site map from disk. This will be our foundation for dynamically generating tests. (We’ll see later why this needs to be synchronous.)

Fortunately, the website in question provides a comprehensive index of all pages, so my crawler only needs to collect unique local URLs from that index page:

function extractLocalLinks(baseURL) {
  let urls = new Set();
  let offset = baseURL.length;
  for(let { href } of document.links) {
    if(href.startsWith(baseURL)) {
      let path = href.slice(offset);
      urls.add(path);
    }
  }
  return Array.from(urls);
}

Wrapping that in a more boring glue code gives us our sitemap.js:

import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";

let ENTRY_POINT = "/topics";
let SITEMAP = join(__dirname, "./sitemap.json");

export async function createSiteMap(baseURL, page) {
  await page.goto(baseURL + ENTRY_POINT);
  let urls = await page.evaluate(extractLocalLinks, baseURL);
  let data = JSON.stringify(urls, null, 4);
  writeFileSync(SITEMAP, data, { encoding: "utf-8" });
}

export function readSiteMap() {
  try {
    var data = readFileSync(SITEMAP, { encoding: "utf-8" });
  } catch(err) {
    if(err.code === "ENOENT") {
      throw new Error("missing site map");
    }
    throw err;
  }
  return JSON.parse(data);
}

function extractLocalLinks(baseURL) {
  // etc.
}

The interesting bit here is that extractLocalLinks is evaluated within the browser context — thus we can rely on DOM APIs, notably document.links — while the rest is executed within the Playwright environment (i.e. Node).

Tests

Now that we have our list of URLs, we basically just need a test file with a simple loop to dynamically generate corresponding tests:

for(let url of readSiteMap()) {
  test(`page at ${url}`, async ({ page }) => {
    await page.goto(url);
    await expect(page).toHaveScreenshot();
  });
}

This is why readSiteMap had to be synchronous above: Playwright doesn’t currently support top-level await within test files.

In practice, we’ll want better error reporting for when the site map doesn’t exist yet. Let’s call our actual test file viz.test.js:

import { readSiteMap } from "./sitemap.js";
import { test, expect } from "@playwright/test";

let sitemap = [];
try {
  sitemap = readSiteMap();
} catch(err) {
  test("site map", ({ page }) => {
    throw new Error("missing site map");
  });
}

for(let url of sitemap) {
  test(`page at ${url}`, async ({ page }) => {
    await page.goto(url);
    await expect(page).toHaveScreenshot();
  });
}

Getting here was a bit of a journey, but we’re pretty much done… unless we have to deal with reality, which typically takes a bit more tweaking.

Exceptions

Because visual testing is inherently flaky, we sometimes need to compensate via special casing. Playwright lets us inject custom CSS, which is often the easiest and most effective approach. Tweaking viz.test.js

// etc.
import { join } from "node:path";

let OPTIONS = {
  stylePath: join(__dirname, "./viz.tweaks.css")
};

// etc.
  await expect(page).toHaveScreenshot(OPTIONS);
// etc.

… allows us to define exceptions in viz.tweaks.css:

/* suppress state */
main a:visited {
  color: var(--color-link);
}

/* suppress randomness */
iframe[src$="/articles/signals-reactivity/demo.html"] {
  visibility: hidden;
}

/* suppress flakiness */
body:has(h1 a[href="/wip/unicode-symbols/"]) {
  main tbody > tr:last-child > td:first-child {
    font-size: 0;
    visibility: hidden;
  }
}

:has() strikes again!

Page vs. viewport

At this point, everything seemed hunky-dory to me, until I realized that my tests didn’t actually fail after I had changed some styling. That’s not good! What I hadn’t taken into account is that .toHaveScreenshot only captures the viewport rather than the entire page. We can rectify that by further extending playwright.config.js.

export let WIDTH = 800;
export let HEIGHT = WIDTH;

// etc.

  projects: BROWSERS.map(ua => ({
    name: ua.toLowerCase().replaceAll(" ", "-"),
    use: {
      ...devices[ua],
      viewport: {
        width: WIDTH,
        height: HEIGHT
      }
    }
  }))

…and then by adjusting viz.test.js‘s test-generating loop:

import { WIDTH, HEIGHT } from "./playwright.config.js";

// etc.

for(let url of sitemap) {
  test(`page at ${url}`, async ({ page }) => {
    checkSnapshot(url, page);
  });
}

async function checkSnapshot(url, page) {
  // determine page height with default viewport
  await page.setViewportSize({
    width: WIDTH,
    height: HEIGHT
  });
  await page.goto(url);
  await page.waitForLoadState("networkidle");
  let height = await page.evaluate(getFullHeight);

  // resize viewport for before snapshotting
  await page.setViewportSize({
    width: WIDTH,
    height: Math.ceil(height)
  });
  await page.waitForLoadState("networkidle");
  await expect(page).toHaveScreenshot(OPTIONS);
}

function getFullHeight() {
  return document.documentElement.getBoundingClientRect().height;
}

Note that we’ve also introduced a waiting condition, holding until there’s no network traffic for a while in a crude attempt to account for stuff like lazy-loading images.

Be aware that capturing the entire page is more resource-intensive and doesn’t always work reliably: You might have to deal with layout shifts or run into timeouts for long or asset-heavy pages. In other words: This risks exacerbating flakiness.

Conclusion

So much for that quick spike. While it took more effort than expected (I believe that’s called “software development”), this might actually solve my original problem now (not a common feature of software these days). Of course, shaving this yak still leaves me itchy, as I have yet to do the actual work of scratching CSS without breaking anything. Then comes the real challenge: Retrofitting dark mode to an existing website. I just might need more downtime.


Automated Visual Regression Testing With Playwright originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/c5Sdg49
via IFTTT

Thursday, March 27, 2025

Partly Cloudy today!



With a high of F and a low of 46F. Currently, it's 59F and Clear outside.

Current wind speeds: 9 from the South

Pollen: 0

Sunrise: March 27, 2025 at 06:43PM

Sunset: March 28, 2025 at 07:11AM

UV index: 0

Humidity: 28%

via https://ift.tt/z2OlhXy

March 28, 2025 at 10:02AM

Wednesday, March 26, 2025

Mostly Clear today!



With a high of F and a low of 41F. Currently, it's 57F and Clear outside.

Current wind speeds: 10 from the South

Pollen: 0

Sunrise: March 26, 2025 at 06:44PM

Sunset: March 27, 2025 at 07:10AM

UV index: 0

Humidity: 45%

via https://ift.tt/lcqOdum

March 27, 2025 at 10:02AM

Tuesday, March 25, 2025

Showers Early today!



With a high of F and a low of 38F. Currently, it's 48F and Partly Cloudy outside.

Current wind speeds: 5 from the Southeast

Pollen: 0

Sunrise: March 25, 2025 at 06:46PM

Sunset: March 26, 2025 at 07:09AM

UV index: 0

Humidity: 49%

via https://ift.tt/7FfAu2h

March 26, 2025 at 10:02AM

Case Study: Combining Cutting-Edge CSS Features Into a “Course Navigation” Component

Monday, March 24, 2025

Partly Cloudy today!



With a high of F and a low of 36F. Currently, it's 49F and Fair outside.

Current wind speeds: 7 from the North

Pollen: 0

Sunrise: March 24, 2025 at 06:48PM

Sunset: March 25, 2025 at 07:08AM

UV index: 0

Humidity: 49%

via https://ift.tt/awfnGLS

March 25, 2025 at 10:02AM

Support Logical Shorthands in CSS

There’s a bit of a blind spot when working with CSS logical properties concerning shorthands. Miriam explains:

Logical properties are a great way to optimize our sites in advance, without any real effort.

But what if we want to set multiple properties at once? This is where shorthands like margin and padding become useful. But they are currently limited to setting physical dimension. Logical properties are great, but they still feel like a second-class feature of the language.

There are a few 2-value shorthands that have been implemented, like margin-block for setting both the -block-start and -block-endmargins. I find those extremely useful and concise. But the existing 4-value shorthands feel stuck in the past. It’s surprising that a size shorthand can’t set the inline-size, and the inset shorthand doesn’t include inset-block-start. Is there any way to update those shorthand properties so that they can be used to set logical dimensions?

She ends with the money question, whether we can do anything about it. We’re currently in a position of having to choose between supporting flow-relative terms like block-start and inline-start with longhand properties and the ergonomic benefits of writing shorthand properties that are evaluated as physical terms like top, bottom, left, and right. Those of us writing CSS for a while likely have the muscle memory to adapt accordingly, but it’s otherwise a decision that has real consequences, particularly for multi-lingual sites.

Note that Miriam says this is something the CSS Working Group has been working on since 2017. And there’s a little momentum to pick it up and do something about it. The first thing you can do is support Miriam’s work — everything she does with the CSS Working Group (and it’s a lot) is a labor of love and relies on sponsorships, so chipping in is one way to push things forward.

The other thing you can do is chime into Miriam’s proposal that she published in 2021. I think it’s a solid idea. We can’t simply switch from physical to flow-relative terms in shorthand properties without triggering compatibility issues, so having some sort of higher-level instruction for CSS at the top of the stylesheet, perhaps as an at-rule that specifies which “mode” we’re in.

<coordinate-mode> = [ logical | physical ] or [ relative | absolute ] or ...

@mode <coordinate-mode>; /* must come after @import and before any style rules */

@mode <coordinate-mode> { <stylesheet> }

selector {
  property: value  !<coordinate-mode>;
}

Perhaps naming aside, it seems pretty reasonable, eh?


Support Logical Shorthands in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/Se8CVRi
via IFTTT

Sunday, March 23, 2025

Mostly Clear today!



With a high of F and a low of 34F. Currently, it's 39F and Clear outside.

Current wind speeds: 10 from the Southwest

Pollen: 0

Sunrise: March 23, 2025 at 06:49PM

Sunset: March 24, 2025 at 07:07AM

UV index: 0

Humidity: 43%

via https://ift.tt/uqU5C78

March 24, 2025 at 10:02AM

Saturday, March 22, 2025

Clear today!



With a high of F and a low of 27F. Currently, it's 44F and Clear outside.

Current wind speeds: 3 from the Northwest

Pollen: 0

Sunrise: March 22, 2025 at 06:51PM

Sunset: March 23, 2025 at 07:06AM

UV index: 0

Humidity: 46%

via https://ift.tt/zcy1D3J

March 23, 2025 at 10:02AM

Friday, March 21, 2025

Clouds Early/Clearing Late today!



With a high of F and a low of 31F. Currently, it's 40F and Clear outside.

Current wind speeds: 4 from the Southeast

Pollen: 0

Sunrise: March 21, 2025 at 06:52PM

Sunset: March 22, 2025 at 07:05AM

UV index: 0

Humidity: 36%

via https://ift.tt/f5J4LuF

March 22, 2025 at 10:02AM

Revisiting CSS border-image

In my last article on “Revisiting CSS Multi-Column Layout”, I mentioned that almost twenty years have flown by since I wrote my first book, Transcending CSS. In it, I explained how and why to use what were, at the time, an emerging CSS property.

Ten years later, I wrote the Hardboiled Web Design Fifth Anniversary Edition, covering similar ground and introducing the new CSS border-image property.

Hint: I published an updated version, Transcending CSS Revisited which is free to read online. Hardboiled Web Design is available from my bookshop.

I was very excited about the possibilities this new property would offer. After all, we could now add images to the borders of any element, even table cells and rows (unless their borders had been set to collapse).

Since then, I’ve used border-image regularly. Yet, it remains one of the most underused CSS tools, and I can’t, for the life of me, figure out why. Is it possible that people steer clear of border-image because its syntax is awkward and unintuitive? Perhaps it’s because most explanations don’t solve the type of creative implementation problems that most people need to solve. Most likely, it’s both.

I’ve recently been working on a new website for Emmy-award-winning game composer Mike Worth. He hired me to create a highly graphical design that showcases his work, and I used border-image throughout.

Design by Andy Clarke, Stuff & Nonsense. Mike Worth’s website will launch in April 2025, but you can see examples from this article on CodePen.

A brief overview of properties and values

First, here’s a short refresher. Most border-image explanations begin with this highly illuminating code snippet:

border-image: \[source\] [slice]/\[width]/[outset\] [repeat]

This is shorthand for a set of border-image properties, but it’s best to deal with properties individually to grasp the concept more easily.

A border-image’s source

I’ll start with the source of the bitmap or vector format image or CSS gradient to be inserted into the border space:

border-image-source: url('/img/scroll.png');

When I insert SVG images into a border, I have several choices as to how. I could use an external SVG file:

border-image-source: url('/img/scroll.svg');

Or I might convert my SVG to data URI using a tool like Base64.Guru although, as both SVG and HTML are XML-based, this isn’t recommended:

border-image-source: url('data:image/svg+xml;base64,…');

Instead, I can add the SVG code directly into the source URL value and save one unnecessary HTTP request:

border-image-source: url('data:image/svg+xml;utf8,…');

Finally, I could insert an entirely CSS-generated conical, linear, or radial gradient into my border:

border-image-source: conical-gradient(…);

Tip: It’s useful to remember that a browser renders a border-image above an element’s background and box-shadow but below its content. More on that a little later.

Slicing up a border-image

Now that I’ve specified the source of a border image, I can apply it to a border by slicing it up and using the parts in different positions around an element. This can be the most baffling aspect for people new to border-image.

Most border-image explanations show an example where the pieces will simply be equally-sized, like this:

Showing nine star shapes in the same images displayed as a three-by-three grid.

However, a border-image can be developed from any shape, no matter how complex or irregular.

Instead of simply inserting an image into a border and watching it repeat around an element, invisible cut-lines slice up a border-image into nine parts. These lines are similar to the slice guides found in graphics applications. The pieces are, in turn, inserted into the nine regions of an element’s border.

Dissecting the top, right, bottom, and left slices of a border image.

The border-image-slice property defines the size of each slice by specifying the distance from each edge of the image. I could use the same distance from every edge:

border-image-slice: 65

I can combine top/bottom and left/right values:

border-image-slice: 115 65;

Or, I can specify distance values for all four cut-lines, running clockwise: top, right, bottom, left:

border-image-slice: 65 65 115 125;

The top-left of an image will be used on the top-left corner of an element’s border. The bottom-right will be used on the bottom-right, and so on.

Diagram of the nine border image slices.

I don’t need to add units to border-image-slice values when using a bitmap image as the browser correctly assumes bitmaps use pixels. The SVG viewBox makes using them a little different, so I also prefer to specify their height and width:

<svg height="600px" width="600px">…</svg>

Don’t forget to set the widths of these borders, as without them, there will be nowhere for a border’s image to display:

border-image-width: 65px 65px 115px 125px;

Filling in the center

So far, I’ve used all four corners and sides of my image, but what about the center? By default, the browser will ignore the center of an image after it’s been sliced. But I can put it to use by adding the fill keyword to my border-image-slice value:

border-image-slice: 65px 65px 115px 125px fill;

Setting up repeats

With the corners of my border images in place, I can turn my attention to the edges between them. As you might imagine, the slice at the top of an image will be placed on the top edge. The same is true of the right, bottom, and left edges. In a flexible design, we never know how wide or tall these edges will be, so I can fine-tune how images will repeat or stretch when they fill an edge.

Showing the same image four times, once per type of repeat, including stretch, repeat, round, and space.

Stretch: When a sliced image is flat or smooth, it can stretch to fill any height or width. Even a tiny 65px slice can stretch to hundreds or thousands of pixels without degrading.

border-image-repeat: stretch;

Repeat: If an image has texture, stretching it isn’t an option, so it can repeat to fill any height or width.

border-image-repeat: repeat;

Round: If an image has a pattern or shape that can’t be stretched and I need to match the edges of the repeat, I can specify that the repeat be round. A browser will resize the image so that only whole pieces display inside an edge.

border-image-repeat: round;

Space: Similar to round, when using the space property, only whole pieces will display inside an edge. But instead of resizing the image, a browser will add spaces into the repeat.

border-image-repeat: space;

When I need to specify a separate stretch, repeat, round, or space value for each edge, I can use multiple keywords:

border-image-repeat: stretch round;

Outsetting a border-image

There can be times when I need an image to extend beyond an element’s border-box. Using the border-image-outset property, I can do just that. The simplest syntax extends the border image evenly on all sides by 10px:

border-image-outset: 10px;

Of course, there being four borders on every element, I could also specify each outset individually:

border-image-outset: 20px 10px; 
/* or */
border-image-outset: 20px 10px 0;

border-image in action

Mike Worth is a video game composer who’s won an Emmy for his work. He loves ’90s animation — especially Disney’s Duck Tales — and he asked me to create custom artwork and develop a bold, retro-style design.

Four examples of page layouts, including the main menu, a default page, message received confirmation, and a 404 page, all featuring bold cartoon illustrations reminiscent of nineties Disney cartoons.

My challenge when developing for Mike was implementing my highly graphical design without compromising performance, especially on mobile devices. While it’s normal in CSS to accomplish the same goal in several ways, here, border-image often proved to be the most efficient.

Decorative buttons

The easiest and most obvious place to start was creating buttons reminiscent of stone tablets with chipped and uneven edges.

Illustration of chipped and zagged edges spliced up for border-image.

I created an SVG of the tablet shape and added it to my buttons using border-image:

button {
  border-image-repeat: stretch;
  border-image-slice: 10 10 10 10 fill;
  border-image-source: url('data:image/svg+xml;utf8,…');
  border-image-width: 20px;
}

I set the border-image-repeat on all edges to stretch and the center slice to fill so these stone tablet-style buttons expand along with their content to any height or width.

Article scroll

I want every aspect of Mike’s website design to express his brand. That means continuing the ’90s cartoon theme in his long-form content by turning it into a paper scroll.

Page layout of a paper scroll with jagged edges on the sides and rolled paper on the top and bottom.

The markup is straightforward with just a single article element:

<article>
  <!-- ... -->
</article>

But, I struggled to decide how to implement the paper effect. My first thought was to divide my scroll into three separate SVG files (top, middle, and bottom) and use pseudo-elements to add the rolled up top and bottom parts of the scroll. I started by applying a vertically repeating graphic to the middle of my article:

article {
  padding: 10rem 8rem;
  box-sizing: border-box;
  /* Scroll middle */
  background-image: url('data:image/svg+xml;utf8,…');
  background-position: center;
  background-repeat: repeat-y;
  background-size: contain;
}

Then, I added two pseudo-elements, each containing its own SVG content:

article:before {
  display: block;
  position: relative;
  top: -30px;
  /* Scroll top */
  content: url('data:image/svg+xml;utf8,…');
}

article:after {
  display: block;
  position: relative;
  top: 50px;
  /* Scroll bottom */
  content: url('data:image/svg+xml;utf8,…');
}

While this implementation worked as expected, using two pseudo-elements and three separate SVG files felt clumsy. However, using border-image, one SVG, and no pseudo-elements feels more elegant and significantly reduces the amount of code needed to implement the effect.

I started by creating an SVG of the complete tablet shape:

And I worked out the position of the four cut-lines:

Then, I inserted this single SVG into my article’s border by first selecting the source, slicing the image, and setting the top and bottom edges to stretch and the left and right edges to round:

article {
  border-image-slice: 150 95 150 95 fill;
  border-image-width: 150px 95px 150px 95px;
  border-image-repeat: stretch round;
  border-image-source: url('data:image/svg+xml;utf8,…');
}

The result is a flexible paper scroll effect which adapts to both the viewport width and any amount or type of content.

Home page overlay

My final challenge was implementing the action-packed graphic I’d designed for Mike Worth’s home page. This contains a foreground SVG featuring Mike’s orangutan mascot and a zooming background graphic:

<section>
  <!-- content -->
  <div>...</div>

  <!-- ape -->
  <div>
    <svg>…</svg>
  </div>
</section>

I defined the section as a positioning context for its children:

section {
  position: relative;
}

Then, I absolutely positioned a pseudo-element and added the zooming graphic to its background:

section:before {
  content: "";
  position: absolute;
  z-index: -1;
  background-image: url('data:image/svg+xml;utf8,…');
  background-position: center center;
  background-repeat: no-repeat;
  background-size: 100%;
}

I wanted this graphic to spin and add subtle movement to the panel, so I applied a simple CSS animation to the pseudo-element:

@keyframes spin-bg {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

section:before {
  animation: spin-bg 240s linear infinite;
}

Next, I added a CSS mask to fade the edges of the zooming graphic into the background. The CSS mask-image property specifies a mask layer image, which can be a PNG image, an SVG image or mask, or a CSS gradient:

section:before {
  mask-image: radial-gradient(circle, rgb(0 0 0) 0%, rgb(0 0 0 / 0) 60%);
  mask-repeat: no-repeat;
}

At this point, you might wonder where a border image could be used in this design. To add more interactivity to the graphic, I wanted to reduce its opacity and change its color — by adding a colored gradient overlay — when someone interacts with it. One of the simplest, but rarely-used, methods for applying an overlay to an element is using border-image. First, I added a default opacity and added a brief transition:

section:before {
  opacity: 1;
  transition: opacity .25s ease-in-out;
}

Then, on hover, I reduced the opacity to .5 and added a border-image:

section:hover::before {
  opacity: .5;
  border-image: fill 0 linear-gradient(rgba(0,0,255,.25),rgba(255,0,0,1));
}

You may ponder why I’ve not used the other border-image values I explained earlier, so I’ll dissect that declaration. First is the border-image-slice value, where zero pixels ensures that the eight corners and edges stay empty. The fill keyword ensures the middle section is filled with the linear gradient. Second, the border-image-source is a CSS linear gradient that blends blue into red. A browser renders this border-image above the background but behind the content.

Conclusion: You should take a fresh look at border-image

The border-image property is a powerful, yet often overlooked, CSS tool that offers incredible flexibility. By slicing, repeating, and outsetting images, you can create intricate borders, decorative elements, and even dynamic overlays with minimal code.

In my work for Mike Worth’s website, border-image proved invaluable, improving performance while maintaining a highly graphical aesthetic. Whether used for buttons, interactive overlays, or larger graphic elements, border-image can create visually striking designs without relying on extra markup or multiple assets.

If you’ve yet to experiment with border-image, now’s the time to revisit its potential and add it to your design toolkit.

Hint: Mike Worth’s website will launch in April 2025, but you can see examples from this article on CodePen.

About Andy Clarke

Often referred to as one of the pioneers of web design, Andy Clarke has been instrumental in pushing the boundaries of web design and is known for his creative and visually stunning designs. His work has inspired countless designers to explore the full potential of product and website design.

Andy’s written several industry-leading books, including Transcending CSS, Hardboiled Web Design, and Art Direction for the Web. He’s also worked with businesses of all sizes and industries to achieve their goals through design.

Visit Andy’s studio, Stuff & Nonsense, and check out his Contract Killer, the popular web design contract template trusted by thousands of web designers and developers.


Revisiting CSS border-image originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/hfMTcI0
via IFTTT

Thursday, March 20, 2025

Partly Cloudy today!



With a high of F and a low of 28F. Currently, it's 40F and Clear outside.

Current wind speeds: 9 from the West

Pollen: 0

Sunrise: March 20, 2025 at 06:54PM

Sunset: March 21, 2025 at 07:04AM

UV index: 0

Humidity: 39%

via https://ift.tt/LEcmVkh

March 21, 2025 at 10:02AM

Quick Reminder That :is() and :where() Are Basically the Same With One Key Difference

I’ve seen a handful of recent posts talking about the utility of the :is() relational pseudo-selector. No need to delve into the details other than to say it can help make compound selectors a lot more readable.

:is(section, article, aside, nav) :is(h1, h2, h3, h4, h5, h6) {
  color: #BADA55;
}

/* ... which would be the equivalent of: */
section h1, section h2, section h3, section h4, section h5, section h6, 
article h1, article h2, article h3, article h4, article h5, article h6, 
aside h1, aside h2, aside h3, aside h4, aside h5, aside h6, 
nav h1, nav h2, nav h3, nav h4, nav h5, nav h6 {
  color: #BADA55;
}

There’s just one catch: the specificity. The selector’s specificity matches the most specific selector in the function’s arguments. That’s not a big deal when working with a relatively flat style structure containing mostly element and class selectors, but if you toss an ID in there, then that’s the specificity you’re stuck with.

/* Specificity: 0 0 1 */
:is(h1, h2, h3, h4, h5, h6) {
  color: #BADA55;
}

/* Specificity: 1 0 0 */
:is(h1, h2, h3, h4, h5, h6, #id) {
  color: #BADA55;
}

That can be a neat thing! For example, you might want to intentionally toss a made-up ID in there to force a style the same way you might do with the !important keyword.

What if you don’t want that? Some articles suggest nesting selectors instead which is cool but not quite with the same nice writing ergonomics.

There’s where I want to point to the :where() selector instead! It’s the exact same thing as :is() but without the specificity baggage. It always carries a specificity score of zero. You might even think of it as a sort of specificity reset.

/* Specificity: 0 0 0 */
:where(h1, h2, h3, h4, h5, h6) {
  color: #BADA55;
}

/* Specificity: 0 0 0 */
:where(h1, h2, h3, h4, h5, h6, #id) {
  color: #BADA55;
}

So, is there a certain selector hijacking your :is() specificity? You might want :where() instead.


Quick Reminder That :is() and :where() Are Basically the Same With One Key Difference originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.



from CSS-Tricks https://ift.tt/OPKe5Yl
via IFTTT

Wednesday, March 19, 2025

Mostly Clear today!



With a high of F and a low of 20F. Currently, it's 27F and Clear outside.

Current wind speeds: 7 from the Northwest

Pollen: 0

Sunrise: March 19, 2025 at 06:56PM

Sunset: March 20, 2025 at 07:03AM

UV index: 0

Humidity: 73%

via https://ift.tt/E2Tmzdp

March 20, 2025 at 10:02AM

Tuesday, March 18, 2025

Blizzard today!



With a high of F and a low of 23F. Currently, it's 36F and Rain/Wind outside.

Current wind speeds: 36 from the North

Pollen: 0

Sunrise: March 18, 2025 at 06:57PM

Sunset: March 19, 2025 at 07:02AM

UV index: 0

Humidity: 83%

via https://ift.tt/gEPBSF0

March 19, 2025 at 10:02AM

Monday, March 17, 2025

Partly Cloudy today!



With a high of F and a low of 41F. Currently, it's 48F and Clear outside.

Current wind speeds: 10 from the West

Pollen: 0

Sunrise: March 17, 2025 at 06:59PM

Sunset: March 18, 2025 at 07:01AM

UV index: 0

Humidity: 32%

via https://ift.tt/ZVuia19

March 18, 2025 at 10:02AM

Styling Counters in CSS

Sunday, March 16, 2025

Mostly Clear today!



With a high of F and a low of 35F. Currently, it's 43F and Clear outside.

Current wind speeds: 12 from the Southwest

Pollen: 0

Sunrise: March 16, 2025 at 07:00PM

Sunset: March 17, 2025 at 07:00AM

UV index: 0

Humidity: 29%

via https://ift.tt/tOWjULN

March 17, 2025 at 10:02AM

Saturday, March 15, 2025

Partly Cloudy today!



With a high of F and a low of 25F. Currently, it's 33F and Partly Cloudy outside.

Current wind speeds: 6 from the Northwest

Pollen: 0

Sunrise: March 15, 2025 at 07:02PM

Sunset: March 16, 2025 at 06:59AM

UV index: 0

Humidity: 58%

via https://ift.tt/IoPliDg

March 16, 2025 at 10:02AM

Friday, March 14, 2025

Partly Cloudy/Wind today!



With a high of F and a low of 24F. Currently, it's 35F and Partly Cloudy/Wind outside.

Current wind speeds: 20 from the Northwest

Pollen: 0

Sunrise: March 14, 2025 at 07:03PM

Sunset: March 15, 2025 at 06:58AM

UV index: 0

Humidity: 66%

via https://ift.tt/NwqcAUv

March 15, 2025 at 10:02AM

Web Components Demystified

Scott Jehl released a course called Web Components Demystified. I love that name because it says what the course is about right on the tin: you’re going to learn about web components and clear up any confusion you may already have about them.

And there’s plenty of confusion to go around! “Components” is already a loaded term that’s come to mean everything from a piece of UI, like a search component, to an element you can drop in and reuse anywhere, such as a React component. The web is chock-full of components, tell you what.

But what we’re talking about here is a set of standards where HTML, CSS, and JavaScript rally together so that we can create custom elements that behave exactly how we want them to. It’s how we can make an element called <tasty-pizza> and the browser knows what to do with it.

This is my full set of notes from Scott’s course. I wouldn’t say they’re complete or even a direct one-to-one replacement for watching the course. You’ll still want to do that on your own, and I encourage you to because Scott is an excellent teacher who makes all of this stuff extremely accessible, even to noobs like me.

Chapter 1: What Web Components Are… and Aren’t

Web components are not built-in elements, even though that’s what they might look like at first glance. Rather, they are a set of technologies that allow us to instruct what the element is and how it behaves. Think of it the same way that “responsive web design” is not a thing but rather a set of strategies for adapting design to different web contexts. So, just as responsive web design is a set of ingredients — including media fluid grids, flexible images, and media queries — web components are a concoction involving:

Custom elements

These are HTML elements that are not built into the browser. We make them up. They include a letter and a dash.

<my-fancy-heading>
  Hey, I'm Fancy
</my-fancy-heading>

We’ll go over these in greater detail in the next module.

HTML templates

Templates are bits of reusable markup that generate more markup. We can hide something until we make use of it.

<template>
  <li class="user">
    <h2 class="name"></h2>
    <p class="bio"></p>
  </li>
</template>

Much more on this in the third module.

Shadow DOM

The DOM is queryable.

document.querySelector("h1");
// <h1>Hello, World</h1>

The Shadow DOM is a fragment of the DOM where markup, scripts, and styles are encapsulated from other DOM elements. We’ll cover this in the fourth module, including how to <slot> content.

There used to be a fourth “ingredient” called HTML Imports, but those have been nixed.

In short, web components might be called “components” but they aren’t really components more than technologies. In React, components sort of work like partials. It defines a snippet of HTML that you drop into your code and it outputs in the DOM. Web Components are built off of HTML Elements. They are not replaced when rendered the way they are in JavaScript component frameworks. Web components are quite literally HTML elements and have to obey HTML rules. For example:

<!-- Nope -->
<ul>
  <my-list-item></my-list-item>
  <!-- etc. -->
</ul>

<!-- Yep -->
<ul>
  <li>
    <my-list-item></my-list-item>
  </li>
</ul>

We’re generating meaningful HTML up-front rather than rendering it in the browser through the client after the fact. Provide the markup and enhance it! Web components have been around a while now, even if it seems we’re only starting to talk about them now.

Chapter 2: Custom Elements

First off, custom elements are not built-in HTML elements. We instruct what they are and how they behave. They are named with a dash and at must contain least one letter. All of the following are valid names for custom elements:

  • <super-component>
  • <a->
  • <a-4->
  • <card-10.0.1>
  • <card-♠️>

Just remember that there are some reserved names for MathML and SVG elements, like <font-face>. Also, they cannot be void elements, e.g. <my-element />, meaning they have to have a correspoonding closing tag.

Since custom elements are not built-in elements, they are undefined by default — and being undefined can be a useful thing! That means we can use them as containers with default properties. For example, they are display: inline by default and inherit the current font-family, which can be useful to pass down to the contents. We can also use them as styling hooks since they can be selected in CSS. Or maybe they can be used for accessibility hints. The bottom line is that they do not require JavaScript in order to make them immediately useful.

Working with JavaScript. If there is one <my-button> on the page, we can query it and set a click handler on it with an event listener. But if we were to insert more instances on the page later, we would need to query it when it’s appended and re-run the function since it is not part of the original document rendering.

Defining a custom element

This defines and registers the custom element. It teaches the browser that this is an instance of the Custom Elements API and extends the same class that makes other HTML elements valid HTML elements:

<my-element>My Element</my-element>

<script>
  customElements.define("my-element", class extends HTMLElement {});
</script>

Check out the methods we get immediate access to:

Showing the prototype methods and properties of a custom element in DevTools, including define, get, getName, upgrade, and whenDefined.

Breaking down the syntax

customElements
  .define(
    "my-element",
    class extends HTMLElement {}
  );
        
// Functionally the same as:
class MyElement extends HTMLElement {}
customElements.define("my-element", MyElement);
export default myElement

// ...which makes it importable by other elements:
import MyElement from './MyElement.js';
const myElement = new MyElement();
document.body.appendChild(myElement);

// <body>
//   <my-element></my-element>
// </body>

// Or simply pull it into a page
// Don't need to `export default` but it doesn't hurt to leave it
// <my-element>My Element</my-element>
// <script type="module" src="my-element.js"></script>

It’s possible to define a custom element by extending a specific HTML element. The specification documents this, but Scott is focusing on the primary way.

class WordCount extends HTMLParagraphElement
customElements.define("word-count", WordCount, { extends: "p" });

// <p is="word-count">This is a custom paragraph!</p>

Scott says do not use this because WebKit is not going to implement it. We would have to polyfill it forever, or as long as WebKit holds out. Consider it a dead end.

The lifecycle

A component has various moments in its “life” span:

  • Constructed (constructor)
  • Connected (connectedCallback)
  • Adopted (adoptedCallback)
  • Attribute Changed (attributeChangedCallback)
  • Disconnected (disconnectedCallback)

We can hook into these to define the element’s behavior.

class myElement extends HTMLElement {
  constructor() {}
  connectedCallback() {}
  adoptedCallback() {}
  attributeChangedCallback() {}
  disconnectedCallback() {}
}

customElements.define("my-element", MyElement);

constructor()

class myElement extends HTMLElement {
  constructor() {
    // provides us with the `this` keyword
    super()
    
    // add a property
    this.someProperty = "Some value goes here";
    // add event listener
    this.addEventListener("click", () => {});
  }
}

customElements.define("my-element", MyElement);

“When the constructor is called, do this…” We don’t have to have a constructor when working with custom elements, but if we do, then we need to call super() because we’re extending another class and we’ll get all of those properties.

Constructor is useful, but not for a lot of things. It’s useful for setting up initial state, registering default properties, adding event listeners, and even creating Shadow DOM (which Scott will get into in a later module). For example, we are unable to sniff out whether or not the custom element is in another element because we don’t know anything about its parent container yet (that’s where other lifecycle methods come into play) — we’ve merely defined it.

connectedCallback()

class myElement extends HTMLElement {
  // the constructor is unnecessary in this example but doesn't hurt.
  constructor() {
    super()
  }
  // let me know when my element has been found on the page.
  connectedCallback() {
    console.log(`${this.nodeName} was added to the page.`);
  }
}

customElements.define("my-element", MyElement);

Note that there is some strangeness when it comes to timing things. Sometimes isConnected returns true during the constructor. connectedCallback() is our best way to know when the component is found on the page. This is the moment it is connected to the DOM. Use it to attach event listeners.

If the <script> tag comes before the DOM is parsed, then it might not recognize childNodes. This is not an uncommon situation. But if we add type="module" to the <script>, then the script is deferred and we get the child nodes. Using setTimeout can also work, but it looks a little gross.

disconnectedCallback

class myElement extends HTMLElement {
  // let me know when my element has been found on the page.
  disconnectedCallback() {
    console.log(`${this.nodeName} was removed from the page.`);
  }
}

customElements.define("my-element", MyElement);

This is useful when the component needs to be cleaned up, perhaps like stopping an animation or preventing memory links.

adoptedCallback()

This is when the component is adopted by another document or page. Say you have some iframes on a page and move a custom element from the page into an iframe, then it would be adopted in that scenario. It would be created, then added, then removed, then adopted, then added again. That’s a full lifecycle! This callback is adopted automatically simply by picking it up and dragging it between documents in the DOM.

Custom elements and attributes

Unlike React, HTML attributes are strings (not props!). Global attributes work as you’d expect, though some global attributes are reflected as properties. You can make any attribute do that if you want, just be sure to use care and caution when naming because, well, we don’t want any conflicts.

A list od global property names, such as hidden, ID, and style.

Avoid standard attributes on a custom element as well, as that can be confusing particularly when handing a component to another developer. Example: using type as an attribute which is also used by <input> elements. We could say data-type instead. (Remember that Chris has a comprehensive guide on using data attributes.)

Examples

Here’s a quick example showing how to get a greeting attribute and set it on the custom element:

class MyElement extends HTMLElement {
  get greeting() {
    return this.getAttribute('greeting');
    // return this.hasAttribute('greeting');
  }
  set greeting(val) {
    if(val) {
      this.setAttribute('greeting', val);
      // this setAttribute('greeting', '');
    } else {
      this.removeAttribute('greeting');
    }
  }
}
customElements.define("my-element", MyElement);

Another example, this time showing a callback for when the attribute has changed, which prints it in the element’s contents:

<my-element greeting="hello">hello</my-element>

<!-- Change text greeting when attribite greeting changes  -->
<script>
  class MyElement extends HTMLElement {
    static observedAttributes = ["greeting"];
  
    attributeChangedCallback(name, oldValue, newValue) {
      if (name === 'greeting' && oldValue && oldValue !== newValue) {
        console.log(name + " changed");
        this.textContent = newValue;
      }
    }
  }
  
  customElements.define("my-element", MyElement);
</script>

A few more custom element methods:

customElements.get('my-element');
// returns MyElement Class

customElements.getName(MyElement);
// returns 'my-element'

customElements.whenDefined("my-element");
// waits for custom element to be defined

const el = document.createElement("spider-man");
class SpiderMan extends HTMLElement {
  constructor() {
    super();
    console.log("constructor!!");
  }
}
customElements.define("spider-man", SpiderMan);

customElements.upgrade(el);
// returns "constructor!!"

Custom methods and events:

<my-element><button>My Element</button></my-element>

<script>
  customElements.define("my-element", class extends HTMLElement {
    connectedCallback() {
      const btn = this.firstElementChild;
      btn.addEventListener("click", this.handleClick)
    }
    handleClick() {
      console.log(this);
    }
  });
</script>

Bring your own base class, in the same way web components frameworks like Lit do:

class BaseElement extends HTMLElement {
  $ = this.querySelector;
}
// extend the base, use its helper
class myElement extends BaseElement {
  firstLi = this.$("li");
}

Practice prompt

Create a custom HTML element called <say-hi> that displays the text “Hi, World!” when added to the page:

Enhance the element to accept a name attribute, displaying "Hi, [Name]!" instead:

Chapter 3: HTML Templates

The <template> element is not for users but developers. It is not exposed visibly by browsers.

<template>The browser ignores everything in here.</template>

Templates are designed to hold HTML fragments:

<template>
  <div class="user-profile">
    <h2 class="name">Scott</h2>
    <p class="bio">Author</p>
  </div>
</template>

A template is selectable in CSS; it just doesn’t render. It’s a document fragment. The inner document is a #document-fragment. Not sure why you’d do this, but it illustrates the point that templates are selectable:

template { display: block; }` /* Nope */
template + div { height: 100px; width: 100px; }  /* Works */

The content property

No, not in CSS, but JavaScript. We can query the inner contents of a template and print them somewhere else.

<template>
  <p>Hi</p>
</template>

<script>
  const myTmpl = documenty.querySelector("template").content;
  console.log(myTmpl);
</script>

Using a Document Fragment without a <template>

const myFrag = document.createDocumentFragment();
myFrag.innerHTML = "<p>Test</p>"; // Nope

const myP = document.createElement("p"); // Yep
myP.textContent = "Hi!";
myFrag.append(myP);

// use the fragment
document.body.append(myFrag);

Clone a node

<template>
  <p>Hi</p>
</template>

<script>
  const myTmpl = documenty.querySelector("template").content;
  console.log(myTmpl);
  
  // Oops, only works one time! We need to clone it.
</script>

Oops, the component only works one time! We need to clone it if we want multiple instances:

<template>
  <p>Hi</p>
</template>

<script>
  const myTmpl = document.querySelector("template").content;
  document.body.append(myTmpl.cloneNode(true)); // true is necessary
  document.body.append(myTmpl.cloneNode(true));
  document.body.append(myTmpl.cloneNode(true));
  document.body.append(myTmpl.cloneNode(true));
</script>

A more practical example

Let’s stub out a template for a list item and then insert them into an unordered list:

<template id="tmpl-user"><li><strong></strong>: <span></span></li></template>

<ul id="users"></ul>

<script>
  const usersElement = document.querySelector("#users");
  const userTmpl = document.querySelector("#tmpl-user").content;
  const users = [{name: "Bob", title: "Artist"}, {name: "Jane", title: "Doctor"}];
  users.forEach(user => {
    let thisLi = userTmpl.cloneNode(true);
    thisLi.querySelector("strong").textContent = user.name;
    thisLi.querySelector("span").textContent = user.title;
    usersElement.append(thisLi);
  });
</script>

The other way to use templates that we’ll get to in the next module: Shadow DOM

<template shadowroot=open>
  <p>Hi, I'm in the Shadow DOM</p>
</template>

Chapter 4: Shadow DOM

Here we go, this is a heady chapter! The Shadow DOM reminds me of playing bass in a band: it’s easy to understand but incredibly difficult to master. It’s easy to understand that there are these nodes in the DOM that are encapsulated from everything else. They’re there, we just can’t really touch them with regular CSS and JavaScript without some finagling. It’s the finagling that’s difficult to master. There are times when the Shadow DOM is going to be your best friend because it prevents outside styles and scripts from leaking in and mucking things up. Then again, you’re most certainly going go want to style or apply scripts to those nodes and you have to figure that part out.

That’s where web components really shine. We get the benefits of an element that’s encapsulated from outside noise but we’re left with the responsibility of defining everything for it ourselves.

Inspecting a select element in DevTools, showing the Shadow DOM nodes in the inspector.
Select elements are a great example of the Shadow DOM. Shadow roots! Slots! They’re all part of the puzzle.

Using the Shadow DOM

We covered the <template> element in the last chapter and determined that it renders in the Shadow DOM without getting displayed on the page.

<template shadowrootmode="closed">
  <p>This will render in the Shadow DOM.</p>
</template>
A template revealed in the DevTools Elements inspector based on the last code example.

In this case, the <template> is rendered as a #shadow-root without the <template> element’s tags. It’s a fragment of code. So, while the paragraph inside the template is rendered, the <template> itself is not. It effectively marks the Shadow DOM’s boundaries. If we were to omit the shadowrootmode attribute, then we simply get an unrendered template. Either way, though, the paragraph is there in the DOM and it is encapsulated from other styles and scripts on the page.

A two-column list of element tags indicating elements that support the Shadow DOM.
These are all of the elements that can have a shadow.

Breaching the shadow

There are times you’re going to want to “pierce” the Shadow DOM to allow for some styling and scripts. The content is relatively protected but we can open the shadowrootmode and allow some access.

<div>
  <template shadowrootmode="open">
    <p>This will render in the Shadow DOM.</p>
  </template>
</div>

Now we can query the div that contains the <template> and select the #shadow-root:

document.querySelector("div").shadowRoot
// #shadow-root (open)
// <p>This will render in the Shadow DOM.</p>

We need that <div> in there so we have something to query in the DOM to get to the paragraph. Remember, the <template> is not actually rendered at all.

Additional shadow attributes

<!-- should this root stay with a parent clone? -->
<template shadowrootcloneable>
<!-- allow shadow to be serialized into a string object — can forget about this -->
<template shadowrootserializable>
<!-- click in element focuses first focusable element -->
<template shadowrootdelegatesfocus>

Shadow DOM siblings

When you add a shadow root, it becomes the only rendered root in that shadow host. Any elements after a shadow root node in the DOM simply don’t render. If a DOM element contains more than one shadow root node, the ones after the first just become template tags. It’s sort of like the Shadow DOM is a monster that eats the siblings.

Slots bring those siblings back!

<div>
  <template shadowroot="closed">
    <slot></slot>
    <p>I'm a sibling of a shadow root, and I am visible.</p>
  </template>
</div>

All of the siblings go through the slots and are distributed that way. It’s sort of like slots allow us to open the monster’s mouth and see what’s inside.

Declaring the Shadow DOM

Using templates is the declarative way to define the Shadow DOM. We can also define the Shadow DOM imperatively using JavaScript. So, this is doing the exact same thing as the last code snippet, only it’s done programmatically in JavaScript:

<my-element>
  <template shadowroot="open">
    <p>This will render in the Shadow DOM.</p>
  </template>
</my-element>

<script>
  customElements.define('my-element', class extends HTMLElement {
    constructor() {
      super();
      // attaches a shadow root node
      this.attachShadow({mode: "open"});
      // inserts a slot into the template
      this.shadowRoot.innerHTML = '<slot></slot>';
    }
  });
</script>

Another example:

<my-status>available</my-status>

<script>
  customElements.define('my-status', class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({mode: "open"});
      this.shadowRoot.innerHTML = '<p>This item is currently: <slot></slot></p>';
    }
  });
</script>

So, is it better to be declarative or imperative? Like the weather where I live, it just depends.

Comparing imperative and declarative benefits to defining the shadow DOM.
Both approaches have their benefits.

We can set the shadow mode via Javascript as well:

// open
this.attachShadow({mode: open});
// closed
this.attachShadow({mode: closed});
// cloneable
this.attachShadow({cloneable: true});
// delegateFocus
this.attachShadow({delegatesFocus: true});
// serialized
this.attachShadow({serializable: true});

// Manually assign an element to a slot
this.attachShadow({slotAssignment: "manual"});

About that last one, it says we have to manually insert the <slot> elements in JavaScript:

<my-element>
  <p>This WILL render in shadow DOM but not automatically.</p>
</my-element>

<script>
  customElements.define('my-element', class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({
        mode: "open",
        slotAssignment: "manual"
      });
      this.shadowRoot.innerHTML = '<slot></slot>';
    }
    connectedCallback(){
      const slotElem = this.querySelector('p');
      this.shadowRoot.querySelector('slot').assign(slotElem);
    }
  });
</script>

Examples

Scott spent a great deal of time sharing examples that demonstrate different sorts of things you might want to do with the Shadow DOM when working with web components. I’ll rapid-fire those in here.

Get an array of element nodes in a slot

this.shadowRoot.querySelector('slot')
  .assignedElements();

// get an array of all nodes in a slot, text too
this.shadowRoot.querySelector('slot')
  .assignedNodes();

When did a slot’s nodes change?

let slot = document.querySelector('div')
  .shadowRoot.querySelector("slot");
  
  slot.addEventListener("slotchange", (e) => {
    console.log(`Slot "${slot.name}" changed`);
    // > Slot "saying" changed
  })

Combining imperative Shadow DOM with templates

Back to this example:

<my-status>available</my-status>

<script>
  customElements.define('my-status', class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({mode: "open"});
      this.shadowRoot.innerHTML = '<p>This item is currently: <slot></slot></p>';
    }
  });
</script>

Let’s get that string out of our JavaScript with reusable imperative shadow HTML:

<my-status>available</my-status>

<template id="my-status">
  <p>This item is currently: 
    <slot></slot>
  </p>
</template>

<script>
  customElements.define('my-status', class extends HTMLElement {
    constructor(){
      super();
      this.attachShadow({mode: 'open'});
      const template = document.getElementById('my-status');
                
      this.shadowRoot.append(template.content.cloneNode(true));
    }
  });
</script>

Slightly better as it grabs the component’s name programmatically to prevent name collisions:

<my-status>available</my-status>

<template id="my-status">
  <p>This item is currently: 
    <slot></slot>
  </p>
</template>

<script>
  customElements.define('my-status', class extends HTMLElement {
    constructor(){
      super();
      this.attachShadow({mode: 'open'});
      const template = document.getElementById( this.nodeName.toLowerCase() );

      this.shadowRoot.append(template.content.cloneNode(true));
    }
  });
</script>

Forms with Shadow DOM

Long story, cut short: maybe don’t create custom form controls as web components. We get a lot of free features and functionalities — including accessibility — with native form controls that we have to recreate from scratch if we decide to roll our own.

In the case of forms, one of the oddities of encapsulation is that form submissions are not automatically connected. Let’s look at a broken form that contains a web component for a custom input:

<form>
  <my-input>
    <template shadowrootmode="open">
      <label>
        <slot></slot>
        <input type="text" name="your-name">
      </label>
    </template>
    Type your name!
  </my-input>
  <label><input type="checkbox" name="remember">Remember Me</label>
  <button>Submit</button>
</form>

<script>
document.forms[0].addEventListener('input', function(){
  let data = new FormData(this);
  console.log(new URLSearchParams(data).toString());
});
</script>

This input’s value won’t be in the submission! Also, form validation and states are not communicated in the Shadow DOM. Similar connectivity issues with accessibility, where the shadow boundary can interfere with ARIA. For example, IDs are local to the Shadow DOM. Consider how much you really need the Shadow DOM when working with forms.

Element internals

The moral of the last section is to tread carefully when creating your own web components for form controls. Scott suggests avoiding that altogether, but he continued to demonstrate how we could theoretically fix functional and accessibility issues using element internals.

Let’s start with an input value that will be included in the form submission.

<form>
  <my-input name="name"></my-input>
  <button>Submit</button>
</form>

Now let’s slot this imperatively:

<script>
  customElements.define('my-input', class extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({mode: 'open'});
      this.shadowRoot.innerHTML = '<label><slot></slot><input type="text"></label>'
    }
  });
</script>

The value is not communicated yet. We’ll add a static formAssociated variable with internals attached:

<script>
  customElements.define('my-input', class extends HTMLElement {
    static formAssociated = true;
    constructor() {
      super();
      this.attachShadow({mode: 'open'});
      this.shadowRoot.innerHTML = '<label><slot></slot><input type="text"></label>'
      this.internals = this.attachedInternals();
    }
  });
</script>

Then we’ll set the form value as part of the internals when the input’s value changes:

<script>
  customElements.define('my-input', class extends HTMLElement {
    static formAssociated = true;
    constructor() {
      super();
      this.attachShadow({mode: 'open'});
      this.shadowRoot.innerHTML = '<label><slot></slot><input type="text"></label>'
      this.internals = this.attachedInternals();
      
      this.addEventListener('input', () => {
        this-internals.setFormValue(this.shadowRoot.querySelector('input').value);
      });
    }
  });
</script>

Here’s how we set states with element internals:

// add a checked state
this.internals.states.add("checked");

// remove a checked state
this.internals.states.delete("checked");

Let’s toggle a “add” or “delete” a boolean state:

<form>
  <my-check name="remember">Remember Me?</my-check>
</form>

<script>
  customElements.define('my-check', class extends HTMLElement {
    static formAssociated = true;
    constructor(){
      super();
      this.attachShadow({mode: 'open'});
      this.shadowRoot.innerHTML = '<slot></slot>';
      this.internals = this.attachInternals();
      let addDelete = false;
      this.addEventListener("click", ()=> {
        addDelete = !addDelete;
        this.internals.states[addDelete ? "add" : "delete"]("checked");
     } );
    }
  });
</script>

Let’s refactor this for ARIA improvements:

<form>
  <style>
    my-check { display: inline-block; inline-size: 1em; block-size: 1em; background: #eee; }
    my-check:state(checked)::before { content: "[x]"; }
  </style>
  <my-check name="remember" id="remember"></my-check><label for="remember">Remember Me?</label>
</form>

<script>
  customElements.define('my-check', class extends HTMLElement {
    static formAssociated = true;
    constructor(){
      super();
      this.attachShadow({mode: 'open'});


      this.internals = this.attachInternals();
      this.internals.role = 'checkbox';
      this.setAttribute('tabindex', '0');
      let addDelete = false;
      this.addEventListener("click", ()=> {
        addDelete = !addDelete;
        this.internals.states[addDelete ? "add" : "delete"]("checked");
        this[addDelete ? "setAttribute" : "removeAttribute"]("aria-checked", true);
      });
    }
  });
</script>
Inspecting a web component for a custom form input in DevTools, revealing the ARIA roles and attributes that were programmatically added to the component.

Phew, that’s a lot of work! And sure, this gets us a lot closer to a more functional and accessible custom form input, but there’s still a long way’s to go to achieve what we already get for free from using native form controls. Always question whether you can rely on a light DOM form instead.

Chapter 5: Styling Web Components

Styling web components comes in levels of complexity. For example, we don’t need any JavaScript at all to slap a few styles on a custom element.

<my-element theme="suave" class="priority">
  <h1>I'm in the Light DOM!</h1>
</my-element>

<style>
  /* Element, class, attribute, and complex selectors all work. */
  my-element {
    display: block; /* custom elements are inline by default */
  }
  .my-element[theme=suave] {
    color: #fff;
  }
  .my-element.priority {
    background: purple;
  }
  .my-element h1 {
    font-size: 3rem;
  }
</style>
  • This is not encapsulated! This is scoped off of a single element just light any other CSS in the Light DOM.
  • Changing the Shadow DOM mode from closed to open doesn’t change CSS. It allows JavaScript to pierce the Shadow DOM but CSS isn’t affected.

Let’s poke at it

<style>
p { color: red; }
</style>

<p>Hi</p>

<div>
  <template shadowrootmode="open">
    <p>Hi</p>
  </template>
</div>

<p>Hi</p>
  • This is three stacked paragraphs, the second of which is in the shadow root.
  • The first and third paragraphs are red; the second is not styled because it is in a <template>, even if the shadow root’s mode is set to open.

Let’s poke at it from the other direction:

<style>
p { color: red; }
</style>

<p>Hi</p>

<div>
  <template shadowrootmode="open">
    <style> p { color: blue;} </style>
    <p>Hi</p>
  </template>
</div>

<p>Hi</p>
  • The first and third paragraphs are still receiving the red color from the Light DOM’s CSS.
  • The <style> declarations in the <template> are encapsulated and do not leak out to the other paragraphs, even though it is declared later in the cascade.

Same idea, but setting the color on the <body>:

<style>
body { color: red; }
</style>

<p>Hi</p>

<div>
  <template shadowrootmode="open">
    <p>Hi</p>
  </template>
</div>

<p>Hi</p>
  • Everything is red! This isn’t a bug. Inheritable styles do pass through the Shadow DOM barrier.
  • Inherited styles are those that are set by the computed values of their parent styles. Many properties are inheritable, including color. The <body> is the parent and everything in it is a child that inherits these styles, including custom elements.
A list of properties that inherit styles.

Let’s fight with inheritance

We can target the paragraph in the <template> style block to override the styles set on the <body>. Those won’t leak back to the other paragraphs.

<style>
  body {
    color: red;
    font-family: fantasy;
    font-size: 2em;
  }
</style>
  
<p>Hi</p>
<div>
  <template shadowrootmode="open">
    <style> 
      /* reset the light dom styles */
      p {
        color: initial; 
        font-family: initial; 
        font-size: initial;
      }
    </style>
    <p>Hi</p>
  </template>
</div>
  
<p>Hi</p>
  • This is protected, but the problem here is that it’s still possible for a new role or property to be introduced that passes along inherited styles that we haven’t thought to reset.
  • Perhaps we could use all: initital as a defensive strategy against future inheritable styles. But what if we add more elements to the custom element? It’s a constant fight.

Host styles!

We can scope things to the shadow root’s :host selector to keep things protected.

<style>
  body {
    color: red;
    font-family: fantasy;
    font-size: 2em;
  }
</style>
  
<p>Hi</p>
<div>
  <template shadowrootmode="open">
    <style> 
      /* reset the light dom styles */
      :host { all: initial; }
    </style>
    <p>Hi</p>
    <a href="#">Click me</a>
  </template>
</div>
  
<p>Hi</p>

New problem! What if the Light DOM styles are scoped to the universal selector instead?

<style>
  * {
    color: red;
    font-family: fantasy;
    font-size: 2em;
  }
</style>

<p>Hi</p>
<div>
  <template shadowrootmode="open">
    <style> 
      /* reset the light dom styles */
      :host { all: initial; }
    </style>
    <p>Hi</p>
    <a href="#">Click me</a>
  </template>
</div>

<p>Hi</p>

This breaks the custom element’s styles. But that’s because Shadow DOM styles are applied before Light DOM styles. The styles scoped to the universal selector are simply applied after the :host styles, which overrides what we have in the shadow root. So, we’re still locked in a brutal fight over inheritance and need stronger specificity.

According to Scott, !important is one of the only ways we have to apply brute force to protect our custom elements from outside styles leaking in. The keyword gets a bad rap — and rightfully so in the vast majority of cases — but this is a case where it works well and using it is an encouraged practice. It’s not like it has an impact on the styles outside the custom element, anyway.

<style>
  * {
    color: red;
    font-family: fantasy;
    font-size: 2em;
  }
</style>

<p>Hi</p>
<div>
  <template shadowrootmode="open">
    <style> 
      /* reset the light dom styles */
      :host { all: initial; !important }
    </style>
    <p>Hi</p>
    <a href="#">Click me</a>
  </template>
</div>

<p>Hi</p>

Special selectors

There are some useful selectors we have to look at components from the outside, looking in.

:host()

We just looked at this! But note how it is a function in addition to being a pseudo-selector. It’s sort of a parent selector in the sense that we can pass in the <div> that contains the <template> and that becomes the scoping context for the entire selector, meaning the !important keyword is no longer needed.

<style>
  * {
    color: red;
    font-family: fantasy;
    font-size: 2em;
  }
</style>

<p>Hi</p>
<div>
  <template shadowrootmode="open">
    <style> 
      /* reset the light dom styles */
      :host(div) { all: initial; }
    </style>
    <p>Hi</p>
    <a href="#">Click me</a>
  </template>
</div>

<p>Hi</p>

:host-context()

<header>
  <my-element>
    <template shadowrootmode="open">
      <style>
        :host-context(header) { ... } /* matches the host! */
      </style>
    </template>
  </my-element>
</header>

This targets the shadow host but only if the provided selector is a parent node anywhere up the tree. This is super helpful for styling custom elements where the layout context might change, say, from being contained in an <article> versus being contained in a <header>.

:defined

Defining an element occurs when it is created, and this pseudo-selector is how we can select the element in that initially-defined state. I imagine this is mostly useful for when a custom element is defined imperatively in JavaScript so that we can target the very moment that the element is constructed, and then set styles right then and there.

<style>
  simple-custom:defined { display: block; background: green; color: #fff; }
</style>
<simple-custom></simple-custom>

<script>
customElements.define('simple-custom', class extends HTMLElement {
  constructor(){
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = "<p>Defined!</p>";
  }
});
</script>

Minor note about protecting against a flash of unstyled content (FOUC)… or unstyled element in this case. Some elements are effectively useless until JavsScript has interacted with it to generate content. For example, an empty custom element that only becomes meaningful once JavaScript runs and generates content. Here’s how we can prevent the inevitable flash that happens after the content is generated:

<style>
  js-dependent-element:not(:defined) {
    visibility: hidden;
  }
</style>

<js-dependent-element></js-dependent-element>

Warning zone! It’s best for elements that are empty and not yet defined. If you’re working with a meaningful element up-front, then it’s best to style as much as you can up-front.

Styling slots

This does not style the paragraph green as you might expect:

<div>
  <template shadowrootmode="open">
    <style>
      p { color: green; }
    </style>
    <slot></slot>
  </template>
  
  <p>Slotted Element</p>
</div>

The Shadow DOM cannot style this content directly. The styles would apply to a paragraph in the <template> that gets rendered in the Light DOM, but it cannot style it when it is slotted into the <template>.

Slots are part of the Light DOM. So, this works:

<style>
  p { color: green; }
</style>

<div>
  <template shadowrootmode="open">
    <slot></slot>
  </template>
  
  <p>Slotted Element</p>
</div>

This means that slots are easier to target when it comes to piercing the shadow root with styles, making them a great method of progressive style enhancement.

We have another special selected, the ::slotted() pseudo-element that’s also a function. We pass it an element or class and that allows us to select elements from within the shadow root.

<div>
  <template shadowrootmode="open">
    <style> ::slotted(p) { color: red; } </style>
    <slot></slot>
  </template>
  
  <p>Slotted Element</p>
</div>

Unfortunately, ::slotted() is a weak selected when compared to global selectors. So, if we were to make this a little more complicated by introducing an outside inheritable style, then we’d be hosed again.

<style>
  /* global paragraph style... */
  p { color: green; }
</style>

<div>
  <template shadowrootmode="open">
    <style>
      /* ...overrides the slotted style */
      ::slotted(p) { color: red; }
    </style>
    <slot></slot>
  </template>
  
  <p>Slotted Element</p>
</div>

This is another place where !important could make sense. It even wins if the global style is also set to !important. We could get more defensive and pass the universal selector to ::slotted and set everything back to its initial value so that all slotted content is encapsulated from outside styles leaking in.

<style>
  /* global paragraph style... */
  p { color: green; }
</style>

<div>
  <template shadowrootmode="open">
    <style>
      /* ...can't override this important statement */
      ::slotted(*) { all: initial !important; }
    </style>
    <slot></slot>
  </template>
  
  <p>Slotted Element</p>
</div>

Styling :parts

A part is a way of offering up Shadow DOM elements to the parent document for styling. Let’s add a part to a custom element:

<div>
  <template shadowrootmode="open">
    <p part="hi">Hi there, I'm a part!</p>
  </template>
</div>

Without the part attribute, there is no way to write styles that reach the paragraph. But with it, the part is exposed as something that can be styled.

<style>
  ::part(hi) { color: green; }
  ::part(hi) b { color: green; } /* nope! */
</style>

<div>
  <template shadowrootmode="open">
    <p part="hi">Hi there, I'm a <b>part</b>!</p>
  </template>
</div>

We can use this to expose specific “parts” of the custom element that are open to outside styling, which is almost like establishing a styling API with specifications for what can and can’t be styled. Just note that ::part cannot be used as part of a complex selector, like a descendant selector:

A bit in the weeds here, but we can export parts in the sense that we can nest elements within elements within elements, and so on. This way, we include parts within elements.

<my-component>
  <!-- exposes three parts to the nested component -->
  <nested-component exportparts="part1, part2, part5"></nested-component>
</my-component>

Styling states and validity

We discussed this when going over element internals in the chapter about the Shadow DOM. But it’s worth revisiting that now that we’re specifically talking about styling. We have a :state pseudo-function that accepts our defined states.

<script>
  this.internals.states.add("checked");
</script>

<style>
  my-checkbox:state(checked) {
    /* ... */
  }
</style>

We also have access to the :invalid pseudo-class.

Cross-barrier custom properties

<style>
:root {
  --text-primary: navy;
  --bg-primary: #abe1e1;
  --padding: 1.5em 1em;
}

p {
  color: var(--text-primary);
  background: var(--bg-primary);
  padding: var(--padding);
}
</style>

Custom properties cross the Shadow DOM barrier!

<my-elem></my-elem>

<script>
  customElements.define('my-elem', class extends HTMLElement {
    constructor(){
      super();
      this.attachShadow({mode: 'open'});
      this.shadowRoot.innerHTML = `
      <style>
        p {
          color: var(--text-primary);
          background: var(--bg-primary);
          padding: var(--padding);
        }
      </style>
      
      <p>Hi there!</p>`;
    }
  })
</script>
DevTools inspector showing the styled paragraph.

Adding stylesheets to custom elements

There’s the classic ol’ external <link> way of going about it:

<simple-custom>
  <template shadowrootmode="open">
    <link rel="stylesheet" href="../../assets/external.css">
    <p>This one's in the shadow Dom.</p>
    <slot></slot>
  </template>

  <p>Slotted <b>Element</b></p>

</simple-custom>

It might seem like an anti-DRY approach to call the same external stylesheet at the top of all web components. To be clear, yes, it is repetitive — but only as far as writing it. Once the sheet has been downloaded once, it is available across the board without any additional requests, so we’re still technically dry in the sense of performance.

CSS imports also work:

<style>
  @import url("../../assets/external.css");
</style>

<simple-custom>
  <template shadowrootmode="open">
    <style>
      @import url("../../assets/external.css");
    </style>
    <p>This one's in the shadow Dom.</p>
    <slot></slot>
  </template>

  <p>Slotted <b>Element</b></p>

</simple-custom>

One more way using a JavaScript-based approach. It’s probably better to make CSS work without a JavaScript dependency, but it’s still a valid option.

<my-elem></my-elem>

<script type="module">
  import sheet from '../../assets/external.css' with { type: 'css' };

  customElements.define('my-elem', class extends HTMLElement {
    constructor(){
      super();
      this.attachShadow({mode: 'open'});
      this.shadowRoot.innerHTML = '<p>Hi there</p>';
      this.shadowRoot.adoptedStyleSheets = [sheet];
    }
  })
</script>

We have a JavaScript module and import CSS into a string that is then adopted by the shadow root using shadowRoort.adoptedStyleSheets . And since adopted stylesheets are dynamic, we can construct one, share it across multiple instances, and update styles via the CSSOM that ripple across the board to all components that adopt it.

Container queries!

Container queries are nice to pair with components, as custom elements and web components are containers and we can query them and adjust things as the container changes.

<div>
<template shadowrootmode="open">
  <style>
    :host {
      container-type: inline-size;
      background-color: tan;
      display: block;
      padding: 2em;
    }
    
    ul {
      display: block;
      list-style: none;
      margin: 0;
    }
    
    li {
      padding: .5em;
      margin: .5em 0;
      background-color: #fff;
    }
    
    @container (min-width: 50em) {
      ul {
        display: flex;
        justify-content: space-between;
        gap: 1em;
      }
      li {
        flex: 1 1 auto;
      }
    }
  </style>
  
  <ul>
    <li>First Item</li>
    <li>Second Item</li>
  </ul>
  
</template>
</div>

In this example, we’re setting styles on the :host() to define a new container, as well as some general styles that are protected and scoped to the shadow root. From there, we introduce a container query that updates the unordered list’s layout when the custom element is at least 50em wide.

Next up…

How web component features are used together!

Chapter 6: HTML-First Patterns

In this chapter, Scott focuses on how other people are using web components in the wild and highlights a few of the more interesting and smart patterns he’s seen.

Let’s start with a typical counter

It’s often the very first example used in React tutorials.

<counter-element></counter-element>

<script type="module">
  customElements.define('counter-element', class extends HTMLElement {
    #count = 0;
    connectedCallback() {
      this.innerHTML = `<button id="dec">-</button><p id="count">${this.#count}</p><button id="inc">+</button>`;
      this.addEventListener('click', e => this.update(e) );
    }
    update(e) {
      if( e.target.nodeName !== 'BUTTON' ) { return }
      this.#count = e.target.id === 'inc' ? this.#count + 1 : this.#count - 1;
      this.querySelector('#count').textContent = this.#count;
    }
  });
</script>

Reef

Reef is a tiny library by Chris Ferdinandi that weighs just 2.6KB minified and zipped yet still provides DOM diffing for reactive state-based UIs like React, which weighs significantly more. An example of how it works in a standalone way:

<div id="greeting"></div>

<script type="module">
  import {signal, component} from '.../reef.es..min.js';
  // Create a signal
  let data = signal({
    greeting: 'Hello',
    name: 'World'
  });
  component('#greeting', () => `<p>${data.greeting}, ${data.name}!</p>`);
</script>

This sets up a “signal” that is basically a live-update object, then calls the component() method to select where we want to make the update, and it injects a template literal in there that passes in the variables with the markup we want.

So, for example, we can update those values on setTimeout:

<div id="greeting"></div>

<script type="module">
  import {signal, component} from '.../reef.es..min.js';
  // Create a signal
  let data = signal({
    greeting: 'Hello',
    name: 'World'
  });
  component('#greeting', () => `<p>${data.greeting}, ${data.name}!</p>`);
  
  setTimeout(() => {
    data.greeting = '¡Hola'
    data,name = 'Scott'
  }, 3000)
</script>

We can combine this sort of library with a web component. Here, Scott imports Reef and constructs the data outside the component so that it’s like the application state:

<my-greeting></my-greeting>

<script type="module">
    import {signal, component} from 'https://cdn.jsdelivr.net/npm/reefjs@13/dist/reef.es.min.js';
    
    window.data = signal({
      greeting: 'Hi',
      name: 'Scott'
    });
    
    customElements.define('my-greeting', class extends HTMLElement {
      connectedCallback(){
        component(this, () => `<p>${data.greeting}, ${data.name}!</p>` );
      }
    });
</script>

It’s the virtual DOM in a web component! Another approach that is more reactive in the sense that it watches for changes in attributes and then updates the application state in response which, in turn, updates the greeting.

<my-greeting greeting="Hi" name="Scott"></my-greeting>

<script type="module">
  import {signal, component} from 'https://cdn.jsdelivr.net/npm/reefjs@13/dist/reef.es.min.js';
  customElements.define('my-greeting', class extends HTMLElement {
    static observedAttributes = ["name", "greeting"];
    constructor(){
      super();
      this.data = signal({
        greeting: '',
        name: ''
      });
    }
    attributeChangedCallback(name, oldValue, newValue) {
      this.data[name] = newValue;
    }
    connectedCallback(){
      component(this, () => `<p>${this.data.greeting}, ${this.data.name}!</p>` );
    }
  });
</script>

If the attribute changes, it only changes that instance. The data is registered at the time the component is constructed and we’re only changing string attributes rather than objects with properties.

HTML Web Components

This describes web components that are not empty by default like this:

<my-greeting></my-greeting>

This is a “React” mindset where all the functionality, content, and behavior comes from JavaScript. But Scott reminds us that web components are pretty useful right out of the box without JavaScript. So, “HTML web components” refers to web components that are packed with meaningful content right out of the gate and Scott points to Jeremy Keith’s 2023 article coining the term.

[…] we could call them “HTML web components.” If your custom element is empty, it’s not an HTML web component. But if you’re using a custom element to extend existing markup, that’s an HTML web component.

Jeremy cites something Robin Rendle mused about the distinction:

[…] I’ve started to come around and see Web Components as filling in the blanks of what we can do with hypertext: they’re really just small, reusable chunks of code that extends the language of HTML.

The “React” way:

<UserAvatar
  src="https://example.com/path/to/img.jpg"
  alt="..."
/>

The props look like HTML but they’re not. Instead, the props provide information used to completely swap out the <UserAvatar /> tag with the JavaScript-based markup.

Web components can do that, too:

<user-avatar
  src="https://example.com/path/to/img.jpg"
  alt="..."
></user-avatar>

Same deal, real HTML. Progressive enhancement is at the heart of an HTML web component mindset. Here’s how that web component might work:

class UserAvatar extends HTMLElement {
  connectedCallback() {
    const src = this.getAttribute("src");
    const name = this.getAttribute("name");
    this.innerHTML = `
      <div>
        <img src="${src}" alt="Profile photo of ${name}" width="32" height="32" />
        <!-- Markup for the tooltip -->
      </div>
    `;
  }
}
customElements.define('user-avatar', UserAvatar);

But a better starting point would be to include the <img> directly in the component so that the markup is immediately available:

<user-avatar>
  <img src="https://example.com/path/to/img.jpg" alt="..." />
</user-avatar>

This way, the image is downloaded and ready before JavaScript even loads on the page. Strive for augmentation over replacement!

resizeasaurus

This helps developers test responsive component layouts, particularly ones that use container queries.

<resize-asaurus>
  Drop any HTML in here to test.
</resize-asaurus>

<!-- for example: -->

<resize-asaurus>
  <div class="my-responsive-grid">
    <div>Cell 1</div> <div>Cell 2</div> <div>Cell 3</div> <!-- ... -->
  </div>
</resize-asaurus>
Five examples of grids with cells where the grid shows how wide it is.

lite-youtube-embed

This is like embedding a YouTube video, but without bringing along all the baggage that YouTube packs into a typical embed snippet.

<lite-youtube videoid="ogYfd705cRs" style="background-image: url(...);">
  <a href="https://youtube.com/watch?v=ogYfd705cRs" class="lyt-playbtn" title="Play Video">
    <span class="lyt-visually-hidden">Play Video: Keynote (Google I/O '18)</span>
  </a>
</lite-youtube>

<link rel="stylesheet" href="./src.lite-yt-embed.css" />
<script src="./src.lite-yt-embed.js" defer></script>

It starts with a link which is a nice fallback if the video fails to load for whatever reason. When the script runs, the HTML is augmented to include the video <iframe>.

Chapter 7: Web Components Frameworks Tour

Lit

Lit extends the base class and then extends what that class provides, but you’re still working directly on top of web components. There are syntax shortcuts for common patterns and a more structured approach.

The package includes all this in about 5-7KB:

  • Fast templating
  • Reactive properties
  • Reactive update lifecycle
  • Scoped styles
<simple-greeting name="Geoff"></simple-greeting>

<script>
  import {html, css, LitElement} from 'lit';
  
  export class SimpleGreeting extends LitElement {
    state styles = css`p { color: blue }`;
    static properties = {
      name: {type = String},
    };
                
    constructor() {
      super();
      this.name = 'Somebody';
    }
                
    render() {
      return html`<p>Hello, ${this.name}!</p>`;
    }
  }
        
  customElements.define('simple-greeting', SimpleGreeting);
</script>
Pros Cons
Ecosystem No official SSR story (but that is changing)
Community
Familiar ergonomics
Lightweight
Industry-proven

webc

This is part of the 11ty project. It allows you to define custom elements as files, writing everything as a single file component.

<!-- starting element / index.html -->
<my-element></my-element>

<!-- ../components/my-element.webc  -->
<p>This is inside the element</p>

<style>
  /* etc. */
</style>

<script>
  // etc.
</script>
Pros Cons
Community Geared toward SSG
SSG progressive enhancement Still in early stages
Single file component syntax
Zach Leatherman!

Enhance

This is Scott’s favorite! It renders web components on the server. Web components can render based on application state per request. It’s a way to use custom elements on the server side. 

Pros Cons
Ergonomics Still in early stages
Progressive enhancement
Single file component syntax
Full-stack stateful, dynamic SSR components

Chapter 8: Web Components Libraries Tour

This is a super short module simply highlighting a few of the more notable libraries for web components that are offered by third parties. Scott is quick to note that all of them are closer in spirit to a React-based approach where custom elements are more like replaced elements with very little meaningful markup to display up-front. That’s not to throw shade at the libraries, but rather to call out that there’s a cost when we require JavaScript to render meaningful content.

Spectrum

<sp-button variant="accent" href="components/button">
  Use Spectrum Web Component buttons
</sp-button>
  • This is Adobe’s design system.
  • One of the more ambitious projects, as it supports other frameworks like React
  • Open source
  • Built on Lit

Most components are not exactly HTML-first. The pattern is closer to replaced elements. There’s plenty of complexity, but that makes sense for a system that drives an application like Photoshop and is meant to drop into any project. But still, there is a cost when it comes to delivering meaningful content to users up-front. An all-or-nothing approach like this might be too stark for a small website project.

FAST

<fast-checkbox>Checkbox</fast-checkbox>
  • This is Microsoft’s system.
  • It’s philosophically like Spectrum where there’s very little meaningful HTML up-front.
  • Fluent is a library that extends the system for UI components.
  • Microsoft Edge rebuilt the browser’s Chrome using these components.

Shoelace

<sl-button>Click Me</sl-button>
  • Purely meant for third-party developers to use in their projects
  • The name is a play on Bootstrap. 🙂
  • The markup is mostly a custom element with some text in it rather than a pure HTML-first approach.
  • Acquired by Font Awesome and they are creating Web Awesome Components as a new era of Shoelace that is subscription-based

Chapter 9: What’s Next With Web Components

Scott covers what the future holds for web components as far as he is aware.

Declarative custom elements

Define an element in HTML alone that can be used time and again with a simpler syntax. There’s a GitHub issue that explains the idea, and Zach Leatherman has a great write-up as well.

Cross-root ARIA

Make it easier to pair custom elements with other elements in the Light DOM as well as other custom elements through ARIA.

Container Queries

How can we use container queries without needing an extra wrapper around the custom element?

HTML Modules

This was one of the web components’ core features but was removed at some point. They can define HTML in an external place that could be used over and over.

External styling

This is also known as “open styling.”

DOM Parts

This would be a templating feature that allows for JSX-string-literal-like syntax where variables inject data.

<section>
  <h1 id="name">{name}</h1>
  Email: <a id="link" href="mailto:{email}">{email}</a>
</section>

And the application has produced a template with the following content:

<template>
  <section>
    <h1 id="name"></h1>
    Email: <a id="link" href=""></a>
  </section>
</template>

Scoped element registries

Using variations of the same web component without name collisions.


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



from CSS-Tricks https://ift.tt/KtcNz9J
via IFTTT

Clear today!

With a high of F and a low of 50F. Currently, it's 67F and Clear outside. Current wind speeds: 12 from the Southeast Pollen: 0 S...