Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Fri, 19 Dec 2025 16:59:45 +0000 en-US hourly 1 https://wordpress.org/?v=6.9 225069128 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
Classic Mac OS System 1 Patterns https://frontendmasters.com/blog/classic-mac-os-system-1-patterns/ https://frontendmasters.com/blog/classic-mac-os-system-1-patterns/#comments Fri, 21 Nov 2025 20:12:52 +0000 https://frontendmasters.com/blog/?p=7827 Paul Smith made these Classic Mac OS System 1 Patterns, which are super tiny (in size) graphics that work with background-repeat to make old school “textures”. They have an awfully nostalgic look for me, but they are so simple I can see them being useful in modern designs as well.

]]>
https://frontendmasters.com/blog/classic-mac-os-system-1-patterns/feed/ 1 7827
Affinity, Free https://frontendmasters.com/blog/affinity-free/ https://frontendmasters.com/blog/affinity-free/#comments Tue, 04 Nov 2025 22:04:11 +0000 https://frontendmasters.com/blog/?p=7695 When people complain about Photoshop or other various Adobe products and the subscription model they require (The Onion had a good one), people tend to reply with two options:

  1. Use GIMP (free, open-source)
  2. Use Affinity (was ~$50, but a one-time cost)

But now, Affinity is free (and all the varieties combined into one app). You can thank the Canva acquisition for that. You might think that would hurt GIMP usage, but I suspect it won’t as GIMP runs on Linux (which neither Adobe or Affinity do) and it feels… Linuxy. I can’t imagine Adobe is happy about it. Canva says “there is absolutely no catch whatsoever”, and also that their mission is to “build one of the world’s most valuable companies” so, ya know, you make the call.

]]>
https://frontendmasters.com/blog/affinity-free/feed/ 1 7695
The Two Button Problem https://frontendmasters.com/blog/the-two-button-problem/ https://frontendmasters.com/blog/the-two-button-problem/#comments Tue, 21 Oct 2025 23:16:01 +0000 https://frontendmasters.com/blog/?p=7422 I see this UI/UX issue all the time: there are two buttons, and it’s not clear which one of them is in the active state. Here’s an example from my TV:

Which one of those two buttons above is active? Like if I press the enter/select button on my remote, am I selecting “Try it free” or “Sign in”? It’s entirely unclear to me based on the design. Those two design styles are ambiguous. Just two random selections from today’s trends of button design.

If I press the up (or down) arrows on my remote, the styles of the buttons just reverses, so even interacting with the interface doesn’t inform the choice.

This is a problem that can be solved at multiple levels. If the buttons are toggles that affect on-page content, the accessibility angle is partially solved by the aria-selected attribute, for example. It’s also slightly less of an issue on devices with a cursor, as you likely just click on the one that you want. This is mostly a problem with remote control and keyboard usage where the active state is unclear or ambiguous.

I call it the “two button” problem because if there were more than two buttons, the one that is styled differently is probably the one that is active. We could use our grade school brains to figure out which button is active.

(via)

Ideally, though, we don’t have to think very hard. It should be obvious which one is active.

Again, the problem:

The most obvious solution here is to make both button styles the same, but be additive when one of them is the active button.

I feel like it’s very clear now that “Try it free” is the selected button now. Even if it’s not to a user immediately. If they tab/arrow/whatever to the other button, that outline design will move to it and it will become clear then.

You could also, ya know, literally point to it:

Perhaps you could resort to more “extreme” design styles like this when there is prove-ably no mouse/cursor involved, like:

@media (hover: none) and (pointer: coarse) {
  button:active {
    /* big obvious arrow styles */
  }
}

We’ve got a recent post on @media queries that goes into lots of situations like this!

This “two button” problem also can come up in the design pattern of “toggles”. Take something like this:

A “pill” toggle design pattern.

Which one of those buttons is currently active? The up arrow? The down arrow? Neither? It’s impossible to tell by look alone.

Sometimes in this case the “active” button is “grayed out”:

The implication here is that the up arrow is the “active” one, so you don’t need to press it again as it won’t do anything. Only the non-active button is pressable. I feel like this is okay-ish as a pattern, but it’s not my favorite as the active state is less prominent instead of more prominent almost indicating it’s disabled for some other reason or doesn’t matter.

This kind of thing makes me almost miss the days of skeuomorphism where digital interfaces were designed to try to mimic real world surfaces and objects. We don’t have to go full leather-coated buttons though, we can just make the active button appear pressed through shadows and flattening.

This situation differs from the TV interface issue above in that this “active” button is indicating the button has already been pressed, not that it’s the button that will be pressed. So you’d need a style for that state as well.

Maybe these aren’t the most amazing examples in the world, but I hope I’ve got you thinking about the two-button problem. When there are only two buttons, you can’t just pick two arbitrary different styles, as that doesn’t help people understand which of the two are active. You need to think about it a little deeper and get those styles in a place where it’s obvious.

]]>
https://frontendmasters.com/blog/the-two-button-problem/feed/ 7 7422
Very Early Playing with random() in CSS https://frontendmasters.com/blog/very-early-playing-with-random-in-css/ https://frontendmasters.com/blog/very-early-playing-with-random-in-css/#comments Mon, 25 Aug 2025 21:59:02 +0000 https://frontendmasters.com/blog/?p=6931 WebKit/Safari has rolled out a preview version of random() in CSS.

