Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Fri, 02 Jan 2026 12:55:23 +0000 en-US hourly 1 https://wordpress.org/?v=6.9 225069128 A first look at the Web Install API https://frontendmasters.com/blog/a-first-look-at-the-web-install-api/ https://frontendmasters.com/blog/a-first-look-at-the-web-install-api/#respond Fri, 02 Jan 2026 12:55:23 +0000 https://frontendmasters.com/blog/?p=8142 Bruce Lawson:

I was excited to see that the proposed new Web Install API has entered Origin Trial in Chromium. It kind of works in Chromium Canary, but is most complete in Microsoft Edge beta […] The reason I was excited is because I read the Web install API explainer when it was announced a few months ago:

The Web Install API provides a way to democratise and decentralise web application acquisition, by enabling “do-it-yourself” end users and developers to have control over the application discovery and distribution process. It provides the tools needed to allow a web site to install a web app.

Here’s a pretty straightforward explainer site. Basically still PWAs (Progressive Web Apps) except you can offer a button-click to install them. There just might be proper cross-browser interest, but it sounds like Safari will be the hardest to get on board. But to be fair, they have their own “Add to Dock” thing.

]]>
https://frontendmasters.com/blog/a-first-look-at-the-web-install-api/feed/ 0 8142
!important and CSS Custom Properties https://frontendmasters.com/blog/important-and-css-custom-properties/ https://frontendmasters.com/blog/important-and-css-custom-properties/#respond Thu, 01 Jan 2026 16:20:06 +0000 https://frontendmasters.com/blog/?p=8128 This just bit me the other day so I’m going to write it down. Again, as it’s surprised me before. I just think I can maybe explain it even more clearly this time.

CSS custom properties are super permissive in what values are valid. Like this is totally fine, and I sure it can get much weirder:

--whats-up: (👍🏻ᴗ _ᴗ)👍🏻;

So my brain extends that to think that this also is a complete valid value:

--color: orange !important;

Like the value of --color is orange !important;

But it’s not! The value is just orange and the declaration itself is “important”. Hopefully this graphic makes it even more clear:

A graphic explaining CSS custom properties, highlighting the difference between the value and the declaration, with emphasis on the statement '--color: orange !important;' and clarifications in colorful text.

This can come up when there are multiple declarations that apply to the same element. Normally specificity and source order help sort out which declaration wins, but just as !important always does, an !important declaration trumps those other things.

So say you have a:

<div class="greeting">Hello</div>

Then two selector blocks:

div {
  --color: red !important;
}

.greeting {
  --color: blue;
  color: var(--color);
}

Even though --color is set to blue right there next to where it is used with a higher-specificity selector, the greeting will actually be red. If !important became part of the value, blue would have won because the custom property declaration is more specific and would have won. But it’s the custom property declaration itself that is important-ized and thus the red value wins.

]]>
https://frontendmasters.com/blog/important-and-css-custom-properties/feed/ 0 8128
Preserve State While Moving Elements in the DOM https://frontendmasters.com/blog/preserve-state-while-moving-elements-in-the-dom/ https://frontendmasters.com/blog/preserve-state-while-moving-elements-in-the-dom/#respond Wed, 31 Dec 2025 23:03:28 +0000 https://frontendmasters.com/blog/?p=8131 Bramus wrote this almost a year ago, but I’d still call it a relatively new feature of JavaScript and one very worth knowing about.

With Node.prototype.moveBefore you can move elements around a DOM tree, without resetting the element’s state.

You don’t need it to maintain event listeners, but, as Bramus notes, it’ll keep an iframe loaded, animations running, dialogs open, etc.

]]>
https://frontendmasters.com/blog/preserve-state-while-moving-elements-in-the-dom/feed/ 0 8131
How I Write Custom Elements with lit-html https://frontendmasters.com/blog/custom-elements-with-lit-html/ https://frontendmasters.com/blog/custom-elements-with-lit-html/#comments Mon, 29 Dec 2025 14:11:35 +0000 https://frontendmasters.com/blog/?p=8102 When I started learning more about web development, or more specifically about front-end frameworks, I thought writing components was so much better and more maintainable than calling .innerHTML() whenever you need to perform DOM operations. JSX felt like a great way to mix HTML, CSS, and JS in a single file, but I wanted a more vanilla JavaScript solution instead of having to install a JSX framework like React or Solid.

So I’ve decided to go with lit-html for writing my own components.

Why not use the entire lit package instead of just lit-html?

Honestly, I believe something like lit-html should be a part of vanilla JavaScript (maybe someday?). So by using lit-html, I basically pretend like it is already. It’s my go-to solution when I want to write HTML in JavaScript. For more solid reasons, you can refer to the following list:

  • Size difference. This often does not really matter for most projects anyway.)
    • lit-html – 7.3 kb min, 3.1 kb min + gzip
    • lit – 15.8 kb min, 5.9 kb min + gzip
  • LitElement creates a shadow DOM by default. I don’t want to use the shadow DOM when creating my own components. I prefer to allow styling solutions like Tailwind to work instead of having to rely on solutions like CSS shadow parts to style my components. The light DOM can be nice.
  • import { html, render } from "lit-html" is all you need to get started to write lit-html templates whereas Lit requires you to learn about decorators to use most of its features. Sometimes you may want to use Lit directives if you need performant renders but it’s not necessary to make lit-html work on your project.

I will be showing two examples with what I consider to be two distinct methods to create a lit-html custom element. The first example will use what I call a “stateless render” because there won’t be any state parameters passed into the lit-html template. Usually this kind of component will only call the render method once during its lifecycle since there is no state to update. The second example will use a “stateful render” which calls the render function every time a state parameter changes.

Stateless Render

For my first example, the custom-element is a <textarea> wrapper that also has a status bar similar to Notepad++ that shows the length and lines of the content inside the <textarea>. The status bar will also display the position of the cursor and span of the selection if any characters are selected. Here is a picture of what it looks like for those readers that haven’t used Notepad++ before.

A screenshot of a text editor displaying an excerpt about Lorem Ipsum, highlighting the text in yellow and showing line and character counts.