Random functions in programming languages are amazing. You can use them to generate variations, to make things feel spontaneous and fresh. Until now there was no way to create a random number in CSS. Now, the random() function is on its way.

Upon first play, it’s great!

This is only in Safari Technical Preview right now. I’ll post videos below so you can see it, and link to live demos.

CSS processors like Sass have been able to do this for ages, but it’s not nearly as nice in that context.

  1. The random() numbers in Sass are only calculated at compile time. So they are only random at the time. Refreshing the page doesn’t mean newly random values.
  2. Random numbers are usually paired with a loop. So if you want 1,000 randomly placed elements, you need 1,000 :nth-child() selectors with a randomly generated value in each, meaning bulky CSS.

With random() in vanilla CSS, no such loops are needed and making the code quite simple and satisfying.

I found a 12-year-old Sass demo of mine playing with random() that is like this:

This compiled to over 200 lines of CSS.

But now it’s just like this:

Demo

Much of the magic, to me, is how each matching element gets its own random values. So if you had three things like this:

<div class="thing"></div>
<div class="thing"></div>
<div class="thing"></div>

Then this simple CSS could make them all quite different:

.thing {
  position: absolute;
  background: red;
  width: 100px;
  height: 100px;
  
  top: random(10px, 200px);
  left: random(100px, 400px);
  
  background: rgb(
    random(0, 255),
    random(0, 255),
    random(0, 255)
  )
}
Demo

The blog post doesn’t mention “unitless” numbers like I’ve used above for the color, but they work fine. If you’re using units, they need to be the same across all parameters.

The “starfield” demo in the blog post is pretty darn compelling!

Demo

I found another old demo where I used a bit of randomized animation-delay where in the SCSS syntax I did it like this:

animation-delay: (random(10) / 40) + s;

Notice I had to append the “s” character at the end to get units there. Now in vanilla CSS you just declare the range with the units on it, like:

animation-delay: calc(random(0s, 10s) / 40);

And it works great!


The feature does have a spec, and I’m pleased that it has covered many things that I hadn’t considered before but are clearly good ideas. The blog post covers this nicely, but allow me to re-iterate:

.random-rect {
  width: random(100px, 200px);
  height: random(100px, 200px);
}

Both the width and height will be different random values. But if you want them to be the same random value, you can set a custom ident value that will “cache” that value for one element:

.random-square {
  width: random(--foo, 100px, 200px);
  height: random(--foo, 100px, 200px);
}

Nice!

But if you had 20 of these elements, how could you make sure all had the same random values? Well there is a special keyword for that, ensuring all matched elements share the same random values:

.shared-random-rect {
  width: random(element-shared, 100px, 200px);
  height: random(element-shared, 100px, 200px);
}

But in that case, all matched elements would share the same random values, but the width and height wouldn’t be equal. So you’d do both to make sure it’s all equal:

.shared-random-squares {
  width: random(--foo element-shared, 100px, 200px);
  height: random(--foo element-shared, 100px, 200px);
}

That’s all very nicely considered, I think.


Ranges are also handled with a final parameter:

top: random(10px, 100px, 20px);
transition-delay: random(0s, 5s, 1s);

The top value above can only be: 10px, 30px, 50px, 70px, or 90px.

The transition-delay value above can only be: 0s, 1s, 2s, 3s, 4s, or 5s.

Otherwise you can get decimal values of either which might be more random than you want. Even 1px for random pixel values as an increment seems to be suggested.

(Note the WebKit blog has a code sample with by 20deg in it, which I think is a typo as that doesn’t work for me.)


I didn’t have a chance to try it yet — but doesn’t it make you wanna force a re-render and see if it will work with document.startViewTransition??

]]>
https://frontendmasters.com/blog/very-early-playing-with-random-in-css/feed/ 4 6931
Web Design: What is the web capable of that is hard to express in design software? https://frontendmasters.com/blog/web-design-what-is-the-web-capable-of-that-is-hard-to-express-in-design-software/ https://frontendmasters.com/blog/web-design-what-is-the-web-capable-of-that-is-hard-to-express-in-design-software/#comments Mon, 18 Aug 2025 18:15:30 +0000 https://frontendmasters.com/blog/?p=6030 Designs aren’t websites, they are pictures of websites.

I heard someone say that once while lamenting the state of design software for web design.

At some point, I think all web designers circle around to the thought that if design software was only more like the web itself that it would be better for it. We would gain efficiency in that there may not need be much translation at all between design and the finished product. Time and quality suffer during the translation required now.

We could look at all the different things tried to bridge this gap. There are many — all of varying interest. I believe it is safe to say that none of them have “won”. Tools that replicate the features of the web faithfully have trouble with complexity and finding an audience that wants that. Even the prevailing web design tool today, Figma, runs on the web but the designs you create there are not really of the web. You’re drawing rectangles on a <canvas> there, not crafting <div>s. And while some of those abstractions in Figma are designed to replicate web features, they are that: a replication, not reality.

But instead of looking at takes on design tool abstraction, I’d rather enumerate many of the things that make design tools that replicate the web difficult.

Staring, naturally, with this most basic of web design concepts.

Link & Button States

Most sites have a style for links and buttons, and design software has no problem accommodating that. But those styles change in different states. What happens when you hover them? Click them? Tab to focus them? Those can (and should) have different designs, but static design software doesn’t always accommodate that very well, leaving you to invent your own system for how to represent it.

I don’t find :active states particularity common, likely because design software doesn’t care to prompt you into state-based design.

Color Modes

Speaking of state, you could think of light/dark mode as a state for the entire design of the website.

Looking at the button designs above, if you put a very dark background instead of the light tan, those dark brown button borders will not be very visible if at all. It’s a designer’s job to consider that, plan for that, and design for that. Can/should/does your design software help with that?

Color modes are complicated anyway, as the choice of a color mode can be set as low-level as the operating system the browser is running on, the browser may change the mode, the browser may offer site-specific color mode settings, and the site itself may offer code mode settings of it’s own. And perhaps more than just light and dark!

Not to mention accessibility considerations. There has been experimental features like browsers than can force sites into certain color modes, and long-standing accessibility features like High Contrast mode on Windows. Does your design software help you with designing for those states?

From “Windows High Contrast Mode, Forced Colors Mode And CSS Custom Properties” by Eric Bailey

Variables

Speaking of code modes, maybe the way you’re handling that is through variables, or custom properties in CSS. Does your design software have a way of specifying variables? Then setting state in various places that change the values of those variables? I can answer that for you: it probably doesn’t. Even if it has a concept of variables, it’s likely abstracted in a way that doesn’t work quite like the web does. The key difficulty being that CSS custom properties cascade through the DOM and can be set/adjusted at any level. This isn’t particularly feasible in a static design, as the DOM is not a concept they are concerned with.

Any Other Kind of State

What about the loading state of dynamically loaded data in your app? Do you have a plan for that? Spinners? Skeletons? Nothing? Do they take up the appropriate space to avoid layout shifting? Are you using some kind of placeholder technique for images? Did you design this stuff in your design tool? Or do you just skip over it and deal with it as a forlorn implementation detail?

What about error states?

What about the state where a paying user’s credit card has lapsed and they no longer have access to certain features?

I find that “different states” is a tricky concept for design tools. They tend to just ignore it and let you invent how you want to handle it yourself, which usually amounts to: draw another rectangle nearby and design the alternate state in there — if you remember to.

Good designers will work from design documents that spell out the differnet necessary states to deal with and organize the documents to showcase those important states. But it can also be true that there are so many permutations of bits of state that hand-designing each one is too much to be practical for most design tools, so they end up more like vague sketches of the somewhat random combinations of state.

Viewport Sizes

Responsive design is so locked in that design tools largely can’t ignore it. But they also tend not to nail it either. I find the most common approach is the: Uhhh I guess just design a largish-one, a pretty small one, and something in between. Then just made educated guesses for stuff in between.

This can and does work fine sometimes.

But when I say it doesn’t nail it, it’s because it doesn’t communicate things in a way that is perfectly relevant to what the web can do. For instance, those two columns of type above, is that saying… always do two columns until the 799px breakpoint then do one column? That seems like a reasonable read, but… maybe the breakpoint should be driven by the direct parent not the whole viewport? Maybe at really wide widths it could go up to more columns? How flexible are those column widths? Do they have a min and max? Should we be thinking about how paragraphs break across them? What if an image or other media goes into the columns, should it behave in any special way?

What about the font sizing in the example above? The biggest bit of text definitely changes size, but how? Fluidly? Set sizes at set breakpoints? Does all type do that or just some? How can we design that?

There is so much to think about with fluid, responsive design. Design tools I feel barely scratch the surface, and it’s been a good decade-and-a-half here. What would it be like to stop thinking about fixed breakpoints at all?

Anything Scrolling or Mouse Movement Related

At the most basic level, we already rarely deal with styling scrollbars at all. Static designs are designed such that you see the entire page from header to footer. Does this affect how we prioritize content? Do we think about how much scrolling is too much?

But more to the point, there are lots of web design possibilities specifically related to scrolling. Think about position: fixed; — how do you represent that on a picture of a website? Consider the different possibilities of background-attachment, can a static design explain that? Smooth scrolling? Scroll snapping? What about all the CSS carousel stuff?

Variable Content

Another classic! Static designs tend to have “happy path” content where the images are beautiful and the content fits perfectly into the layout.

Some classic lorem ipsum text in here. It gets ridiculed sometimes because it means you aren’t designing around even a single real world design situation let alone the many that are likely needed.

What if that card design above had 12 tags? It would probably look silly if they wrapped, pushing the more-important header too far down. What if one of the tags was “Hamlet in Northumberland, England“. Will it break the container?

It looks like the header above is already truncated to two lines. It’s nice we can do that on the web, but as Karen always reminds us, truncation is not a content strategy. Does the CMS get involved to limit things? Are design people at the CMS meetings so that good design ideas can be a part of the decision making process?