I used a library called TLN (“Textarea with Line Numbers”) to make the aesthetic of the textarea feel more like Notepad++, similar to the library’s official demo. Since the base template has no state parameters, I’m using plain old JavaScript events to manually modify the DOM in response to changes within the textarea. I also used the render function again to display the updated status bar contents instead of user .innerHTML() to keep it consistent with the surrounding code.

Using lit-html to render stateless components like these is useful, but perhaps not taking full advantage of the power of lit-html. According to the official documentation:

When you call render, lit-html only updates the parts of the template that have changed since the last render. This makes lit-html updates very fast.

You may ask: “Why should you use lit-html in examples like this where it won’t make that much of a difference performance wise? Since the root render function is really only called once (or once every connectedCallback()) in the custom elements lifecycle.”

My answer is that, yes, it’s not necessary if you just want rendering to the DOM to be fast. The main reason I use lit-html is that the syntax is so much nicer to me compared to setting HTML to raw strings. With vanilla JavaScript, you have to perform .createElement(), .append(), and .addEventListener() to create deeply nested HTML structures. Calling .innerHTML() = `<large html structure></>` is much better, but you still need to perform .querySelector() to lookup the newly created HTML and add event listeners to it.

The @event syntax makes it much more clear where the event listener is located compared to the rest of the template. For example…

class MyElement extends LitElement {
  ...
  render() {
    return html`
      <p><button @click="${this._doSomething}">Click Me!</button></p>
    `;
  }
  _doSomething(e) {
    console.log("something");
  }
}

It also makes it much more apparent to me on first glance that event.currentTarget can only be the HTMLElement where you attached the listener and event.target can be the same but also may come from any child of the said HTMLElement. The template also calls .removeEventListener() on its own when the template is removed from the DOM so that’s also one less thing to worry about.

The Status Bar Area

Before I continue explaining the change events that make the status bar work, I would like to highlight one of the drawbacks of the “stateless render”: there isn’t really a neat way to render the initial state of HTML elements. I could add placeholder content for when the input is empty and no selection was made yet, but the render() function only appends the template to the given root. It doesn’t delete siblings within the root so the status bar text would end up being doubled. This could be fixed if I call an initial render somewhere in the custom element, similar to the render calls within the event listeners, but I’ve opted to omit that to keep the example simple.

The input change event is one of the more common change events. It’s straightforward to see that this will be the change event used to calculate and display the updated input length and the number of newlines that the input has.

I thought I would have a much harder time displaying the live status of selected text, but the selectionchange event provides everything I need to calculate the selection status within the textarea. This change event is relatively new too, having only been a part of baseline last September 2024.

Since I’ve already highlighted the two main events driving the status bar, I’ll proceed to the next example.

Stateful Render

My second example is a <pokemon-card> custom-element. The pokemon card component will generate a random Pokémon from a specific pokemon TCG set. The specifications of the web component are as follows:

  • The placeholder will be this generic pokemon card back.
  • A Generate button that adds a new Pokémon card from the TCG set.
  • Left and right arrow buttons for navigation.
  • Text that shows the name and page of the currently displayed Pokémon.

In this example, only two other external libraries were used for the web component that weren’t related to lit and lit-html. I used shuffle from es-toolkit to make sure the array of cards is in a random order each time the component is instantiated. Though the shuffle function itself is likely small enough that you could just write your own implementation in the same file if you want to minimize dependencies.

I also wanted to mention es-toolkit in this article for readers that haven’t heard about it yet. I think it has a lot of useful utility functions so I included it in my example. According to their introduction, “es-toolkit is a modern JavaScript utility library that offers a collection of powerful functions for everyday use.” It’s a modern alternative to lodash, which used to be a staple utility library in every JavaScript project especially during the times before ES6 was released.

There are many ways to implement a random number generator or how to randomly choose an item from a list. I decided to just create a list of all possible choices, shuffle it, then use the pop method so that it’s guaranteed no card will get generated twice. The es-toolkit shuffle type documentation states that it “randomizes the order of elements in an array using the Fisher-Yates algorithm”.

Handling State using Signals

Vanilla JavaScript doesn’t come with a state management solution. While LitElement’s property and state decorators do count as solutions, I want to utilize a solution that I consider should be a part of Vanilla JavaScript just as with lit-html. The state management solution for the component will be JavaScript Signals. Unlike lit-html, signals are already a Stage 1 Proposal so there is a slightly better chance it will become a standard part of the JavaScript specification within the next few years.

As you can see from the Stage 1 Proposal, explaining JavaScript Signals from scratch can be very long that it might as well be its own multi-part article series so I will just give a rundown on how I used it in the <pokemon-card> custom-element. If you’re interested in a quick explanation of what signals are, the creator of SolidJS, which is a popular framework that uses signals, explains their thoughts here.

Signals need an effect implementation to work which is not a part of the proposed signal API, since according to the proposal, it ties into “framework-specific state or strategies which JS does not have access to”. I will be copy and pasting the watcher code in the example despite the comments recommending otherwise. My components are also too basic for any performance related issues to happen anyways. I also used the @lit-labs/signals to keep the component “lit themed” but you can also just use the recommended signal-polyfill directly too.

Signal Syntax

The syntax I used to create a signal state in my custom HTMLElement are as follows:

#visibleIndex = new Signal.State(0)

get visibleIndex() {
  return this.#visibleIndex.get()
}

set visibleIndex(value: number) {
  this.#visibleIndex.set(value)
}

There is a much more concise way to define the above example which involves auto accessors and decorators. Unfortunately, CodePen only supports TypeScript 4.1.3 as of writing, so I’ve opted to just use long-hand syntax in the example. An example of the accessor syntax involving signals is also shown in the signal-polyfill proposal.

Card Component Extras

The Intersection Observer API was used to allow the user to navigate the card component via horizontal scroll bar while also properly updating the state of the current page being displayed.

There is also a keydown event handler present to also let the user navigate between the cards via keyboard presses. Depending on the key being pressed, it calls either the handlePrev() or handleNext() method to perform the navigation.