What if there isn’t enough content? Will this card work without the description text? Without a photo or author?

What about translation? That’s a big one as sites are translated whether or not you deal with it. Can languages that translate with generally more characters than the original language find enough room?

Units & Math

CSS has interesting, real, meaningful units. Sure there are “pixels” and that’s all well and good, but we’ve got relative units that relate to each other and are flexible with user settings. We’ve got percentages. We’ve got typography-relational units. We’ve got units relative to the viewport or the container. And quite a bit more!

Design tools don’t even go there with units. They tend to be unitless.

Something is “244” wide, which is probably best mappable to px, but doesn’t actually say px because the exported files may or may not actually map to that number. Units alone represent a significant devide between static designs and real website implementations.

And then there is the slew of functional calculations possible on the web available in JavaScript and CSS. Your calc() and tan() and min() and clamp() and the slew of other functions that help express interesting design intent are largely absent in design tools.

Even a real practical grid layout like:

.grid {
  display: grid;
  grid-template-columns: minmax(100px, auto) 4fr minmax(100px, 1fr);
}

Above, I’m trying to say: the 1st column is a navigation, so have it be as wide as the widest bit of content, but it’s allowed to be squished smaller, to a point, then the the widest, generally about 2/3rds of layout, and the 3rd column takes the leftover after that, and is allowed to squish smaller than auto. That’s a mouthful, but it’s pretty practical/common and all but unexpressable in design tools.

Animations & Transitions

Transitions can be so simple. Here’s just a smidge of scaling on some element on hover:

https://sarthakmishra.com/

Even that, does your design tool make that easy to mock up? And thus show to your team and talk about and get signoff or whatever is needed? It seems to me these types of things tend to come later in the process. Like they get slapped on during implementation because code is such a better place to explore and play with these ideas. Maybe that’s OK. Maybe that’s a shame that design tools to make it easier to think through these things during initial design.

From the same nice website as above, look at these slightly more complex transitions:

One look at a GSAP showreel and you gotta wonder: how much of this is done in a design tool vs how much of this is just people who know how code works just getting into the browser and just working there?

Animation isn’t an isolated concept here either, it can be linked to scrolling and state, for instance.

Device-Specific Details

How do you mockup handling the touch target requirements for devices with touch screens? Are you thinking about where people normally have their thumbs? Have you done everything you can to make form input good, particularly for on-screen keyboards? Do you think you might have to make some design choices for low-bandwidth situations? Did you make a mockup for landscape mode? How’s that information hierarchy on a tiny screen?

What about big ol’ 1980px wide screens with browsers open on the entire thing, which isn’t even particularly rare! Or even bigger — does the design hold up? Are you implementing appropriate cursors in appropriate situations for systems with a mouse? For eCommerce sites, have you thought about designing the checkout experience differences between browsers that support Google Pay vs Apple Pay and those types of differences?

Some of this stuff design software is fine at. They can’t anticipate every design need for every app, that’s on you, so you do the work. But some stuff is just hard in design software. It certainly isn’t going to help you design appropriate cursors, for instance.

Media Handling

Did you mock up how videos are integrated into the site? Are you doing anything with audio? More exotic stuff like a 3D viewing experience? You’re likely to be faking parts of it at best in static designs.

So Many More Things

I’m not trying to dog on design software. I actually quite like a lot of design software, and think it’s often an ideal place for ideas to start and early marination of design choices. Sometimes design software frees up the mind to think about design choices you may not make once you’re in code. An example of that might be something like strangely-overlapping elements which is easy to do while dragging rectangles around in design software, but harder think of once they are <div>s.

But then it ends up as like… 5%? Five percent of the time spent in the design software getting ideas going, and the rest of the time in the browser. And the rest of that time is also design work. Not just implementation but design itself.

Stay tuned for the Frontend Masters course Award-Winning Marketing Websites with Matias Gonzalez, who will surely be blowing minds while teaching you the power of design on the web platform.


I first had an inkling to write this blog post when listening to the Complementary podcast episode The Gap Between Design and Execution with Alex Krasikov.

]]>
https://frontendmasters.com/blog/web-design-what-is-the-web-capable-of-that-is-hard-to-express-in-design-software/feed/ 1 6030
Mingcute https://frontendmasters.com/blog/mingcute/ https://frontendmasters.com/blog/mingcute/#respond Sat, 16 Aug 2025 20:12:18 +0000 https://frontendmasters.com/blog/?p=6782

🔥 Mingcute has been my go-to icon library for a while.- Open source and open license- "Cute" and bubbly icon style with more options than most- Really nice Figma plugin- Iconify support to use in any web project

Ben Holmes (@bholmes.dev) 2025-08-04T12:39:30.195Z
]]>
https://frontendmasters.com/blog/mingcute/feed/ 0 6782
Get the number of auto-fit/auto-fill columns in CSS https://frontendmasters.com/blog/count-auto-fill-columns/ https://frontendmasters.com/blog/count-auto-fill-columns/#comments Wed, 06 Aug 2025 15:08:24 +0000 https://frontendmasters.com/blog/?p=6567 Ever wanted to get the number of auto-fit/auto-fill columns in a grid? For example, because you want to highlight just the items in the first or last row or column? Do something special just for even or for odd rows or columns (e.g. zebra striping)? Or for any one specific row or column? Create responsive non-rectangular grids? And all of this with zero breakpoints?