Finally, while entirely optional, I also added a feature to the component that will preload the next card in JavaScript to improve loading times between generating new cards.

]]>
https://frontendmasters.com/blog/custom-elements-with-lit-html/feed/ 1 8102
Toggle `position: sticky` to `position: fixed` on Scroll https://frontendmasters.com/blog/toggle-position-sticky-to-position-fixed-on-scroll/ https://frontendmasters.com/blog/toggle-position-sticky-to-position-fixed-on-scroll/#comments Wed, 24 Dec 2025 14:41:32 +0000 https://frontendmasters.com/blog/?p=8090 It’s quite an unusual look when you see an element glide along it’s parent element as position: fixed;, the slide right on out of it, as if the positoning of it somehow magically changes at just the right moment, to position: sticky;. This is exactly what we’re going to pull of here with the help of scroll-driven animation and scroll state queries.

Both sticky and fixed positioning are about locking an element to a point on screen where it stays stuck throughout scrolling. A sticky element is stuck within its scrollable ancestor, and a fixed element sticks to the viewport. Both great for user interfaces that have to be persistent, like alert banners. They also make for nice visual effects.

Switching between these two types of position can give the illusion of an element breaking out of its scrollable container while the user is scrolling the page. Here’s an example:

Let’s see the mechanism behind that change.

The Layout

<div class="scrollPort">

  <div class="visualBlock">
    <div class="stickyElement"></div>
  </div>

  <!-- more blocks -->

</div>
.scrollPort {
  /* etc. */

  overflow-y: auto;

  .visualBlock {
    /* etc. */

    .stickyElement {
      position: sticky;
      top: 40px;
    }
  }
}

The .scrollPort is a scroll container with a set of .visualBlocks that overflow the container. Each .visualBlock has a sticky element inside.

Sizing the Sticky Element

Fixed units for the dimensions of the sticky element won’t be a problem, but if they have to be relative, there are some precautions to take.

.visualBlock {
  /* etc. */
  
  container-type: inline-size;

  .stickyElement {
    /* etc. */
    
    /* Sets the width to 80% of the query container's (.visualBlock) width */
    width: 80cqw;

  }
}

We can’t use a percentage (like 80%) to size the sticky element relative to its parent, because the reference element for a percentage unit is its nearest parent, which changes when the element goes from sticky to fixed*.

*A fixed element’s reference point in a document flow is the viewport.

To use the same reference for relatively sizing the sticky element, even when it becomes fixed, use container query units:

  1. Establish the .visualBlock as an inline-size* query container
  2. Use cqw unit for .stickyElement’s width

*In horizontal writing, the width is along the inline axis.

With sizing done, we move onto the code to change the position value.

Method 1: Using Scroll-Driven Animation

We use CSS view() function to run a keyframe animation that’ll turn .stickyElement from sticky to fixed.

.visualBlock {
  /* etc. */

  --stickyPosition: sticky;

  animation: toFixed;
  animation-timeline: view(block 0% 100%);


  .stickyElement {
    /* etc. */
    
    position: var(--stickyPosition); /* previously, position: sticky; */
  }
}

@keyframes toFixed {
  to { 
    --stickyPosition: fixed; 
  }
}

The parts above:

  • --stickyPosition: sticky; — Set a CSS variable in .visualBlock with an initial value of sticky. This value is used by .stickyElement to set its position.
  • animation: toFixed; — Apply the CSS animation toFixed (explained later) to .visualBlock.
  • animation-timeline: view(block 0% 100%); — The animation’s progress is based on .visualBlock’s visibility within .scrollPort. It starts when .visualBlock scrolls into view (0%) and ends (100% progress) when it scrolls out of view.
  • toFixed — At the end* (to) of the animation progress set --stickyPosition to fixed.

*The position CSS property is discrete. When animated, it changes from its start to end value halfway through the animation.

We’re not done yet, but here’s how it works when toFixed animation is applied through view():

A couple of things to take care of. First, when .stickyElement turns fixed it shifts slightly, since its top is no longer relative to .visualBlock. Needs reassigning the correct top value to prevent the shift.

Second, .stickyElement reverts to sticky when its .visualBlock goes off-screen, which is too soon since we want it to reach the next .stickyElement. Time to expand the area tracked for the view timeline to include the space between .visualBlocks and above .stickyElement.

I’ll keep these values is CSS variables for ease of update.

.scrollPort {
  /* etc. */

  container-type: size;

  .visualBlock {
    /* etc. */

    --visualBlockMargin: 60px;
    --stickyPosition: sticky;
    --stickyMarginTop: 50px;
    --stickyTopTemp: 40px;
    --stickyTop: var(--stickyTopTemp);

    margin: var(--visualBlockMargin) auto; 
    /* the space between .visualBlocks */

    animation: toFixed;
    animation-timeline: view(block calc(-1 * (var(--visualBlockMargin) + var(--stickyMarginTop))) 100%);
    /* includes the space above .visualBlock and .stickyElement */

    .stickyElement {
      /* etc. */

      margin: var(--stickyMarginTop) auto auto; 
     /* the space above .stickyElement */

      position: var(--stickyPosition);
      top: var(--stickyTop);
    }
  }
}

@keyframes toFixed {
  to {
    --stickyPosition: fixed;
    --stickyTop: calc(50vh - 50cqh + var(--stickyTopTemp) - var(--stickyMarginTop));
    /* includes the space above .scrollPort and .stickyElement */
  }
}

Negative inset values in view() expand the element’s visibility range outward from the boundary edges.

Here’s the result:

This is the method used in our first example, shown at the beginning of the article.

Method 2: Using Scroll State Queries

The second method, using scroll state queries, is the most efficient way to achieve what we want. The only downside is that scroll state queries are not widely supported by browsers yet.

We don’t need a keyframe animation for this one. What we need is a sticky scroll state container.

<div class="scrollPort">

  <div class="visualBlock">
    <div class="stickyWrapper">
      <div class="stickyElement"></div>
    </div>
  </div>

  <!-- more visual blocks -->

</div>
.stickyWrapper {
  /* etc. */

  container-type: scroll-state;

  position: sticky;
  --stickyTop: 40px;
  top: var(--stickyTop);

  .stickyElement {
      /* etc. */
   }
}

A scroll state container lets its descendants use scroll state queries to apply styles based on the container’s scrolling state.

That’s why we use a .stickyWrapper to provide the sticky positioning and be used as the scroll state query container.

When .stickyWrapper gets stuck, we’ll turn its child, .stickyElement, to fixed.

@container scroll-state(stuck: top) {
  .stickyElement {
    position: fixed;
    top: calc(50vh - 50cqh + var(--stickyTop));
  }
}

Here’s how it looks:

As you can see, this method requires much less code in CSS. But since view() is widely supported at the moment, compared to scroll state queries, it’s good to have the first method available, too. Choose whichever method or design you want. The key for this to work is to simply maintain the right size and position for the element when it shifts back and forth between its sticky and fixed behavior to look like it’s moving between the visual blocks.

Uses and Variants

If there’s a visual element that’s not to be unnecessarily shown to the user right off the bat, but once shown could be useful to keep it on screen, toggling its position like the examples in this post might do the trick. It could be a call-to-action button, or a banner, or it could be graphics moving between slides in a presentation once a particular slide is shown.

On top of the position change, if other visual changes are layered, that opens up even more variations for how this can play out.

As mentioned before, focus on where and how you want the element to appear when its sticky and when its fixed, for the desired effect to come through as the position changes on scroll.

]]>
https://frontendmasters.com/blog/toggle-position-sticky-to-position-fixed-on-scroll/feed/ 3 8090
Exploring Multi-Brand Systems with Tokens and Composability https://frontendmasters.com/blog/exploring-multi-brand-systems-with-tokens-and-composability/ https://frontendmasters.com/blog/exploring-multi-brand-systems-with-tokens-and-composability/#respond Fri, 19 Dec 2025 16:59:44 +0000 https://frontendmasters.com/blog/?p=8041 Design systems aren’t just about keeping everything looking the same; they’re about making things flexible enough to handle whatever gets thrown at them.

When you’re juggling multiple brands or different use‑cases, a rigid component can feel more limiting than helpful. That’s where tokens, composition, and configuration come in. Tokens let you swap out brand colors and styles without rewriting code, composition gives you building blocks you can remix into new layouts, and configuration keeps all those variations tidy and predictable. Put them together, and suddenly a single component can stretch to fit diverse brands, layouts, and experiences all without breaking from the system.

Let’s look at a typical “Card” component that is designed with all three of these things in mind such that it can support usage across different brands without breaking a sweat.

Setting up the Basic Card Structure

Say our project calls for a simple card. The card has a toggle-able banner that accepts a text string, a thumbnail image, title, description, and a button.

A card showcasing Red Rocks Park and Amphitheatre with a featured location banner, a vibrant image of a concert, title, description, and a button inviting users to view events.

In this article, all examples are written in Vue, but these principles are not Vue specific. They can be used in any design system that builds from components, even native Web Components.

Our component should end up looking something like this:

<template>
  <article class="system-card">
    <div class="system-card-media">
      <span v-if="bannerText" class="system-card-banner">
        {{ bannerText }}
      </span>
      <img :src="imageUrl" :alt="imageAlt" class="system-card-image" />
    </div>

    <div class="system-card-content">
      <h3 class="system-card-title">{{ title }}</h3>
      <p class="system-card-description">{{ description }}</p>
      <button class="system-card-button">{{ buttonText }}</button>
    </div>
  </article>
</template>

<script setup>
defineOptions({ name: "SystemCard" });

defineProps({
  bannerText: { type: String, default: null },
  imageUrl: { type: String, default: "<https://via.placeholder.com/350x150>" },
  imageAlt: { type: String, default: "Card Image" },
  title: { type: String, default: "Card Title" },
  description: {
    type: String,
    default:
      "This is a description of the card content. It provides more details about the card.",
  },
  buttonText: { type: String, default: "Learn More" },
});
</script>