This is all doable with pure CSS by using container query units, CSS variables, and CSS mathematical functions! Of course, it also involves navigating browser bugs and support gaps. But at the end of the day, it is possible to do it cross-browser!

Let’s see how!

The Basic Idea

Setup

We start with a .grid with a lot of items, let’s say 100. I normally prefer to generate them in a loop using a preprocessor to avoid clutter in the HTML and to make it easy to change their number, but it’s also possible to do so using Emmet. For the demos illustrating the concept here, we’re using Pug, and also numbering our items via their text content:

.grid
- for(let i = 0; i < 100; i++)
.item #{i + 1}

Our .grid has auto-fit columns:

.grid {
  --u: 7em;

  display: grid;
  grid-template-columns: repeat(auto-fit, var(--u));
  container-type: inline-size
}

This means our .grid has as many columns of unit width u as can fit within its own content-box width. This width is flexible and is given by the page layout, we don’t know it. However, its children (the .item elements) can know it as 100cqw in container query units. To have these container units available for the .grid element’s children (and pseudos), we’ve made the .grid an inline container.

This should work just fine. And it does, in both Chrome and Firefox. However, if we try it out in Safari, we see our .grid is collapsed into a point. Unfortunately, in Safari, auto-fit grids break if they are also containers. (Note: this Safari bug is actually fixed, it’s just waiting to make its way to a stable release.)

We have two options in this case.

The first would be to replace auto-fit with auto-fill. When we have as many items as we do in this case, we can use either of them, the difference between them is only noticeable when we don’t even have enough items to fill one row.

.grid {
  --u: 7em;

  display: grid;
  grid-template-columns: repeat(auto-fill, var(--u));
  container-type: inline-size
}

The second would be to put the .grid inside a wrapper .wrap element and move the container property on the wrapper.

.wrap { container-type: inline-size }

.grid {
  --u: 7em;

  display: grid;
  grid-template-columns: repeat(auto-fit, var(--u))
}

We’re going for the first option here.

Now we’re getting to the interesting part!

Getting the number of columns

In theory, we could get the number n of columns on the .item children of the .grid via division, whose result we round down (if the container width of 100cqw is 2.23 times the unit width u of a column, then we round down this ratio to get the number of columns we can fit, which is 2 in this case):

--n: round(down, 100cqw/var(--u))

In practice, while this should work, it only works in Safari (since Sept 2024) and in Chrome (since June 2025), where we can test it out by displaying it using the counter hack:

.grid::before {
  --n: round(down, 100cqw/var(--u));

  counter-reset: n var(--n);
  content: counter(n)
}

We’ve wrapped this inside a @supports block so we have a message that lets us know about this failing in non-supporting browsers (basically Firefox), where we see the following:

base_idea_fallback
the result in non-supporting browsers: no number of columns can be computed

In Safari and Chrome, things look like in the recording below:

We can see we have a problem when we have one column and it overflows the parent: the ratio between the parent .grid width of 100cqw and the column unit width u drops below 1, so we can fit one item 0 times inside the content-box width of the .grid. And this is reflected in the n value, even though, in practice, we cannot have a grid with less than one column. However, the fix for this is simple: use a max() function to make sure n is always at least 1.

--n: max(1, round(down, 100cqw/var(--u)))

Whenever the division result drops below 1, the result of the max() function isn’t the round() value anymore, but 1 instead.

You can see it in action in demo below, but keep in mind it can only compute the number of columns in supporting browsers (Safari/Chrome):

Great, but what Firefox? The Firefox bug looks like it’s dormant, so we cannot get the ratio between two length values there.

Extending support

However, we have a clever hack to solve the problem!

The idea behind is the following: the tangent of an acute angle in a right triangle is the ratio between the length of the cathetus opposing the angle and the length of the cathetus adjacent to it. So basically, the tangent is a ratio between two length values and such a ratio is precisely what we need.

A diagram illustrating basic trigonometry, featuring the tangent function, labeled 'tan(a) = opposing / adjacent', with definitions for 'opposing cathetus' and 'adjacent cathetus', and an angle 'a' represented in a right triangle.
basic trigonometry recap

Now you may be wondering what right triangle and what angle do we even have here. Well, we can imagine building a triangle where a cathetus has the same length as the .grid parent’s content-box width (100cqw on the .item elements, which we’ll call w) and the other has the same length as the column unit width (u).

The tangent of the angle opposing the cathetus of length w is the ratio between w and u. Okay, but what is this angle?

A mathematical diagram illustrating the relationship between the tangent function, the angle 'a', and the lengths 'w' and 'u' in a right triangle.
using trigonometric functions to get around browser support gaps

We can get this angle using the atan2() function, which takes two arguments, the length of the opposing cathetus w and the length of the adjacent cathetus u:

--a: atan2(var(--w), var(--u))

Having the angle a and knowing that the ratio f between w and u is the tangent of this angle, we can write:

--f: tan(var(--a))

Or, replacing the angle in the formula:

--f: tan(atan2(var(--w), var(--u)))

In general, know that a length ratio like w/u can always be computed as tan(atan2(w, u)).

Rounding down this ratio f gives us the number of columns of unit width u that fit within the .grid parent’s content-box width w.

--n: round(down, var(--f))

So we can write it all as follows, introducing also the correction that the number of columns needs to be at least 1:

--f: tan(atan2(var(--w), var(--u)));
--n: max(1, round(down, var(--f)))

That’s it, that’s the formula for --n in the case when we don’t have support for getting the ratio of two length values! There is one catch, though: both --w and --u have to be registered as lengths in order for atan2() to work properly!

Putting it all together, the relevant code for our demo looks as follows:

.grid {
  --u: 7em;

  display: grid;
  grid-template-columns: repeat(auto-fill, var(--u));
  container-type: inline-size
}

.grid::before, .item {
  --w: 100cqw;
  --f: var(--w)/var(--u);
  --n: max(1, round(down, var(--f)));
}

@supports not (scale: calc(100cqh/3lh)) {
  @property --w {
    syntax: '<length-percentage>';
    initial-value: 0px;
    inherits: true
  }

  @property --u {
    syntax: '<length-percentage>';
    initial-value: 0px;
    inherits: true
  }
	
  .grid::before, .item { --f: tan(atan2(var(--w), var(--u))) }
}

Note that the .grid pseudo is only needed to display the --n value (using the counter hack) for us to see in the demo without having to register it and then look for it in DevTools (which is the tactic I most commonly use to check the computed value of a CSS variable).

Almost there, but not exactly.

Fixing tiny issues

If you’ve played with resizing the demo above, you may have noticed something is off in Firefox at times. At certain points when the .grid element’s content-box width w is a multiple of the unit column width u, for example, when w computes to 1008px and the unit column with u of 112px fits inside it exactly 9 times, Firefox somehow computes the number of columns as being smaller (8 instead of 9, in this example).

My first guess was this is probably due to some rounding errors in getting the angle via atan2() and then going back from an angle to a ratio using tan(). Indeed, if we register --f so we can see its value in DevTools, it’s displayed as 8.99999 in this case, even though 1008px/112px is exactly 9.

Screenshot of browser developer tools showing a grid layout with items numbered from 1 to 18. The grid has a header indicating the number of auto-fill columns, which is 8. The inspector highlights CSS variables related to grid sizing.
rounding error caught by Firefox DevTools

So this means rounding down f results in the number of columns n being computed as 8, even though it’s actually 9. Hmm, in this case, it might be better to round f to a tiny precision of .00001 before rounding it down to get the number of columns n:

--f: round(tan(atan2(var(--w), var(--u))), .00001)

This seems to get the job done.

Still, I’m a bit worried this still might fail in certain scenarios, even though I’ve kept resizing obsessively in Firefox and haven’t encountered any problems after rounding f.

So let’s make sure we’re on the safe side and place the .grid in a wrapper .wrap, make this wrapper the container, compute the number of columns n on the .grid and use it to set the grid-template-columns. This way, the essential CSS becomes:

.wrap {
  container-size: inline-type;
}

.grid {
  --w: 100cqw;
  --u: 7em;
  --f: var(--w) / var(--u);
  --n: max(1, round(down, var(--f)));

  display: grid;
  grid-template-columns: repeat(var(--n), var(--u));
  justify-content: center;
}

@supports not (scale: calc(100cqh / 3lh)) {
  @property --w {
    syntax: "<length-percentage>";
    initial-value: 0px;
    inherits: true;
  }

  @property --u {
    syntax: "<length-percentage>";
    initial-value: 0px;
    inherits: true;
  }

  .grid {
    --f: round(tan(atan2(var(--w), var(--u))), 0.00001);
  }
}

Note that we may also use 1fr instead of var(--u) for the grid-template-columns property if we want the .item elements to stretch.

Mind the gap

Nice, but oftentimes we also want to have a gap in between our rows and columns, so let’s see how the number of columns can be computed in that case.

Whenever we have n columns, we have n - 1 gaps in between them.

This means that n times the unit column width plus (n - 1) times the gap space adds up to the container’s content-box width:

n·u + (n - 1)·s = w

If we add s on both sides in the equation above, we get:

n·u + (n - 1)·s + s = w + s ⇒ 
n·u + n·s - s + s = w + s ⇒
n·u + n·s = w + s ⇒
n·(u + s) = w + s ⇒
n = (w + s)/(u + s)

Putting this into CSS, our ratio looks as follows:

(var(--w) + var(--s))/(var(--u) + var(--s))

Note that in our case, it’s the fraction f that we compute this way before we round it to get the number of items n and ensure n is always at least 1.

Also note that the CSS variables we need to register for the no calc() length division fallback are the numerator and denominator of this fraction. So our essential CSS becomes:

.wrap {
  container-size: inline-type;
}

.grid {
  --w: 100cqw;
  --u: 7em;
  --s: 3vmin;
  --p: calc(var(--w) + var(--s)); /* numerator */
  --q: calc(var(--u) + var(--s)); /* denominator */
  --f: var(--p) / var(--q);
  --n: max(1, round(down, var(--f)));

  display: grid;
  grid-gap: var(--s);
  grid-template-columns: repeat(var(--n), 1fr);
}