<style scoped>
.system-card {
  background: #fff;
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.system-card-media {
  display: flex;
  flex-direction: column;
}

.system-card-banner {
  background-color: #a3533c;
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  padding: 4px;
  text-align: center;
  width: 100%;
}

.system-card-content {
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* ... other card styling, like .system-card-button */
</style>

The Card is then used like this:

<SystemCard
  bannerText="Featured Location"
  imageUrl="image/of/red-rock.jpg"
  imageAlt="Red Rocks Amphitheater during a night concert"
  title="Red Rocks Park and Amphitheater"
  description="There's No Better Place to See The Stars."
  buttonText="View Events"
/>

Supporting Multiple Brands/Themes with Tokens

At the most basic level, we can modify this card to support different brands or themes through the use of tokens or variables. I will be using CSS Custom Properties (variables) for all examples in this article, but tokens are not limited to just CSS, you can learn more about Design Tokens from the Design Tokens Community Group (which just shipped their first stable version!)

All of our existing markup stays the same, but we need to modify the CSS to use variables for configuration. While we’re at it, we should also look at any duplicate or hard-coded values and convert those to variable as well to maintain clean, reusable code.

Our new style declarations should look like this:

<style scoped>
/* These root variables are likely to be set up in a more global stylesheet */
:root {
  --system-card-bg-color: #ffffff;
  --system-card-accent-bg-color: #a3533c;
  --system-card-accent-text-color: #ffffff;

  --system-card-border-radius: 8px;
  --system-card-padding: 16px;
  --system-card-gap: 8px;
}

.system-card {
  background: var(--system-card-bg-color);
  border-radius: var(--system-card-border-radius);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.system-card-media {
  display: flex;
  flex-direction: column;
}

.system-card-banner {
  background-color: var(--system-card-accent-bg-color);
  color: var(--system-card-accent-text-color);
  font-size: 14px;
  font-weight: 600;
  padding: 4px;
  text-align: center;
  width: 100%;
}

.system-card-content {
  padding: var(--system-card-padding);
  display: flex;
  flex-direction: column;
  gap: var(--system-card-gap);
}
...
</style>

Now that our component is using tokens, we can create a custom class and assign those tokens new values. Simply by swapping from statically assigned values to dynamic tokens; CSS Custom Properties in this case, we have enabled our card component to support different visual themes.

<SystemCard
  class="custom-blue-card"
  bannerText="Featured Location"
  imageUrl="image/of/shedd-aquarium.jpg"
  imageAlt="Underwater tunnel at the Shedd Aquarium"
  title="Shedd Aquarium"
  description="Look Nature in the Eye."
  butonText="Plan Your Visit"
/>
.custom-blue-card {
  --system-card-accent-bg-color: #328198;
  --system-card-accent-text-color: #00070B;
}

Alternatively, we could add a new property on the component that assigns a class with new token definitions if we have a known set of themes we want to support:

<SystemCard
  bannerText="Featured Location"
  imageUrl="image/of/shedd-aquarium.jpg"
  imageAlt="Underwater tunnel at the Shedd Aquarium"
  title="Shedd Aquarium"
  description="Look Nature in the Eye."
  butonText="Plan Your Visit"
  theme="light-blue"
/>
/* These would likely be global overrides in a global stylesheet */
.theme--light-blue {
  --system-card-accent-bg-color: #328198;
  --system-card-accent-text-color: #00070B;
}
Card displaying Shedd Aquarium with a featured location banner, an underwater scene, title 'Shedd Aquarium', description 'Look Nature in the Eye.' and a button 'Plan Your Visit'.

Customizing Content with Composable Slots

A description is great, but what if we want to show a list of details instead? In a situation like this we have two options, either maintain unique code for every different variation of the component, or create composable areas within a single component that engineers can write custom code into, in programming these are often referred to as slots.

Taking our code from before:

<template>
  <article class="system-card">
    <div class="system-card-media">
      <span v-if="bannerText" class="system-card-banner">{{ bannerText }}</span>
      <img :src="imageUrl" :alt="imageAlt" class="system-card-image" />
    </div>
    <div class="system-card-content">
      <h3 class="system-card-title">{{ title }}</h3>
      <p class="system-card-description">{{ description }}</p>
      <button class="system-card-button">{{ buttonText }}</button>
    </div>
  </article>
</template>

We can modify our code to replace the .system-card-description element with a slot, we’ll use the name card-details to identify what we expect the contents of this slot to be.

<template>
  <article class="system-card">
    <div class="system-card-media">
      <span v-if="bannerText" class="system-card-banner">{{ bannerText }}</span>
      <img :src="imageUrl" :alt="imageAlt" class="system-card-image" />
    </div>
    <div class="system-card-content">
      <h3 class="system-card-title">{{ title }}</h3>
      <slot name="card-details" />
      <button class="system-card-button">{{ buttonText }}</button>
    </div>
  </article>
</template>

For the existing cards with a tagline we can simply place the .system-card-description element within the slot to achieve the same result.

<SystemCard
  bannerText="Featured Location"
  imageUrl="image/of/shedd-aquarium.jpg"
  imageAlt="Underwater tunnel at the Shedd Aquarium"
  title="Shedd Aquarium  butonText="Plan Your Visit"
  theme="light-blue"
>
<!-- This is our slot -->
<p class="system-card-description">Look Nature in the Eye.</p>
<!-- This is our slot -->
</SystemCard>
A card component showcasing the Shedd Aquarium with a featured location banner, an underwater scene, and a call-to-action button.

By creating a slot however, we’ve now opened up the possibility for custom content within in. The team can now create a snippet of code to display the list of highlights producing a unique variation on the existing component without completely breaking away from the system.

Component usage could then look like this:

<SystemCard
  bannerText="Featured Location"
  imageUrl="image/of/shedd-aquarium.jpg"
  imageAlt="Underwater tunnel at the Shedd Aquarium"
  title="Shedd Aquarium  butonText="Plan Your Visit"
  theme="light-blue"
>
  <!-- This is our slot -->
  <ul class="highlight-list">
    <li class="highlight-item">
      <p class="highlight-text">Touch Experiences</p>
    </li>
    <li class="highlight-item">
      <p class="highlight-text">Animal Encounters</p>
    </li>
    <li class="highlight-item">
      <p class="highlight-text">Stingray Feedings</p>
    </li>
  </ul>
  <!-- This is our slot -->
</SystemCard>

Producing a result like this:

A card design showcasing the Shedd Aquarium with a featured location banner, an underwater scene, and a list of available experiences including 'Touch Experiences', 'Animal Encounters', and 'Stringray Feedings'. Below, there is a button labeled 'Plan Your Visit'.

A Note on Keeping Things Organized

The concept of completely free and open slots is likely terrifying to a designer or engineer who is focused on maintaining clean and organized code since now we’re allowing people to add whatever custom work they want to an area.

To avoid this we can provide “ready-made” child components that we would prefer teams use in these areas, whether through a predetermined list of parts that we know consumers will want, or by paying attention to and adopting repeated usage patterns.

In our examples so far we know we want to support system-card-description and a new system-card-list as options for this space; we can create those as smaller components or “partials.” Engineering and design teams can then use those formally adopted and verified options when they fit their needs, and they maintain all of the benefits of using system components over needing to create custom solutions.

Using these partials might look like this:

<SystemCard
  bannerText="Featured Location"
  imageUrl="image/of/shedd-aquarium.jpg"
  imageAlt="Underwater tunnel at the Shedd Aquarium"
  title="Shedd Aquarium  butonText="Plan Your Visit"
  theme="light-blue"
>
  <!-- This is our slot -->
  <SystemCardDescription text="Look Nature in the Eye.">
  <!-- This is our slot -->
</SystemCard>

or:

<SystemCard
  bannerText="Featured Location"
  imageUrl="image/of/shedd-aquarium.jpg"
  imageAlt="Underwater tunnel at the Shedd Aquarium"
  title="Shedd Aquarium  butonText="Plan Your Visit"
  theme="light-blue"
>
  <!-- This is our slot -->
  <SystemCardList items="[array of items]">
  <!-- This is our slot -->
</SystemCard>

Extending Configuration and Composability for Further Customization

Using slots and partials for configuration and composition is not limited to single areas within a component either, once you start thinking in this model you can create incredibly flexible components that can support a vast array of different styles and layouts.

Examining our card component through the lenses of composition and configuration we can create a layout like this.

An aerial view of the Adler Planetarium showcasing its unique dome structure and surrounding landscape, featuring a prominent banner reading 'Escape to the Stars'. Below, there is a section displaying the title 'Adler Planetarium' and a list of upcoming events.

The first thing we need to do is identify what pieces are configurable and what pieces are composable. Configurable elements are typically controlled through attributes, where composable sections are typically slots.

In our case the location of the .card-media__banner can be configurable to either the top or bottom of the image.

As for composition, we’ve taken the entire system-card-content area and turned it into a slot, allowing users of the component to build out whatever layout meets their needs. In this case we’re putting the button first, followed by the title, a list of details, and social links..

The component code now looks like this:

<template>
  <article class="system-card">
    <CardMedia
      :bannerText="bannerText"
      :bannerLocation="bannerLocation"
      :imageUrl="imageUrl"
      :imageAlt="imageAlt"
    />
    <div class="system-card-content">
      <slot name="card-content" />
    </div>
  </article>
</template>

<script setup>
import CardMedia from "./partials/CardMedia.vue";

defineOptions({ name: "SystemCard" });

defineProps({
  bannerText: { type: String, default: null },
  bannerLocation: { type: String, default: "top" },
  imageUrl: { type: String, default: "<https://via.placeholder.com/350x150>" },
  imageAlt: { type: String, default: "Card Image" },
  title: { type: String, default: "Card Title" },
  description: {
    type: String,
    default:
      "This is a description of the card content. It provides more details about the card.",
  },
  buttonText: { type: String, default: "Learn More" },
});
</script>

and in use:

<SystemCard
  bannerText="Escape to the Stars"
  bannerLocation="bottom"
  imageUrl="image/of/adler-planetarium.jpg"
  imageAlt="Aerial view or Adler Planetarium"
>
  <SystemButton type="filled" text="Join Us" iconEnd="arrow-right" />
  <SystemCardTitle text="Adler Planetarium" />
  <SystemCardList
    title="Upcoming Events and Shows"
    items="[array of items]">
    <SystemCardSocials
      facebook="link.to.social"
      twitter="link.to.social"
      youtube="link.to.social"
      instagram="link.to.social"
    />
  </SystemCardList>
</SystemCard>

We’ve moved the banner and image to a new partial we’re importing called CardMedia that partial is then passed the prop bannerLocation and determines whether the banner should appear on the top of bottom of the image. Now, because our entire content area is a slot we’ve added multiple elements as children of the SystemCard; we’ve got a SystemButton, SystemCardTitle, SystemCardSocial, and our SystemCardList from before, in a new layout that we have defined ourselves all while continuing to use the design system without breaking.

Our final result is a highly adaptable card that supports all of our examples through the use of configuration through props and configuration through custom slots.

Three card components showcasing different featured locations with images, titles, descriptions, and action buttons.

Turning Principles into Practice

Supporting multiple brands and use‑cases doesn’t have to mean duplicating components or maintaining endless forks of code. By grounding your system in tokens, composition, and configuration, you can keep one core component flexible enough to handle divergent needs.

  • Tokens: Centralize design decisions like color, spacing, and typography so brand shifts are a matter of swapping variables or themes, not rewriting CSS.
  • Composition (slots/partials): Create structured areas where teams can plug in approved variations, reducing the need for custom one‑offs while still allowing an escape hatch when needed.
  • Configuration (props/attributes): Expose common and repeated options for styles, layouts, and behavior so components adapt without breaking consistency.

The payoff is huge: fewer bespoke components to maintain, faster delivery across brands, and a system that scales without losing cohesion. Instead of fighting divergence, you’re designing for it, and that’s how systems stay resilient in the real world.

]]>
https://frontendmasters.com/blog/exploring-multi-brand-systems-with-tokens-and-composability/feed/ 0 8041
Liquid Glass in the Browser https://frontendmasters.com/blog/liquid-glass-in-the-browser/ https://frontendmasters.com/blog/liquid-glass-in-the-browser/#respond Thu, 18 Dec 2025 16:54:54 +0000 https://frontendmasters.com/blog/?p=8065 There was a quick surge of people re-creating liquid glass in the browser shortly after Apple’s debut of the aesthetic. Now that it’s settled in a bit, there have been some slower more detailed studies.

  • Chris Feijoo leaned into SVG because of the advanced filtering abilities and has the most faithful looking demos I’ve seen yet.
  • Chester Li used the all-powerful WebGL shaders to get some of the most unusual effects like chromatic aberration and distortion. (Note the blog is literally Notion, which I’m not sure I’ve seen anyone lean into quite like that before.)
]]>
https://frontendmasters.com/blog/liquid-glass-in-the-browser/feed/ 0 8065
Different Page Transitions For Different Circumstances https://frontendmasters.com/blog/different-page-transitions-for-different-circumstances/ https://frontendmasters.com/blog/different-page-transitions-for-different-circumstances/#comments Wed, 17 Dec 2025 01:40:39 +0000 https://frontendmasters.com/blog/?p=8067 I feel like common usage for multi-page view transitions is to set up a general system for them that applies generally to all pages and elements, then let it ride.

But I just recently saw the DOM events in JavaScript and how they can be used to set a “type”. So check out the events first:

// The old / unloading page
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
 
  }
}

// the new / loading page
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
 
  }
}

You can do anything you might want in there, but an especially interesting thing to me is that you can set the view transition type, and do so conditionally.

Customize the View Transition Type for One Particular URL

Just to clearly illustrate the point, say you want one particular page to have a different transition animation than all the rest of them. Says it’s the “Shows” page on a website at the relative URL /shows. Then we’d watch for the pagereveal event and test that URL and if it’s a match we’ll set the type:

window.addEventListener("pagereveal", async (e) => {
  if (
    e.viewTransition && 
    document.location.pathname === "/shows"
  ) {
    e.viewTransition.types.add("toShowsPage");
  }
});

That toShowsPage is just an arbitrary name we’re making up to use in CSS to customize the animation when it’s set.

The “Default” View Transition

We’ve got a custom type being set, but let’s set up the default first. Something like this is neat:

::view-transition-old(main) {
  animation-name: slide-out-to-left;
  animation-duration: 1s;
}
::view-transition-new(main) {
  animation-name: slide-in-from-right;
  animation-duration: 1s;
}

@keyframes slide-out-to-left {
  to {
    translate: -150px 0;
    opacity: 0;
    scale: 0.5;
  }
}
@keyframes slide-in-from-right {
  from {
    translate: 100vi 0;
  }
}