@supports not (scale: calc(100cqh / 3lh)) {
  @property --p {
    /* numerator */
    syntax: "<length-percentage>";
    initial-value: 0px;
    inherits: true;
  }

  @property --q {
    /* denominator */
    syntax: "<length-percentage>";
    initial-value: 0px;
    inherits: true;
  }

  .grid {
    --f: round(tan(atan2(var(--p), var(--q))), 0.00001);
  }
}

Let’s Go Wild!

And let’s see where we can use this!

Highlighting items on a certain column

In order to do something like this, we use the item indices. Once sibling-index() is supported cross-browser, we’ll be able to do this:

.item { --i: calc(sibling-index() - 1) }

Note that we need to subtract 1 because sibling-index() is 1-based and we need our index i to be 0-based for modulo and division purposes.

Until then, we add these indices in style attributes when generating the HTML:

.grid
- for(let i = 0; i < 100; i++)
.item(style=`--i: ${i}`) #{i + 1}

Let’s say we want to highlight the items on the first column. We get the number of columns n just like before. An item is on the first column if i%n (which gives us the 0-based index of the column an item of index i is on) is 0. Now given I used the word if there, you might be thinking about the new CSS if() function. However, we have a way better supported method here.

If the column index i%n is 0, then min(1, i%n) is 0. If the column index i%n isn’t 0, then min(1, i%n) is 1. So we can do the following:

.item {
  --nay: min(1, mod(var(--i), var(--n))); /* 1 if NOT on the first column */
  --yay: calc(1 - var(--nay)); /* 1 if on the first column! */
}

So then we can use --yay to highlight the items on the first column by styling them differently, for example by giving them a different background:

.item {
  --nay: min(1, mod(var(--i), var(--n))); /* 1 if NOT on the first column */
  --yay: calc(1 - var(--nay)); /* 1 if on the first column! */

  background: color-mix(in srgb, #fcbf49 calc(var(--yay)*100%), #dedede)
}

You can see it in action in the live demo below:

Now let’s say we want to highlight the items on the last column. In this case, the column index i%n is n - 1, which means that their difference is 0:

(n - 1) - (i%n) = 0

Using this, we can do something very similar to what we did before, as the minimum between 1 and this difference is 0 for items on the last column and 1 for those that aren’t on the last column:

.item {
  /* 1 if NOT on the last column */
  --nay: min(1, (var(--n) - 1) - mod(var(--i), var(--n))));
  /* 1 if on the last column! */
  --yay: calc(1 - var(--nay));
}

For example, if n is 7, then the column index i%n can be 0, 1, … 6 and n - 1 is 6. If our item of index i is on the last column, then its column index i%n = i%7 = 6, so the difference between n - 1 = 7 - 1 = 6 and i%n = i%7 = 6 is 0. If our item of index i isn’t on the last column, then its column index i%n = i%7 < 6, so the difference between n - 1 = 6 and i%n < 6 is 1 or bigger. Taking the minimum between 1 and this difference ensures we always get either 0 or 1.

In general, if we want to highlight a column of index k (0-based, but we can just subtract 1 in the formula below if it’s given 1-based), we need to compute the difference between it and i%n (the column index of an item of index i), then use the absolute value of this difference inside the min():

.item {
  --dif: var(--k) - mod(var(--i), var(--n));
  --abs: abs(var(--dif));
  --nay: min(1, var(--abs)); /* 1 if NOT on column k */
  --yay: calc(1 - var(--nay)); /* 1 if on column k! */
}

The difference and its absolute value are 0 when the item of index i is on column k and different (bigger in the case of absolute value) when it isn’t.

We need the absolute value here because, while the difference between n - 1 and i%7 is always 0 or bigger, that is not the case for the difference between any random k and i%n. For example, if n is 7 and k is 2, the k - i%n difference is negative when k is smaller than i%n, for example when i%n is 5. And we need the difference that goes into the min() to be 0 or bigger in order for the min() to always give us either 0 or 1.

All modern stable browsers support abs(), but for the best possible browser support, we can still test for support and use the fallback:

@supports not (scale: abs(-2)) {
  .item { --abs: max(var(--dif), -1*(var(--dif))) }
}

Also, note that if the selected column index k is equal to n or bigger, no items get selected.

In the interactive demo below, clicking an item selects all items on the same column:

It does this by setting --k (in the style attribute of the .grid) to the index of that column.

A code snippet from a web developer's browser console showcasing CSS rules for items in a grid layout, including custom properties for styling.
Chrome DevTools screenshot showing --k being set on the .grid parent and used in computations on .item children

We can also highlight items on either odd or even columns:

.item {
  /* 1 if on an even column, 0 otherwise */
  --even: min(1, mod(mod(var(--i), var(--n)), 2));
  /* 1 if on an odd colum, 0 otherwise */
  --odd: calc(1 - var(--even));
}

This is a particular case of highlighting every k-th column starting from column j (again, j is a 0-based index and smaller than k):