In my example here, it assumes a <main> content area with view-transition-name: main; so that is the element being targeted specifically here. Now when I move pages (by just clicking regular ol’ links) I get this effect:

Using the Custom Type for a Custom Animation

When the “Shows” link is clicked and the /shows page is loaded, we’re setting the “toShowsPage” type, and this is the magic moment we can use it in CSS:

html:active-view-transition-type(toShowsPage) {
  &::view-transition-new(main) {
    animation: to-shows-page 1s forwards;
  }
}

@keyframes to-shows-page {
  from {
    scale: 1.1;
    translate: 0 -200px;
  }
}

Because of the extra specificity over just ::view-transition-new, this gives us an opportunity to override the default animation here with a new set of keyframes. Now just the Shows page will come down from the top instead. See the difference:

Notes

I think it’s cool we have this level of control and interplay between JavaScript and CSS.

I first saw this in Bramus’ Cross-document view transitions for multi-page applications, which is a good resource and covers “forwards”, “backwards”, and “reload” view transition types which seems extremely practical and makes me wish were something we have native CSS access to detect.

There is a native CSS way to declare the types, but I’m not quite understanding why that is useful or important to do. All I understand so far is that any type that isn’t listed there when you do declare them invalidates them, so maybe that’s useful somehow?

I would have thought the “types” stuff would have been a bit newer, and thus lower browser support, than other view transitions stuff, but that’s wrong. MDN has JavaScript type setting as well as CSS :active-view-transition-type() as the same level of browser support as multi-page view transitions in general, that is to say, Chrome, Safari, and flagged in Firefox.

]]>
https://frontendmasters.com/blog/different-page-transitions-for-different-circumstances/feed/ 1 8067
Default parameters: your code just got smarter https://frontendmasters.com/blog/default-parameters-your-code-just-got-smarter/ https://frontendmasters.com/blog/default-parameters-your-code-just-got-smarter/#respond Fri, 12 Dec 2025 15:22:11 +0000 https://frontendmasters.com/blog/?p=8036 Matt Smith with wonderfully straightforward writing on why default parameters for functions are a good idea. I like the tip where you can still do it with an object-style param.

function createUser({ name = 'Anonymous', age = 24 } = {}) {
  console.log(`${name} is ${age} years old.`);
}

createUser(); // Anonymous is 24 years old.

]]>
https://frontendmasters.com/blog/default-parameters-your-code-just-got-smarter/feed/ 0 8036
Thoughts on Native CSS Mixins https://frontendmasters.com/blog/thoughts-on-native-css-mixins/ https://frontendmasters.com/blog/thoughts-on-native-css-mixins/#respond Thu, 11 Dec 2025 22:29:27 +0000 https://frontendmasters.com/blog/?p=8020 I have some notes from various times I’ve thought about the idea of native CSS mixins so I figured I’d get ’em down on (digital) paper!

For the record, they don’t really exist yet, but Miriam Suzanne says:

The CSS Working Group has agreed to move forward with CSS-native mixins.

And there is a spec, but the spec only deals with @function (which does exist). Functions are a little similar but act only as a single value rather than a block of styles.

The idea comes from Sass @mixin.

We happen to use Sass (SCSS) at CodePen and as I write, we have 328 @mixin definitions in the codebase, so it’s clearly of use.

Here’s a practical-if-basic example:

@mixin cover {
  position: absolute;
  inset: 0;
}

In Sass, that doesn’t compile to anything. You have to use it. Like:

.modal-overlay {
  @include cover;
}

.card.disabled {
  &::before {
    @include cover;
    background: lch(0% 0 0 / 0.8);
  }
}

See how I’ve used it twice above. Compiled Sass will dump in the contents of the mixin in both places:

.modal-overlay {
  position: absolute;
  inset: 0;
}

.card.disabled {
  &::before {
    position: absolute;
    inset: 0;
    background: lch(0% 0 0 / 0.8);
  }
}

Things can get a little fancier in Sass, but it’s all pretty straightforward:

  • Mixins can include nesting and work in nested code. They can even slot in nested content you pass to it.
  • Mixins can use other mixins.
  • Mixins can have parameters (like a function) and use/calculate off those values in the output.

I would assume and hope that all of this is supported in native CSS mixins. The native version, as explained so far on Miriam’s site (which will almost definitley change!), the only difference is the usage syntax:

@mixin --cover {
  position: absolute;
  inset: 0;
}
.modal-overlay {
  @apply --cover;
}

I imagine it’s @apply instead of @include literally because Sass uses @include and Sass would have a hard time “leaving it alone” when processing down to CSS.

Is there enough here for browsers/standards to actually do it?

The W3C CSS Working Group has already OK’d the idea of all this, so I assume it’s already been determined there is value to native CSS having this ability at all. But what are those reasons?

  • Not having to reach for a preprocessor tool like Sass. I don’t think this is enough of a reason all by itself for them, but personally, I do. This is a paved cowpath, as they say.
  • Preprocessor output has potentially a lot of duplicate code. This leads to bigger CSS files. Perhaps not a huge issue with gzip/brotli in play, but still, smaller files is almost always good.
  • Integration with --custom-properties. I would think the parameters could be custom properties and there could be custom properties used generally with the style block. Custom properties can change dynamically, causing re-evaluated styles, so mixins can become more powerful expressions of style based on a comparatively small custom property change.
  • Custom Properties can cascade and be different values at different points in the DOM, so mixins might also do different things at different points in the DOM. All this custom property stuff would be impossible in a preprocessor.
  • It’s a nicer API than faking it with @container style(). You can test a custom property with a style query and dump out styles in certain places now, but it doesn’t feel quite right.