.item {
  --dif: var(--j) - mod(mod(var(--i), var(--n)), var(--k));
  --abs: abs(var(--dif));
  --nay: min(1, var(--abs));
  /* 1 if on one of every kth col starting from col of index j */
  --yay: calc(1 - var(--nay));
}

Highlighting items on a certain row

If we want to highlight the items on the first row, this means their index i must be smaller than the number of columns n. This means the difference n - i must be bigger than 0 for items on the first row. If we clamp it to the [0, 1] interval, we get a value that’s 0 on every row but the first and 1 on the first row.

.item {
  --yay: clamp(0, var(--n) - var(--i), 1)  /* 1 if on the first row! */
}

There is more than one way to skin a cat however, so another approach would be to get the row index, which is the result of i/n rounded down. If this is 0, the item of index i is on the first row. If it’s bigger than 0, it isn’t. This makes the minimum between 1 and i/n rounded down be 0 when the item of index i is on the first row and 1 when it isn’t.

.item {
  --nay: min(1, round(down, var(--i)/var(--n))); /* 1 if NOT on the first row */
  --yay: calc(1 - var(--nay)); /* 1 if on the first row! */
}

This second approach can be modified to allow for highlighting the items on any row of index k as the difference between k and i/n rounded down (the row index) is 0 if the item of index i is on the row of index k and non-zero otherwise:

.item {
  --dif: var(--k) - round(down, var(--i)/var(--n));
  --abs: abs(var(--dif));
  --nay: min(1, var(--abs)); /* 1 if NOT on row of index k */
  --yay: calc(1 - var(--nay)); /* 1 if on row of index k! */
}

Highlighting the items on any row includes the last one. For this, we need to know the total number t of items on our grid. This means (t - 1) is the index of the last grid item, and we can get the index of the row it’s on (that is, the index of the final row) by rounding down (t - 1)/n. Then we substitute k in the previous formula with the index of the final row we’ve just obtained this way.

.item {
  /* 1 if NOT on last row */
  --nay: min(1, round(down, (var(--t) - 1)/var(--n)) - round(down, var(--i)/var(--n)));
  /* 1 if on last row! */
  --yay: calc(1 - var(--nay));
}

There are two things to note here.

One, we don’t need the absolute value here anymore, as the last row index is always going to be bigger or equal to any other row index.

Two, we’re currently passing the total number of items t to the CSS as a custom property when generating the HTML:

- let t = 24; //- total number of items on the grid

.wrap
.grid(style=`--t: ${t}`)
- for(let i = 0; i < t; i++)
.item(style=`--i: ${i}`) #{i + 1}

But once sibling-count() is supported cross-browser, we won’t need to do this anymore and we’ll be able to write:

.item { --t: sibling-count() }

Just like before, we can highlight items on odd or even rows.

.item {
  /* 1 if on an even row */
  --even: min(1, mod(round(down, var(--i)/var(--n)), 2));
  /* 1 if on an odd row */
  --odd: calc(1 - var(--even));
}

And the odd/ even scenario is a particular case of highlighting items on every k-th row, starting from row of index j.

.item {
  --dif: var(--j) - mod(round(down, var(--i)/var(--n)), var(--k));
  --abs: abs(var(--dif));
  --nay: min(1, var(--abs));
  /* 1 if on one of every kth row starting from row of index j */
  --yay: calc(1 - var(--nay));
}

Taking it Further

Another thing this technique can be used for is creating responsive grids of non-rectangular shapes with no breakpoints. An example would be the hexagon grid below. We aren’t going into the details of it here, but know it was done using this technique plus a few more computations to get the right hexagon alignment.

]]>
https://frontendmasters.com/blog/count-auto-fill-columns/feed/ 2 6567
Out-of-your-face AI https://frontendmasters.com/blog/out-of-your-face-ai/ https://frontendmasters.com/blog/out-of-your-face-ai/#respond Sun, 01 Jun 2025 15:29:05 +0000 https://frontendmasters.com/blog/?p=6007 A very interesting aspect of the AI smashing its way into every software product known to man, is how it’s integrated. What does it look like? What does it do? Are we allowed to control it? UX patterns are evolving around this. In coding tools, I’ve felt the bar being turned up on “anticipate what I’m doing and offer help”. Personally I’ve gone from, hey that’s nice thanks to woah woah woah, chill out, you’re getting in my way.

I’m sure this will be fascinating to watch for a long time to come. For example, “Subtle Mode” in Zed gets AI more “out of your face” and if you want to see a suggestion, you press a button. I love that idea. But I also understand Kojo Osei’s point here: there should be no AI button.

]]>
https://frontendmasters.com/blog/out-of-your-face-ai/feed/ 0 6007
The HTML Review https://frontendmasters.com/blog/the-html-review/ https://frontendmasters.com/blog/the-html-review/#respond Tue, 08 Apr 2025 00:42:30 +0000 https://frontendmasters.com/blog/?p=5538 The fourth issue of The HTML Review is out. Wonderful writing framed by entirely different and unusual interactive interfaces, brought to you by the power of web technology. A zine come to life.

Just try to pick a favorite.

]]>
https://frontendmasters.com/blog/the-html-review/feed/ 0 5538