I wonder what else tipped the scales toward the working group doing it.

Parameter handling seems tricky.

You can pass things to a mixin, which I think is pretty crucial to their value.

@mixin --setColors(--color) {
  color: var(--color);
  background-color: oklch(from var(--color) calc(l - 40%) c h / 0.9);
}

But things can get weird with params. Like what happens if you call setColors() with no params? Does it just fail and output nothing?

.card {
  @apply --setColors(); /* ??? */
}

It’s possible --color is set anyway at the cascade level it’s being used at, so maybe it has access to that and outputs anyway? I assume if --color is set at the same cascade level and the param is passed, the param value wins? How does !important factor in?

And what about typed params? And default values? Seems doable but quite verbose feeling, especially for CSS. Is it like…

@mixin --setColors(
  --color type(color): red
) {
  color: var(--color);
  background-color: oklch(from var(--color) calc(l - 40%) c h / 0.9);
}

Maybe like that? I’m not sure what the syntax limitations are. Or maybe we don’t need default values at all because the var() syntax supports fallbacks already?

Feels like it could open up a world of more third-party CSS usage.

Imagine CSS carousels. They are so cool. And they are quite a bit of CSS code. Perhaps their usage could be abstracted into a @mixin.

The jQuery days were something like this pseudo-code:

// <script src="/plugins/owl-carousel.js"></script>
$(".owl-carousel").owlCarousel({
  gap: 10, 
  navArrows: true,
  navDots: true
});

Which morphed into JavaScript components:

@import SlickCarousel from "slickcarousel";

<SlickCarousel
  gap="10"
  navArrows={true}
  navDots={true}
/>

Maybe that becomes:

@import "/node_modules/radcarousel/carousel.css";

.carousel {
  @apply --radCarousel(
    --gap: 10px,
    --navArrows: true,
    --navDots: true
  );
}

The jQuery version was DIY HTML and this would be too. You could call that SSR for free, kids.

What about “private” variables?

I sort of remember Miriam talking about this at CSS Day this past year. I think this was the issue:

@mixin --my-thing {
  --space: 1rem;
  gap: var(--space);
  margin: var(--space);
}

.card {
  @apply --my-thing;
  padding: var(--space); /* defined or not? */
}

The question is, does that --space custom property “leak out” when you apply the mixin and thus can be used there? It either 1) does 2) doesn’t 3) some explicit syntax is needed.

I can imagine it being useful to “leak” (return) them, so say you wanted that behavior by default, but the option to not do that. Maybe it needs to be like…

@mixin --my-thing {
  @private {
    --space: 1rem;
  }
  gap: var(--space);
  margin: var(--space);
}

Don’t hate it. Miriams post also mentions being more explicit about what is returned like using an @output block or privatizing custom properties with a !private flag.

What about source order?

What happens here?

@mixin --set-vars {
  --papaBear: 30px;
  --mamaBear: 20px;
  --babyBear: 10px;
}

.card {
  --papaBear: 50px;
  @apply --set-vars;
  margin: var(--papaBear);
}

What margin would get set here? 50px because it’s set right there? 30px because it’s being overridden by the mixin? What if you reversed the order of the first two lines? Will source order be the determining factor here?

Are Custom Idents required?

All the examples use the --my-mixin style naming, with the double-dashes in front, like custom properties have. This type of using is called a “custom ident” as far as I understand it. It’s what custom functions are required to use, and they share the same spec, so I would think it would be required for mixins too.

/* 🚫 */
@mixin doWork {
}

/* ✅ */
@mixin --doWork {
}

Is this just like the way forward for all custom named things forever in CSS? I think it’s required for anchor names too, but not container names? I wish it was consistent, but I like backwards compatibility better so I can live.

Wouldn’t it be better if it was required for keyframes, for example? Like if you saw this code below, is it obvious what the user-named word is and what other things are language syntax features?

.leaving {
  animation: slide 0.2s forwards;
}

It’s slide here, so you’d have to go find it:

@keyframes slide {
  to { translate: -200px 0; }
}

To me it would be much more clear if it was:

.leaving {
  animation: --slide 0.2s forwards;
}
@keyframes --slide {
  to { translate: -200px 0; }
}

Annnnnnnd there is nothing really stopping us from doing that so maybe we should. Or take it one step further and adopt an emoji naming structure.

Calling Multiple Mixins

Would it be like?

@apply --mixin-one, --mixin-two;

Maybe space-separated?

@apply --mixin-one --mixin-two;

Or that is weird? Maybe you just gotta do it individually?

@apply --mixin-one;
@apply --mixin-two;

Does it matter?

Functions + Mixins

It seems to make sense that a mixin could call a function…

@mixin --box {
  gap: --get-spacing(2);
  margin-trim: block;
  > * {
    padding: --get-spacing(4);
  }
}

But would it be forbidden the other way around, a function calling a mixin?

@function --get-spacing(--size) {
  @apply get-vars(); /* ??? */
  result: 
    if (
      style(--some-other-var: xxx): 3rem;
      style(--size: 2): 1rem;
      style(--size: 4): 2rem;
      else: 0.5rem;
    )
}

Or is that fine?

Infinite Loops

Is it possible this opens up infinite loop problems in calculated styles? I don’t know if this is an actual problem but it’s brain-bending to me.

@mixin --foo(--val) {
  --val: 2;
}

.parent {
  --val: 1;
  .thing {
    @apply --foo(--val);
    --val: if(
        style(--val: 1): 2;
        else: 1;
      );
  }
}

Like, when evaluating a .thing, --val is 1 because of inheritance, but then we apply a mixin which changes it to 2, then we reset it back to 1, but if it’s 1 shouldn’t it reevaluate to 2? I just don’t know.

Unmixing

Miriam asks can you un-mix a mixin? Which is a great question. It’s very worth thinking about, because if there ends up being an elegant way to do it, it makes native mixins even more powerful and a big feather in their cap above what any preprocessor can do. I don’t hate an @unapply at first thought.

Thoughts?

Are you stoked for native mixins? Against it? Worried?

]]>
https://frontendmasters.com/blog/thoughts-on-native-css-mixins/feed/ 0 8020