Creating a Cohesive User Experience Using HSL Colors in CSS

Category Image 052

We all know what importance colors hold in anything, whether it’s a website layout, image, video, or any other graphical element. In essence, color is a subjective experience that results from the interaction between light, the eye, and the brain. Adding colors to the website gives a new life to the whole layout and graphical elements. Nobody likes to visit web pages with white, black, and gray colors on them. Colors make the elements look more realistic and catchy to the human eye.

Not just theoretically, psychology also comes into play when we use colors on websites. It has been scientifically proven that a specific set of colors triggers particular emotions in the human brain, such as autumn colors like orange and yellow representing joy or happiness, red color to festive seasons, and blue viewed as calm and trustworthy. Besides, you must have noticed that many food companies often use red and yellow on their websites, pharmaceutical companies tend to use green on their sites, fitness companies sometimes use orange, and so on.

What Are CSS Container Style Queries Good For?

Category Image 052

We’ve relied on media queries for a long time in the responsive world of CSS but they have their share of limitations and have shifted focus more towards accessibility than responsiveness alone. This is where CSS Container Queries come in. They completely change how we approach responsiveness, shifting the paradigm away from a viewport-based mentality to one that is more considerate of a component’s context, such as its size or inline-size.

Querying elements by their dimensions is one of the two things that CSS Container Queries can do, and, in fact, we call these container size queries to help distinguish them from their ability to query against a component’s current styles. We call these container style queries.

Existing container query coverage has been largely focused on container size queries, which enjoy 90% global browser support at the time of this writing. Style queries, on the other hand, are only available behind a feature flag in Chrome 111+ and Safari Technology Preview.

The first question that comes to mind is What are these style query things? followed immediately by How do they work?. There are some nice primers on them that others have written, and they are worth checking out.

But the more interesting question about CSS Container Style Queries might actually be Why we should use them? The answer, as always, is nuanced and could simply be it depends. But I want to poke at style queries a little more deeply, not at the syntax level, but what exactly they are solving and what sort of use cases we would find ourselves reaching for them in our work if and when they gain browser support.

Why Container Queries

Talking purely about responsive design, media queries have simply fallen short in some Aspects, but I think the main one is that they are context-agnostic in the sense that they only consider the viewport size when applying styles without involving the size or dimensions of an element’s parent or the content it contains.

This usually isn’t a problem since we only have a main element that doesn’t share space with others along the x-axis, so we can style our content depending on the viewport’s dimensions. However, if we stuff an element into a smaller parent and maintain the same viewport, the media query doesn’t kick in when the content becomes cramped. This forces us to write and manage an entire set of media queries that target super-specific content breakpoints.

Container queries break this limitation and allow us to query much more than the viewport’s dimensions.

How Container Queries Generally Work

Container size queries work similarly to media queries but allow us to apply styles depending on the container’s properties and computed values. In short, they allow us to make style changes based on an element’s computed width or height regardless of the viewport. This sort of thing was once only possible with JavaScript or the ol’ jQuery, as this example shows.

As noted earlier, though, container queries can query an element’s styles in addition to its dimensions. In other words, container style queries can look at and track an element’s properties and apply styles to other elements when those properties meet certain conditions, such as when the element’s background-color is set to hsl(0 50% 50%).

That’s what we mean when talking about CSS Container Style Queries. It’s a proposed feature defined in the same CSS Containment Module Level 3 specification as CSS Container Size Queries — and one that’s currently unsupported by any major browser — so the difference between style and size queries can get a bit confusing as we’re technically talking about two related features under the same umbrella.

We’d do ourselves a favor to backtrack and first understand what a “container” is in the first place.

Containers

An element’s container is any ancestor with a containment context; it could be the element’s direct parent or perhaps a grandparent or great-grandparent.

A containment context means that a certain element can be used as a container for querying. Unofficially, you can say there are two types of containment context: size containment and style containment.

Size containment means we can query and track an element’s dimensions (i.e., aspect-ratio, block-size, height, inline-size, orientation, and width) with container size queries as long as it’s registered as a container. Tracking an element’s dimensions requires a little processing in the client. One or two elements are a breeze, but if we had to constantly track the dimensions of all elements — including resizing, scrolling, animations, and so on — it would be a huge performance hit. That’s why no element has size containment by default, and we have to manually register a size query with the CSS container-type property when we need it.

On the other hand, style containment lets us query and track the computed values of a container’s specific properties through container style queries. As it currently stands, we can only check for custom properties, e.g. --theme: dark, but soon we could check for an element’s computed background-color and display property values. Unlike size containment, we are checking for raw style properties before they are processed by the browser, alleviating performance and allowing all elements to have style containment by default.

Did you catch that? While size containment is something we manually register on an element, style containment is the default behavior of all elements. There’s no need to register a style container because all elements are style containers by default.

And how do we register a containment context? The easiest way is to use the container-type property. The container-type property will give an element a containment context and its three accepted values — normal, size, and inline-size — define which properties we can query from the container.

/* Size containment in the inline direction */
.parent {
  container-type: inline-size;
}

This example formally establishes a size containment. If we had done nothing at all, the .parent element is already a container with a style containment.

Size Containment

That last example illustrates size containment based on the element’s inline-size, which is a fancy way of saying its width. When we talk about normal document flow on the web, we’re talking about elements that flow in an inline direction and a block direction that corresponds to width and height, respectively, in a horizontal writing mode. If we were to rotate the writing mode so that it is vertical, then “inline” would refer to the height instead and “block” to the width.

Consider the following HTML:

<div class="cards-container">
  <ul class="cards">
    <li class="card"></li>
  </ul>
</div>

We could give the .cards-container element a containment context in the inline direction, allowing us to make changes to its descendants when its width becomes too small to properly display everything in the current layout. We keep the same syntax as in a normal media query but swap @media for @container

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

  @container (width < 700px) {
  .cards {
    background-color: red;
  }
}

Container syntax works almost the same as media queries, so we can use the and, or, and not operators to chain different queries together to match multiple conditions.

@container (width < 700px) or (width > 1200px) {
  .cards {
    background-color: red;
  }
}

Elements in a size query look for the closest ancestor with size containment so we can apply changes to elements deeper in the DOM, like the .card element in our earlier example. If there is no size containment context, then the @container at-rule won’t have any effect.

/* 👎 
 * Apply styles based on the closest container, .cards-container
 */
@container (width < 700px) {
  .card {
    background-color: black;
  }
}

Just looking for the closest container is messy, so it’s good practice to name containers using the container-name property and then specifying which container we’re tracking in the container query just after the @container at-rule.

.cards-container {
  container-name: cardsContainer;
  container-type: inline-size;
}

@container cardsContainer (width < 700px) {
  .card {
    background-color: #000;
  }
}

We can use the shorthand container property to set the container name and type in a single declaration:

.cards-container {
  container: cardsContainer / inline-size;

  /* Equivalent to: */
  container-name: cardsContainer;
  container-type: inline-size;
}

The other container-type we can set is size, which works exactly like inline-size — only the containment context is both the inline and block directions. That means we can also query the container’s height sizing in addition to its width sizing.

/* When container is less than 700px wide */
@container (width < 700px) {
  .card {
    background-color: black;
  }
}

/* When container is less than 900px tall */
@container (height < 900px) {
  .card {
    background-color: white;
  }
}

And it’s worth noting here that if two separate (not chained) container rules match, the most specific selector wins, true to how the CSS Cascade works.

So far, we’ve touched on the concept of CSS Container Queries at its most basic. We define the type of containment we want on an element (we looked specifically at size containment) and then query that container accordingly.

Container Style Queries

The third value that is accepted by the container-type property is normal, and it sets style containment on an element. Both inline-size and size are stable across all major browsers, but normal is newer and only has modest support at the moment.

I consider normal a bit of an oddball because we don’t have to explicitly declare it on an element since all elements are style containers with style containment right out of the box. It’s possible you’ll never write it out yourself or see it in the wild.

.parent {
  /* Unnecessary */
  container-type: normal;
}

If you do write it or see it, it’s likely to undo size containment declared somewhere else. But even then, it’s possible to reset containment with the global initial or revert keywords.

.parent {
  /* All of these (re)set style containment */
  container-type: normal;
  container-type: initial;
  container-type: revert;
}

Let’s look at a simple and somewhat contrived example to get the point across. We can define a custom property in a container, say a --theme.

.cards-container {
  --theme: dark;
}

From here, we can check if the container has that desired property and, if it does, apply styles to its descendant elements. We can’t directly style the container since it could unleash an infinite loop of changing the styles and querying the styles.

.cards-container {
  --theme: dark;
}

@container style(--theme: dark) {
  .cards {
    background-color: black;
  }
}

See that style() function? In the future, we may want to check if an element has a max-width: 400px through a style query instead of checking if the element’s computed value is bigger than 400px in a size query. That’s why we use the style() wrapper to differentiate style queries from size queries.

/* Size query */
@container (width > 60ch) {
  .cards {
    flex-direction: column;
  }
}

/* Style query */
@container style(--theme: dark) {
  .cards {
    background-color: black;
  }
}

Both types of container queries look for the closest ancestor with a corresponding containment-type. In a style() query, it will always be the parent since all elements have style containment by default. In this case, the direct parent of the .cards element in our ongoing example is the .cards-container element. If we want to query non-direct parents, we will need the container-name property to differentiate between containers when making a query.

.cards-container {
  container-name: cardsContainer;
  --theme: dark;
}

@container cardsContainer style(--theme: dark) {
  .card {
    color: white;
  }
}
Weird and Confusing Things About Container Style Queries

Style queries are completely new and bring something never seen in CSS, so they are bound to have some confusing qualities as we wrap our heads around them — some that are completely intentional and well thought-out and some that are perhaps unintentional and may be updated in future versions of the specification.

Style and Size Containment Aren’t Mutually Exclusive

One intentional perk, for example, is that a container can have both size and style containment. No one would fault you for expecting that size and style containment are mutually exclusive concerns, so setting an element to something like container-type: inline-size would make all style queries useless.

However, another funny thing about container queries is that elements have style containment by default, and there isn’t really a way to remove it. Check out this next example:

.cards-container {
  container-type: inline-size;
  --theme: dark;
}

@container style(--theme: dark) {
  .card {
    background-color: black;
  }
}

@container (width < 700px) {
  .card {
    background-color: red;
  }
}

See that? We can still query the elements by style even when we explicitly set the container-type to inline-size. This seems contradictory at first, but it does make sense, considering that style and size queries are computed independently. It’s better this way since both queries don’t necessarily conflict with each other; a style query could change the colors in an element depending on a custom property, while a container query changes an element’s flex-direction when it gets too small for its contents.

But We Can Achieve the Same Thing With CSS Classes and IDs

Most container query guides and tutorials I’ve seen use similar examples to demonstrate the general concept, but I can’t stop thinking no matter how cool style queries are, we can achieve the same result using classes or IDs and with less boilerplate. Instead of passing the state as an inline style, we could simply add it as a class.

<ol>
  <li class="item first">
    <img src="..." alt="Roi's avatar" />
    <h2>Roi</h2>
  </li>
  <li class="item second"><!-- etc. --></li>
  <li class="item third"><!-- etc. --></li>
  <li class="item"><!-- etc. --></li>
  <li class="item"><!-- etc. --></li>
</ol>

Alternatively, we could add the position number directly inside an id so we don’t have to convert the number into a string:

<ol>
  <li class="item" id="item-1">
    <img src="..." alt="Roi's avatar" />
    <h2>Roi</h2>
  </li>
  <li class="item" id="item-2"><!-- etc. --></li>
  <li class="item" id="item-3"><!-- etc. --></li>
  <li class="item" id="item-4"><!-- etc. --></li>
  <li class="item" id="item-5"><!-- etc. --></li>
</ol>

Both of these approaches leave us with cleaner HTML than the container queries approach. With style queries, we have to wrap our elements inside a container — even if we don’t semantically need it — because of the fact that containers (rightly) are unable to style themselves.

We also have less boilerplate-y code on the CSS side:

#item-1 {
  background: linear-gradient(45deg, yellow, orange); 
}

#item-2 {
  background: linear-gradient(45deg, grey, white);
}

#item-3 {
  background: linear-gradient(45deg, brown, peru);
}

See the Pen Style Queries Use Case Replaced with Classes [forked] by Monknow.

As an aside, I know that using IDs as styling hooks is often viewed as a no-no, but that’s only because IDs must be unique in the sense that no two instances of the same ID are on the page at the same time. In this instance, there will never be more than one first-place, second-place, or third-place player on the page, making IDs a safe and appropriate choice in this situation. But, yes, we could also use some other type of selector, say a data-* attribute.

There is something that could add a lot of value to style queries: a range syntax for querying styles. This is an open feature that Miriam Suzanne proposed in 2023, the idea being that it queries numerical values using range comparisons just like size queries.

Imagine if we wanted to apply a light purple background color to the rest of the top ten players in the leaderboard example. Instead of adding a query for each position from four to ten, we could add a query that checks a range of values. The syntax is obviously not in the spec at this time, but let’s say it looks something like this just to push the point across:

/* Do not try this at home! */
@container leaderboard style(4 >= --position <= 10) {
  .item {
    background: linear-gradient(45deg, purple, fuchsia);
  }
}

In this fictional and hypothetical example, we’re:

  • Tracking a container called leaderboard,
  • Making a style() query against the container,
  • Evaluating the --position custom property,
  • Looking for a condition where the custom property is set to a value equal to a number that is greater than or equal to 4 and less than or equal to 10.
  • If the custom property is a value within that range, we set a player’s background color to a linear-gradient() that goes from purple to fuschia.

This is very cool, but if this kind of behavior is likely to be done using components in modern frameworks, like React or Vue, we could also set up a range in JavaScript and toggle on a .top-ten class when the condition is met.

See the Pen Style Ranged Queries Use Case Replaced with Classes [forked] by Monknow.

Sure, it’s great to see that we can do this sort of thing directly in CSS, but it’s also something with an existing well-established solution.

Separating Style Logic From Logic Logic

So far, style queries don’t seem to be the most convenient solution for the leaderboard use case we looked at, but I wouldn’t deem them useless solely because we can achieve the same thing with JavaScript. I am a big advocate of reaching for JavaScript only when necessary and only in sprinkles, but style queries, the ones where we can only check for custom properties, are most likely to be useful when paired with a UI framework where we can easily reach for JavaScript within a component. I have been using Astro an awful lot lately, and in that context, I don’t see why I would choose a style query over programmatically changing a class or ID.

However, a case can be made that implementing style logic inside a component is messy. Maybe we should keep the logic regarding styles in the CSS away from the rest of the logic logic, i.e., the stateful changes inside a component like conditional rendering or functions like useState and useEffect in React. The style logic would be the conditional checks we do to add or remove class names or IDs in order to change styles.

If we backtrack to our leaderboard example, checking a player’s position to apply different styles would be style logic. We could indeed check that a player’s leaderboard position is between four and ten using JavaScript to programmatically add a .top-ten class, but it would mean leaking our style logic into our component. In React (for familiarity, but it would be similar to other frameworks), the component may look like this:

const LeaderboardItem = ({position}) => {
  <li className={item ${position &gt;= 4 && position &lt;= 10 ? "top-ten" : ""}} id={item-${position}}>
    <img src="..." alt="Roi's avatar" />
    <h2>Roi</h2>
  </li>;
};

Besides this being ugly-looking code, adding the style logic in JSX can get messy. Meanwhile, style queries can pass the --position value to the styles and handle the logic directly in the CSS where it is being used.

const LeaderboardItem = ({position}) => {
  <li className="item" style={{"--position": position}}>
    <img src="..." alt="Roi's avatar" />
    <h2>Roi</h2>
  </li>;
};

Much cleaner, and I think this is closer to the value proposition of style queries. But at the same time, this example makes a large leap of assumption that we will get a range syntax for style queries at some point, which is not a done deal.

Conclusion

There are lots of teams working on making modern CSS better, and not all features have to be groundbreaking miraculous additions.

Size queries are definitely an upgrade from media queries for responsive design, but style queries appear to be more of a solution looking for a problem.

It simply doesn’t solve any specific issue or is better enough to replace other approaches, at least as far as I am aware.

Even if, in the future, style queries will be able to check for any property, that introduces a whole new can of worms where styles are capable of reacting to other styles. This seems exciting at first, but I can’t shake the feeling it would be unnecessary and even chaotic: styles reacting to styles, reacting to styles, and so on with an unnecessary side of boilerplate. I’d argue that a more prudent approach is to write all your styles declaratively together in one place.

Maybe it would be useful for web extensions (like Dark Reader) so they can better check styles in third-party websites? I can’t clearly see it. If you have any suggestions on how CSS Container Style Queries can be used to write better CSS that I may have overlooked, please let me know in the comments! I’d love to know how you’re thinking about them and the sorts of ways you imagine yourself using them in your work.

Chris’ Corner: Real World CSS

Category Image 035

I enjoyed Lee Robinson’s take on How I’m Writing CSS in 2024. Rather than jump right into tools and syntax, it starts with the user:

What does a great experience look like loading stylesheets when visiting a website?

  1. Stylesheets should load as fast as possible (small file sizes)
  2. Stylesheets should not re-download unless changed (proper caching headers)
  3. The page content should have minimal or no layout shift
  4. Fonts should load as fast as possible and minimize layout shift

Agreed! Number 3, and to some degree 4, are almost more in the JavaScript bucket than CSS, but it’s a good starter list. I’d add “The page styles shouldn’t interfere with default accessibility”.

Then, after those, the developer experience is considered:

How can the DX of the styling tools we use help us create a better UX?

  1. Prune unused styles, minify, and compress CSS for smaller file sizes
  2. Generate hashed file names to enable safe, immutable caching
  3. Bundle CSS files together to make fewer network requests
  4. Prevent naming collisions to avoid visual regressions

What about to help us write more maintainable, enjoyable CSS?

  1. Easy to delete styles when deleting corresponding UI code
  2. Easy to adhere to a design system or set of themes
  3. Editor feedback with TypeScript support, autocompletion, and linting
  4. Receive tooling feedback in-editor to prevent errors (type checking, linting)

I like how the DX concerns are about making things easier that the UX demands. I want all that stuff! Although I admit I still bristle at the idea of dealing with unused styles. It’s very hard to properly detect unused styles and I worry about tools making those decisions.

Lee’s ultimate recommendations are CSS Modules, Tailwind, or StyleX (or just vanilla CSS on simple stuff), and I feel like those feel fair based on his own journey and accomplish the things he laid out. I’m a fan of the CSS Modules approach myself. It’s largely vanilla CSS, but with great scoping built in, it couples to components nicely, and is so well established it’s everywhere you need it.


Speaking of writing CSS in the real world, Ahmad Shadeed did quite a deep dive of looking at the TechCrunch Layout and approaching it with modern techniques.

Sure, it’s just a three column layout, but the different columns have all sorts of different constraints. The first is in a fixed position, the main content has a maximum width but is otherwise fluid as well as contains nested grids. There is a maximum width overall too, with the third column involving absolute positioning. That’s without getting into the (five!) major breakpoints and footer complexities. If you’re into nerding out on CSS layout, Ahmad tackles it literally five different ways, ultimately landing on a nice CSS grid powered technique. He called it easy to implement, but looking at the column declarations I think it only looks easy to someone who was on his fifth iteration. 🤣. And that’s only half the article.


To think that Ahmad’s tackling of a complex layout, in the end, only boiled down to a few lines of CSS is rather incredible. CSS is certainly more powerful. But is it easier? Geoff Graham thinks yeah, it is a little easier to write actually, in some ways.

To name a few, grouping styles is easier, centering is easier, translation needs are easier, and spacing is easier. Geoff names more. And by easier, really truly easier in all ways. Less and more direct code that is easier to reason about and does what it says.


Roman Komarov outlines The Shrinkwrap Problem, which is maybe a little niche but certainly a very interesting layout situation. The deal is that if content wraps, the element essentially takes up all available width. Not that strange, but when you look at how a wrapped title looks with text-wrap: balance;, for example, it looks a little weird. A header might only take up half the space visually, yet still take up all the available space.

Roman goes really deep on this, with solutions that involve even new tech like anchor positioning which is an awfully weird thing to invoke just for this, but hey, needs are needs. Just when you think this is all far too much for such a niche thing, Roman gets to the use-cases which are actually pretty basic and straightforward. Things like chat bubbles where full-width bubbles would look awkward. Or decorations on either side of a header.


David Bushell has a fun and illuminating post about button-specific CSS styles.

Have you ever repeatedly tapped on a button only for the page to zoom in unexpectedly? Rewind and fast-forward buttons in an audio player for example. This unwanted side effect can be removed with touch-action.

There are four others in there that are all in the decent-chance-you-hadn’t-thought-of-it category.

Chris’ Corner: More Surprising Powers of CSS

Category Image 052

Kilian Valkhof has a good one this week, You don’t need JavaScript for that, as part of the HTMLHell yearly Advent Calendar (of blog posts). He opens with the rule of least power:

Choose the least powerful language suitable for a given purpose.

That’s fun for us CSS nerds, because CSS is a pretty low language when it comes to building websites. (It doesn’t mean you should try to write an accordion component in Assembly.) It means, as he puts it:

On the web this means preferring HTML over CSS, and then CSS over JS.

If you’re at the JS level already, it means preferring JS over a JS framework or meta language. It’s not that those things aren’t valuable (Heck, one of the purposes of CodePen is making using language abstractions and library easier) it’s that, well, it’s just a better idea to go lower level. There is less to download, less to break, higher chances of the browser optimizing it, higher chances it will be accessible, higher chances it will last over time. That stuff matters.

Killian opens with a custom “toggle” component, and really, component is probably too strong a word. It’s just a styled HTML checkbox. It’s not even all that much CSS, and no JS is used at all, to make this:

While I was reading, where I thought Killian was going was using an <input type="checkbox"> actually turn on and off features on a website. That’s the kind of thing that feels like definite-JavaScript territory, and yet, because of the classic “Checkbox Hack” (e.g. using the :checked selector and selector combinators) we actually can control a lot of a website with just a checkbox.

The ability to do that, control a website with a checkbox, has increased dramatically now that we have :has() in CSS. For instance:

<body>
  ... literally anywhere deep in the bowels of the DOM ...
  <input class="feature" type="checkbox">

We don’t have to worry about where in the DOM this checkbox is anymore, we can style anything there like this now:

body:has([.feature:checked]) .literally-anything {
  /* do styles */
}

We can start the styling choices way up at the body level with :has(), looking for that checkbox (essentially a toggle), and style anything on the page on if it is checked or not. That feels extraordinarily powerful to me.


Speaking of the mental crossover between CSS and JavaScript, Yaphi Berhanu’s Write Better CSS By Borrowing Ideas From JavaScript Functions was interesting. CSS doesn’t actually have functions (but just wait for it!) but that doesn’t mean we can’t consider how our thinking about the code can relate. I liked the bit about considering too many parameters vs not enough parameters. Yaphi connects that idea too too many selectors with too many repeat declarations, and yeah I can get behind that.

But where my mind goes is taking that lesson and trying to apply it to Custom Properties. I have seen (and written!) CSS that just takes Custom Property usage just too far — to the point where the code feels harder to reason about and maintain, not easier.

Like if you were going to make a variable for an animation…

.do-animation {
  animation: 3s ease-in 1s infinite reverse both running slidein;
}

You could decide that the right way to make this controllable and re-suable is something like:

:root {
  --animation-duration: 3s;
  --animation-easing: ease-in;
  --animation-delay: 1s;
  --animation-direction: reverse;
  --animation-fill-mode: both;
  --animation-play-state: running;
  --animation-name: slidein;
}

.do-animation {
  animation: var(--animation-duration) var(--animation-easing) var(--animation-delay) var(--animation-direction) var(--animation-fill-mode) var(--animation-play-state) var(--animation-name);
}

Maybe you love that? But I think it’s already going too far. And it could easily go further, since you could have another whole set of variables that set default fallbacks, for example.

It depends on what you want to do, but if your goal is simply “reusability” then doing like…

:root {
  --brandAnimation: 3s ease-in 1s infinite reverse both running slidein;
}

And then using it when you needed it is closer to baby bear’s porridge.


Little reminder: don’t sleep on View Transitions.

We won’t know what Interop 2024 will do until January, but based on the amount of upvotes from the proposals, I think it stands a good chance.

Jeremy Keith has a well measured take in Add view transitions to your website. It’s got links to good resources and examples on using it, like Tyler’s Gaw’s great post. It ends with an update and warning about how maybe it’s not such a great idea because it may “poison the feature with legacy content”. Meaning if too many people do this, the powers that be will be hesitant to change anything, even for the better. I agree that would suck if such a choice is made, as I do think there is room for improvement (e.g. transition groups). I dunno though, I think because this stuff is literally behind a feature flag, that’s enough of an at your own risk warning. If they want to change something that breaks any code I’ve shipped, I’m cool with that. I know the stakes.

I think we’ll start seeing more interesting examples and experiences soon. And! Gotchas! Like Nic Chan’s View transitions and stacking context: Why does my CSS View Transition ignore z-index? It is confusing. It’s like the elements kinda stop being elements when that are being transitioned. They look like elements, but they are really temporary rasterized ghosts. You can still select them with special pseudo element selectors though, so you should be able to get done what you need.


There is this inherit complexity with doing the whole Dark Mode / Light Mode thing on websites. There is both a system-wide preference that you can choose to honor, and that’s a good idea. And it’s likely that a site implementing this also offers a UI toggle to set the theme. So that’s two separate bits of preference you need to deal with, and the code likely handles them separately.

Christopher Kirk-Nielsen’s A Future of Themes with CSS Container Style Queries gives us a good taste of this. Let’s jump right to code:

/* No theme has been set, or override set to light mode */
html:where(:not([data-theme])),
:root[data-theme=light] {
  --color: black;
  --background: antiquewhite;
  /* … and all your other "variables" */
}

/* Apply dark mode if user preferences call for it, and if the user hasn't selected a theme override */
@media (prefers-color-scheme: dark) {
  html:where(:not([data-theme])) {
    --color: ghostwhite;
    --background: midnightblue;
    /* … and all your other "variables" */
  }
}

/* Explicitly set the properties for the selected theme */
:root[data-theme=dark] {
  --color: ghostwhite;
  --background: midnightblue;
  /* … and all your other "variables" */
}

If you read though that I think you’ll see, you need @media to deal with system preferences, then the HTML attributes to deal with a direct-set preference, and one needs to overlap the other.

Enter Style Queries. What they really are is: “when an element has a certain custom property and value, also do this other stuff.” Christopher’s “future approach” using them is more code, but I’d agree that’s more clean and readable.

/* Optionally, we can define the theme variable */
@property --theme {
  syntax: '<custom-ident>'; /* We could list all the themes separated by a pipe character but this will do! */
  inherits: true;
  initial-value: light;
}

/* Assign the --theme property accordingly */
html:where(:not([data-theme])),
:root[data-theme=light] {
  --theme: light;
}

@media (prefers-color-scheme: dark) {
  html:where(:not([data-theme])) {
    --theme: dark;
  }
}

:root[data-theme=dark] {
  --theme: dark;
}

/* Then assign the custom properties based on the active theme */
@container style(--theme: light) {
  body {
    --color: black;
    --background: antiquewhite;
    /* … and all your other "variables" */
  }
}

@container style(--theme: dark) {
  body {
    --color: ghostwhite;
    --background: midnightblue;
    /* … and all your other "variables" */
  }
}

Check out Christopher’s article though, there is a lot more to go over.


Let’s end with a little good ol’ fashioned CSS enthusiasm.

For many, many years, creating high-quality websites largely meant that designers had to fight what felt like an uphill battle to somehow make their ideas work in browsers. At the same time, sentences like “I’m really sorry, but this solution you designed just can’t be done with CSS” […]

This has now changed to the opposite.

Want to emulate and confidently design a layout that leverages the potential of CSS Grid in any of the major design tools like Figma, Adobe XD, or Sketch? Not possible. 

Matthias Ott, The New CSS

And:

I still keep Sass around, as well as PostCSS, for things that CSS just wont ever be able to do (nor should it), but they’re fading into the background, rather than being at the forefront of my mind when writing styles. And for small, simple projects, I don’t use them at all. Just pure CSS. I haven’t found it lacking.

Jeff Sandberg, CSS is fun again

Modern CSS Layouts: You Might Not Need A Framework For That

Category Image 052

Establishing layouts in CSS is something that we, as developers, often delegate to whatever framework we’re most comfortable using. And even though it’s possible to configure a framework to get just what we need out of it, how often have you integrated an entire CSS library simply for its layout features? I’m sure many of us have done it at some point, dating back to the days of 960.gs, Bootstrap, Susy, and Foundation.

Modern CSS features have significantly cut the need to reach for a framework simply for its layout. Yet, I continue to see it happen. Or, I empathize with many of my colleagues who find themselves re-creating the same Grid or Flexbox layout over and over again.

In this article, we will gain greater control over web layouts. Specifically, we will create four CSS classes that you will be able to take and use immediately on just about any project or place where you need a particular layout that can be configured to your needs.

While the concepts we cover are key, the real thing I want you to take away from this is the confidence to use CSS for those things we tend to avoid doing ourselves. Layouts used to be a challenge on the same level of styling form controls. Certain creative layouts may still be difficult to pull off, but the way CSS is designed today solves the burdens of the established layout patterns we’ve been outsourcing and re-creating for many years.

What We’re Making

We’re going to establish four CSS classes, each with a different layout approach. The idea is that if you need, say, a fluid layout based on Flexbox, you have it ready. The same goes for the three other classes we’re making.

And what exactly are these classes? Two of them are Flexbox layouts, and the other two are Grid layouts, each for a specific purpose. We’ll even extend the Grid layouts to leverage CSS Subgrid for when that’s needed.

Within those two groups of Flexbox and Grid layouts are two utility classes: one that auto-fills the available space — we’re calling these “fluid” layouts — and another where we have greater control over the columns and rows — we’re calling these “repeating” layouts.

Finally, we’ll integrate CSS Container Queries so that these layouts respond to their own size for responsive behavior rather than the size of the viewport. Where we’ll start, though, is organizing our work into Cascade Layers, which further allow you to control the level of specificity and prevent style conflicts with your own CSS.

Setup: Cascade Layers & CSS Variables

A technique that I’ve used a few times is to define Cascade Layers at the start of a stylesheet. I like this idea not only because it keeps styles neat and organized but also because we can influence the specificity of the styles in each layer by organizing the layers in a specific order. All of this makes the utility classes we’re making easier to maintain and integrate into your own work without running into specificity battles.

I think the following three layers are enough for this work:

@layer reset, theme, layout;

Notice the order because it really, really matters. The reset layer comes first, making it the least specific layer of the bunch. The layout layer comes in at the end, making it the most specific set of styles, giving them higher priority than the styles in the other two layers. If we add an unlayered style, that one would be added last and thus have the highest specificity.

Related: “Getting Started With Cascade Layers” by Stephanie Eckles.

Let’s briefly cover how we’ll use each layer in our work.

Reset Layer

The reset layer will contain styles for any user agent styles we want to “reset”. You can add your own resets here, or if you already have a reset in your project, you can safely move on without this particular layer. However, do remember that un-layered styles will be read last, so wrap them in this layer if needed.

I’m just going to drop in the popular box-sizing declaration that ensures all elements are sized consistently by the border-box in accordance with the CSS Box Model.

@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  body {
    margin: 0;
  }
}

Theme Layer

This layer provides variables scoped to the :root element. I like the idea of scoping variables this high up the chain because layout containers — like the utility classes we’re creating — are often wrappers around lots of other elements, and a global scope ensures that the variables are available anywhere we need them. That said, it is possible to scope these locally to another element if you need to.

Now, whatever makes for “good” default values for the variables will absolutely depend on the project. I’m going to set these with particular values, but do not assume for a moment that you have to stick with them — this is very much a configurable system that you can adapt to your needs.

Here are the only three variables we need for all four layouts:

@layer theme {
  :root {
    --layout-fluid-min: 35ch;
    --layout-default-repeat: 3;
    --layout-default-gap: 3vmax;
  }
}

In order, these map to the following:

Notice: The variables are prefixed with layout-, which I’m using as an identifier for layout-specific values. This is my personal preference for structuring this work, but please choose a naming convention that fits your mental model — naming things can be hard!

Layout Layer

This layer will hold our utility class rulesets, which is where all the magic happens. For the grid, we will include a fifth class specifically for using CSS Subgrid within a grid container for those possible use cases.

@layer layout {  
  .repeating-grid {}
  .repeating-flex {}
  .fluid-grid {}
  .fluid-flex {}

  .subgrid-rows {}
}

Now that all our layers are organized, variables are set, and rulesets are defined, we can begin working on the layouts themselves. We will start with the “repeating” layouts, one based on CSS Grid and the other using Flexbox.

Repeating Grid And Flex Layouts

I think it’s a good idea to start with the “simplest” layout and scale up the complexity from there. So, we’ll tackle the “Repeating Grid” layout first as an introduction to the overarching technique we will be using for the other layouts.

Repeating Grid

If we head into the @layout layer, that’s where we’ll find the .repeating-grid ruleset, where we’ll write the styles for this specific layout. Essentially, we are setting this up as a grid container and applying the variables we created to it to establish layout columns and spacing between them.

.repeating-grid {
  display: grid;
  grid-template-columns: repeat(var(--layout-default-repeat), 1fr);
  gap: var(--layout-default-gap);
}

It’s not too complicated so far, right? We now have a grid container with three equally sized columns that take up one fraction (1fr) of the available space with a gap between them.

This is all fine and dandy, but we do want to take this a step further and turn this into a system where you can configure the number of columns and the size of the gap. I’m going to introduce two new variables scoped to this grid:

  • --_grid-repeat: The number of grid columns.
  • --_repeating-grid-gap: The amount of space between grid items.

Did you notice that I’ve prefixed these variables with an underscore? This was actually a JavaScript convention to specify variables that are “private” — or locally-scoped — before we had const and let to help with that. Feel free to rename these however you see fit, but I wanted to note that up-front in case you’re wondering why the underscore is there.

.repeating-grid {
  --_grid-repeat: var(--grid-repeat, var(--layout-default-repeat));
  --_repeating-grid-gap: var(--grid-gap, var(--layout-default-gap));

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

Notice: These variables are set to the variables in the @theme layer. I like the idea of assigning a global variable to a locally-scoped variable. This way, we get to leverage the default values we set in @theme but can easily override them without interfering anywhere else the global variables are used.

Now let’s put those variables to use on the style rules from before in the same .repeating-grid ruleset:

.repeating-grid {
  --_grid-repeat: var(--grid-repeat, var(--layout-default-repeat));
  --_repeating-grid-gap: var(--grid-gap, var(--layout-default-gap));

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

What happens from here when we apply the .repeating-grid to an element in HTML? Let’s imagine that we are working with the following simplified markup:

<section class="repeating-grid">
  <div></div>
  <div></div>
  <div></div>
</section>

If we were to apply a background-color and height to those divs, we would get a nice set of boxes that are placed into three equally-sized columns, where any divs that do not fit on the first row automatically wrap to the next row.

Time to put the process we established with the Repeating Grid layout to use in this Repeating Flex layout. This time, we jump straight to defining the private variables on the .repeating-flex ruleset in the @layout layer since we already know what we’re doing.

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));
}

Again, we have two locally-scoped variables used to override the default values assigned to the globally-scoped variables. Now, we apply them to the style declarations.

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));

  display: flex;
  flex-wrap: wrap;
  gap: var(--_repeating-flex-gap);
}

We’re only using one of the variables to set the gap size between flex items at the moment, but that will change in a bit. For now, the important thing to note is that we are using the flex-wrap property to tell Flexbox that it’s OK to let additional items in the layout wrap into multiple rows rather than trying to pack everything in a single row.

But once we do that, we also have to configure how the flex items shrink or expand based on whatever amount of available space is remaining. Let’s nest those styles inside the parent ruleset:

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));

  display: flex;
  flex-wrap: wrap;
  gap: var(--_repeating-flex-gap);

  > * {
    flex: 1 1 calc((100% / var(--_flex-repeat)) - var(--_gap-repeater-calc));
  }
}

If you’re wondering why I’m using the universal selector (*), it’s because we can’t assume that the layout items will always be divs. Perhaps they are <article> elements, <section>s, or something else entirely. The child combinator (>) ensures that we’re only selecting elements that are direct children of the utility class to prevent leakage into other ancestor styles.

The flex shorthand property is one of those that’s been around for many years now but still seems to mystify many of us. Before we unpack it, did you also notice that we have a new locally-scoped --_gap-repeater-calc variable that needs to be defined? Let’s do this:

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));

  /* New variables */
  --_gap-count: calc(var(--_flex-repeat) - 1);
  --_gap-repeater-calc: calc(
    var(--_repeating-flex-gap) / var(--_flex-repeat) * var(--_gap-count)
  );

  display: flex;
  flex-wrap: wrap;
  gap: var(--_repeating-flex-gap);

  > * {
    flex: 1 1 calc((100% / var(--_flex-repeat)) - var(--_gap-repeater-calc));
  }
}

Whoa, we actually created a second variable that --_gap-repeater-calc can use to properly calculate the third flex value, which corresponds to the flex-basis property, i.e., the “ideal” size we want the flex items to be.

If we take out the variable abstractions from our code above, then this is what we’re looking at:

.repeating-flex {
  display: flex;
  flex-wrap: wrap;
  gap: 3vmax

  > * {
    flex: 1 1 calc((100% / 3) - calc(3vmax / 3 * 2));
  }
}

Hopefully, this will help you see what sort of math the browser has to do to size the flexible items in the layout. Of course, those values change if the variables’ values change. But, in short, elements that are direct children of the .repeating-flex utility class are allowed to grow (flex-grow: 1) and shrink (flex-shrink: 1) based on the amount of available space while we inform the browser that the initial size (i.e., flex-basis) of each flex item is equal to some calc()-ulated value.

Because we had to introduce a couple of new variables to get here, I’d like to at least explain what they do:

  • --_gap-count: This stores the number of gaps between layout items by subtracting 1 from --_flex-repeat. There’s one less gap in the number of items because there’s no gap before the first item or after the last item.
  • --_gap-repeater-calc: This calculates the total gap size based on the individual item’s gap size and the total number of gaps between items.

From there, we calculate the total gap size more efficiently with the following formula:

calc(var(--_repeating-flex-gap) / var(--_flex-repeat) * var(--_gap-count))

Let’s break that down further because it’s an inception of variables referencing other variables. In this example, we already provided our repeat-counting private variable, which falls back to the default repeater by setting the --layout-default-repeat variable.

This sets a gap, but we’re not done yet because, with flexible containers, we need to define the flex behavior of the container’s direct children so that they grow (flex-grow: 1), shrink (flex-shrink: 1), and with a flex-basis value that is calculated by multiplying the repeater by the total number of gaps between items.

Next, we divide the individual gap size (--_repeating-flex-gap) by the number of repetitions (--_flex-repeat)) to equally distribute the gap size between each item in the layout. Then, we multiply that gap size value by one minus the total number of gaps with the --_gap-count variable.

And that concludes our repeating grids! Pretty fun, or at least interesting, right? I like a bit of math.

Before we move to the final two layout utility classes we’re making, you might be wondering why we want so many abstractions of the same variable, as we start with one globally-scoped variable referenced by a locally-scoped variable which, in turn, can be referenced and overridden again by yet another variable that is locally scoped to another ruleset. We could simply work with the global variable the whole time, but I’ve taken us through the extra steps of abstraction.

I like it this way because of the following:

  1. I can peek at the HTML and instantly see which layout approach is in use: .repeating-grid or .repeating-flex.
  2. It maintains a certain separation of concerns that keeps styles in order without running into specificity conflicts.

See how clear and understandable the markup is:

<section class="repeating-flex footer-usps">
  <div></div>
  <div></div>
  <div></div>
</section>

The corresponding CSS is likely to be a slim ruleset for the semantic .footer-usps class that simply updates variable values:

.footer-usps {
  --flex-repeat: 3;
  --flex-gap: 2rem;
}

This gives me all of the context I need: the type of layout, what it is used for, and where to find the variables. I think that’s handy, but you certainly could get by without the added abstractions if you’re looking to streamline things a bit.

Fluid Grid And Flex Layouts

All the repeating we’ve done until now is fun, and we can manipulate the number of repeats with container queries and media queries. But rather than repeating columns manually, let’s make the browser do the work for us with fluid layouts that automatically fill whatever empty space is available in the layout container. We may sacrifice a small amount of control with these two utilities, but we get to leverage the browser’s ability to “intelligently” place layout items with a few CSS hints.

Fluid Grid

Once again, we’re starting with the variables and working our way to the calculations and style rules. Specifically, we’re defining a variable called --_fluid-grid-min that manages a column’s minimum width.

Let’s take a rather trivial example and say we want a grid column that’s at least 400px wide with a 20px gap. In this situation, we’re essentially working with a two-column grid when the container is greater than 820px wide. If the container is narrower than 820px, the column stretches out to the container’s full width.

If we want to go for a three-column grid instead, the container’s width should be about 1240px wide. It’s all about controlling the minimum sizing values in the gap.

.fluid-grid {
  --_fluid-grid-min: var(--fluid-grid-min, var(--layout-fluid-min));
  --_fluid-grid-gap: var(--grid-gap, var(--layout-default-gap));
}

That establishes the variables we need to calculate and set styles on the .fluid-grid layout. This is the full code we are unpacking:

 .fluid-grid {
  --_fluid-grid-min: var(--fluid-grid-min, var(--layout-fluid-min));
  --_fluid-grid-gap: var(--grid-gap, var(--layout-default-gap));

  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(var(--_fluid-grid-min), 100%), 1fr)
  );
  gap: var(--_fluid-grid-gap);
}

The display is set to grid, and the gap between items is based on the --fluid-grid-gap variable. The magic is taking place in the grid-template-columns declaration.

This grid uses the repeat() function just as the .repeating-grid utility does. By declaring auto-fit in the function, the browser automatically packs in as many columns as it possibly can in the amount of available space in the layout container. Any columns that can’t fit on a line simply wrap to the next line and occupy the full space that is available there.

Then there’s the minmax() function for setting the minimum and maximum width of the columns. What’s special here is that we’re nesting yet another function, min(), within minmax() (which, remember, is nested in the repeat() function). This a bit of extra logic that sets the minimum width value of each column somewhere in a range between --_fluid-grid-min and 100%, where 100% is a fallback for when --_fluid-grid-min is undefined or is less than 100%. In other words, each column is at least the full 100% width of the grid container.

The “max” half of minmax() is set to 1fr to ensure that each column grows proportionally and maintains equally sized columns.

See the Pen Fluid grid [forked] by utilitybend.

That’s it for the Fluid Grid layout! That said, please do take note that this is a strong grid, particularly when it is combined with modern relative units, e.g. ch, as it produces a grid that only scales from one column to multiple columns based on the size of the content.

Fluid Flex

We pretty much get to re-use all of the code we wrote for the Repeating Flex layout for the Fluid Flex layout, but only we’re setting the flex-basis of each column by its minimum size rather than the number of columns.

.fluid-flex {
  --_fluid-flex-min: var(--fluid-flex-min, var(--layout-fluid-min));
  --_fluid-flex-gap: var(--flex-gap, var(--layout-default-gap));

  display: flex;
  flex-wrap: wrap;
  gap: var(--_fluid-flex-gap);

  > * {
    flex: 1 1 var(--_fluid-flex-min);
  }
}

That completes the fourth and final layout utility — but there’s one bonus class we can create to use together with the Repeating Grid and Fluid Grid utilities for even more control over each layout.

Optional: Subgrid Utility

Subgrid is handy because it turns any grid item into a grid container of its own that shares the parent container’s track sizing to keep the two containers aligned without having to redefine tracks by hand. It’s got full browser support and makes our layout system just that much more robust. That’s why we can set it up as a utility to use with the Repeating Grid and Fluid Grid layouts if we need any of the layout items to be grid containers for laying out any child elements they contain.

Here we go:

.subgrid-rows {
  > * {
    display: grid;
    gap: var(--subgrid-gap, 0);
    grid-row: auto / span var(--subgrid-rows, 4);
    grid-template-rows: subgrid;
  }
}

We have two new variables, of course:

  • --subgrid-gap: The vertical gap between grid items.
  • --subgrid-rows The number of grid rows defaulted to 4.

We have a bit of a challenge: How do we control the subgrid items in the rows? I see two possible methods.

Method 1: Inline Styles

We already have a variable that can technically be used directly in the HTML as an inline style:

<section class="fluid-grid subgrid-rows" style="--subgrid-rows: 4;">
  <!-- items -->
</section>

This works like a charm since the variable informs the subgrid how much it can grow.

Method 2: Using The :has() Pseudo-Class

This approach leads to verbose CSS, but sacrificing brevity allows us to automate the layout so it handles practically anything we throw at it without having to update an inline style in the markup.

Check this out:

.subgrid-rows {
  &:has(> :nth-child(1):last-child) { --subgrid-rows: 1; }
  &:has(> :nth-child(2):last-child) { --subgrid-rows: 2; }
  &:has(> :nth-child(3):last-child) { --subgrid-rows: 3; }
  &:has(> :nth-child(4):last-child) { --subgrid-rows: 4; }
  &:has(> :nth-child(5):last-child) { --subgrid-rows: 5; }
  /* etc. */

  > * {
    display: grid;
    gap: var(--subgrid-gap, 0);
    grid-row: auto / span var(--subgrid-rows, 5);
    grid-template-rows: subgrid;
  }
}

The :has() selector checks if a subgrid row is the last child item in the container when that item is either the first, second, third, fourth, fifth, and so on item. For example, the second declaration:

&:has(> :nth-child(2):last-child) { --subgrid-rows: 2; }

…is pretty much saying, “If this is the second subgrid item and it happens to be the last item in the container, then set the number of rows to 2.”

Whether this is too heavy-handed, I don’t know; but I love that we’re able to do it in CSS.

The final missing piece is to declare a container on our children. Let’s give the columns a general class name, .grid-item, that we can override if we need to while setting each one as a container we can query for the sake of updating its layout when it is a certain size (as opposed to responding to the viewport’s size in a media query).

:is(.fluid-grid:not(.subgrid-rows),
.repeating-grid:not(.subgrid-rows),
.repeating-flex, .fluid-flex) {
    > * {
    container: var(--grid-item-container, grid-item) / inline-size;
  }
}

That’s a wild-looking selector, but the verbosity is certainly kept to a minimum thanks to the :is() pseudo-class, which saves us from having to write this as a larger chain selector. It essentially selects the direct children of the other utilities without leaking into .subgrid-rows and inadvertently selecting its direct children.

The container property is a shorthand that combines container-name and container-type into a single declaration separated by a forward slash (/). The name of the container is set to one of our variables, and the type is always its inline-size (i.e., width in a horizontal writing mode).

The container-type property can only be applied to grid containers — not grid items. This means we’re unable to combine it with the grid-template-rows: subgrid value, which is why we needed to write a more complex selector to exclude those instances.

Demo

Check out the following demo to see how everything comes together.

See the Pen Grid system playground [forked] by utilitybend.

The demo is pulling in styles from another pen that contains the full CSS for everything we made together in this article. So, if you were to replace the .fluid-flex classname from the parent container in the HTML with another one of the layout utilities, the layout will update accordingly, allowing you to compare them.

Those classes are the following:

  • .repeating-grid,
  • .repeating-flex,
  • .fluid-grid,
  • .fluid-flex.

And, of course, you have the option of turning any grid items into grid containers using the optional .subgrid-rows class in combination with the .repeating-grid and .fluid-grid utilities.

Conclusion: Write Once And Repurpose

This was quite a journey, wasn’t it? It might seem like a lot of information, but we made something that we only need to write once and can use practically anywhere we need a certain type of layout using modern CSS approaches. I strongly believe these utilities can not only help you in a bunch of your work but also cut any reliance on CSS frameworks that you may be using simply for its layout configurations.

This is a combination of many techniques I’ve seen, one of them being a presentation Stephanie Eckles gave at CSS Day 2023. I love it when people handcraft modern CSS solutions for things we used to work around. Stephanie’s demonstration was clean from the start, which is refreshing as so many other areas of web development are becoming ever more complex.

After learning a bunch from CSS Day 2023, I played with Subgrid on my own and published different ideas from my experiments. That’s all it took for me to realize how extensible modern CSS layout approaches are and inspired me to create a set of utilities I could rely on, perhaps for a long time.

By no means am I trying to convince you or anyone else that these utilities are perfect and should be used everywhere or even that they’re better than <framework-du-jour>. One thing that I do know for certain is that by experimenting with the ideas we covered in this article, you will get a solid feel of how CSS is capable of making layout work much more convenient and robust than ever.

Create something out of this, and share it in the comments if you’re willing — I’m looking forward to seeing some fresh ideas!

Chris’ Corner: Esoteric Stuff in CSS

Category Image 052

Listen I ain’t trying to scare you, but this CSS stuff can get complicated. It doesn’t have to be. CSS is just selectors with key value pairs in the end. The vast majority of CSS I write is pretty darn straightforward, especially once you have a general system (what files go where? how do we generally name things? how do we do variables?). But fair is fair, CSS can get wildly complex. It doesn’t help the complexity situation that anything new added increases that complexity, because, well, everything in CSS affects everything else. Selectors can get confusing and nesting can exacerbate that. Variables can get changed at any time and it’s not always clear from where. The situation on the page (sizing, events, settings) can effect what’s happening in CSS in increasingly bigger ways (querying containers, querying user preferences). If you want to push the edges of what CSS can do, now is the time.

I often save links of other people’s explorations of these edges of CSS. They are fascinating to me, naturally, as one of the proprietors of a front-end coding tool and ex-owner of CSS-Tricks. But sometimes I struggle to find a way to share them and contextualize them because of the complexity. In fact I think it’s fair to not use techniques based on complexity alone, after all, you write it you maintain it.

But let’s remove the limiter and look at CSS stuff regardless of how complex it might be.


I’ll start with one that has broken my brain for a few years now. @James0x57 calls this technique “Space Toggles” (who credits Ana Tudor for the discovery). Lea Verou calls it The -​-var: ; hack. Just to lock in my own understanding, I called it The CSS Custom Property Toggle Trick.

The idea is that CSS custom properties can be valid and invalid, a valid value even just being " " (a single space). You can concatenate that custom property with others to produce either-valid-or-invalid other custom properties. If it’s valid, you can do one thing, if it’s invalid, you can use the fallback for the custom property to do another thing. So it’s essentially if/then logic for custom properties.

The closest I ever came to simplifying it is this. And I admit it’s not exactly straightforward.

The good news is that CSS just decided that if() would be a thing, so now we wait for browser implementations. It should be quite a bit more straightforward, although the ease-of-use will lead to more exotic usage of it and, I’m afraid to admit, more complex CSS.


Have you ever prepped for needing to do a sorting algorithm in a job interview? Can you explain what a Bubble Sort is? It’s the one where you loop through a list comparing the two things next to each other, and swap them if they need to be swapped. You keep running through the list until you don’t do any more swaps. I think, anyway, I’m not some computer genius over here. GrahamTheDev is though! Their article Bubble Sort…in PURE CSS? is bonkers.

It literally works.

This is the complexity that comes through the ability to compare variables (e.g. we have min() and max() in CSS now) and set new variables based on other variables. Write enough of that with delays and animations and you got yourself a bubble sort. Good luck grokking all that though.

And speaking of the edges of CSS:

warning: on mobile the last few animations might not play and just go blank. On PC your fan may spin up!

This is a limitation of using so many calculations that rely on previous calculations…I am not sure if it runs out of memory or what, but I defo pushed the limits of CSS here!


What’s the value of cos(25deg) in CSS? Tyler Gaw wanted to figure it out exactly.

I know that will return a number between -1 and 1. But what number?

You can’t even set any property to exactly that, because just the number it produces is invalid for most properties.

You can do something like width: calc(1px * cos(25deg)) then check the width value in the devtools computed styles panel and get close, but not exact. Also, width: cos(25deg) is invalid CSS and using a custom prop like --v: cos(25deg) doesn’t really work either because the custom prop value is stored as cos(25deg).

But obviously cos(25deg) produces a number?! Tyler found a way to extract the CSS-generated number in JavaScript. I felt like having a poke at it too, but I didn’t get much further. I did discover that the output is a unitless number, so it’s valid for CSS properties that take unitless numbers, like line-height. So you can set line-height directly with it and then read the computed value. The problem is the computed value isn’t the direct output of the cos() function it’s the final px value so… blech, hard.


There is some CSS trickery that involves @keyframe animations. For example, styles applied during an animation apply extra strongly, so a style applied with a animation that runs for, say, a year is a way to do style overrides. Not recommended, heh. Setting animations to a particular place and pausing them is another weird trick for managing state in CSS.

Those, at least, I feel like I can get my brain around quickly. Animations have some extra power (and complexity) lately via Scroll-Driven Animations. Roman Komarov is the master recently of extremely exotic and complex CSS trickery, and his Scroll-Driven State Transfer is the pièce de résistance.

… an ability to mirror a particular state of some element — for example, hovered or focused — to an element in a different place on the page without a common or unique ancestral element that could have been used to deliver that state

You might think of :has() as the new hotness for being able to access state anywhere in CSS from anywhere else, and you’d be right, but apparently this trick is a bit more adaptable, not requiring us to write more CSS later:

… we could implement this with the :has() selector, but for any new values we’d have to modify the stylesheet afterwards

It looks like the trick involves those Space Toggles too, meaning I think this is one of the most complicated CSS tricks I’ve ever seen. Apart from the super math-y stuff that always blows my mind, that is.


We’ve established that animations can be one of the vehicles for CSS complexity, which is true when using them just to animate, not strong-arm them into some wild state machine.

One way animations can get complicated is combining them. It was just the other day I reminded myself that nesting elements and then animating them both essentially doubles the speed of the inside animation. That’s just shooting a cannon from an airplane though, no fancy tech there. CSS actually allows us to manually combine animations, with tools like animation-composition. It can be confusing though! Bramus wrote up The mysterious case of using CSS animation-composition: accumulate on a scale transform outlining just how weird it can get even in relatively simple situations. Maybe you’ll scratch your head too:

Accumulating a scale(0.5) with scale(2) does not give scale(2.5) but scale(1.5)

After scratching my own head about this long enough, it did sort of start to make sense. Even though blur(2) and blur(3) certainly make blur(5) when accumulated, those are both “blur something more”. In the case of scale, 0.5 makes something smaller. So adding 2.0 + 0.5 isn’t 2.5 because they aren’t both “do something more”, one is “make smaller” and one is “make bigger”, hence the spec having to step in and explain.

Solving Media Object Float Issues With CSS Block Formatting Contexts

Category Image 052

Let’s imagine we’re making a small component. It can be anything, really, but let’s use a media object as an example. Nicole Sullivan had a solid definition of media objects from way back in 2010, and you probably already know the pattern well: some form of media (often an image) on the left and text beside it on the right. The media could be an image or a video, for example.

This is the basic HTML for the layout, minimized for brevity:

<section class="container">
  <article class="float-left">
    <img src="https://picsum.photos/100">
      <p>I've never had to cook or clean since I discovered Xyz. They perform all my tasks for me. I recommend them.</p>
      <h3>Dan Somore</h3>
  </article>

  <!-- more articles -->

</section>

This HTML gives us a <section> element that is the container for four <article> elements, where each one is a testimonial container that holds an <img> and a <div> with a block of text — our media objects.

Let’s apply some light styling in CSS:

/* Give the parent container breathing room */
.container {
  padding: 20px;
}

/* 
  Styles for each testimonial container 
  Each container is floated left
*/
.float-left {
  border: 2px solid blue;
  background-color: transparent;
  float: left;
  width: 45%;
  min-height: 150px;
  margin-bottom: 20px;
  margin-right: 20px;
}

/* Testimonial images are floated left */
img {
  float: left;
  margin-right: 10px;
}

This code is by no means perfect. In fact, it introduces the wrapping and overflow issues we’re about to discuss. We will look at these issues together before getting into solutions.

Issue 1: Height Collapsing

When an element is floated in its container, it exits its normal document flow and into a floated position, making no contributions to the container’s height. In a container of many floated media objects, the container element’s height is collapsed to contain only non-floated elements. The collapsed height might be inconspicuous in containers without a border or non-floated elements and could disrupt the layout of other elements after a media object container. However, this issue can be easily discovered if there is a non-floated element in the container, among other floated elements.

Let’s add a border to the parent container to see the height-collapsing effect.

The height of the content is what influences the height of the testimonial container. If the image were in the container’s flow, it would be taller than the text, and the container would adjust to it. But, alas, that’s not the case since we introduced a block formatting context when floating the image.

The popular solution with a single line of CSS on the testimonial’s parent container:


.container {
  overflow: auto;
}

The BFC this generates establishes a new document flow within the page’s root element, containing all the container's child elements, including floated media objects. It effectively prevents the testimonial elements from being displaced beyond the parent container’s borders — no extra divs or pseudo-elements are needed like the clearfix approach.

See the Pen Float Solutions: overflow: auto [forked] by Geoff Graham.

That certainly gets the job done! But I want to show you one more way to do this because I believe it’s the best of the bunch.

The Best Solution: display: flow-root

display: flow-root was introduced to address inconsistencies associated with using overflow for generating BFCs. In fact, display: flow-root was explicitly designed to produce BFC, while the overflow property is designed to manage content that surpasses its container. Consequently, overflow can induce unintended side effects, from unwanted scrollbars to data loss.

That’s why I recommend using display: flow-root. It is meant to create a BFC when you need it, whereas the other solutions are more like workarounds.

Conclusion

CSS block formatting contexts are great because they allow you to leave the main document flow, allowing elements to interact differently in a layout. But, of course, those different interactions can feel like buggy behavior if you’re unaware that you’re actually working in a different formatting context.

This is exactly why we have modern layout techniques like Flexbox and Grid. Before we had them, floats were a nice trick for faking columns. But the BFC they created wasn’t so nice. Hence clever workarounds like the clearfix to create a BFC to wrangle the other BFC.

Perhaps the bigger takeaway from all this, though, is to evaluate your layout strategy. If you’re reaching for a float, is it really the best option for what you’re trying to do? Because if so, you may as well embrace the natural text-wrapping behavior rather than trying to fight it. And if you don’t want to fight it, that’s a sure sign you ought to reach for a more modern layout technique, like Flexbox or Grid.

Resources

Further Reading On SmashingMag

Chris’ Corner: Some AdviCSS

Category Image 052

Get it?! Like “advice”, but for CSS.

When should you nest CSS?

Scott Vandehey says:

There’s a simple answer and a slightly more complicated answer. The simple answer is “avoid nesting.” The more practical, but also more complex answer is “nest pseudo-selectors, parent modifiers, media queries, and selectors that don’t work without nesting.”

The big idea behind avoiding nesting (which is a native CSS feature now, if you hadn’t heard) is that it can lead to specificity increases that just aren’t necessary. Like:

.card {
  .content {
    .byline {

    }
  }
}

That .byline selector probably doesn’t gain anything by being nested like that. Break it out of there and it’ll be more re-usable and easier to override if you need to.

But this:

.card {
  @container (width > 60ch) {

  }
}

Is probably good! It just saves you from having to re-write the .card selector again. Scott gets more in-depth though with more examples and I largely agree.

How do you adjust an existing color light and darker?

I’m a biiiig fan of the relative color syntax, which is great at this job, but before I go on a tangent about that, it’s not well supported yet so let’s not. It’s on the Interop 2024 list though!

Better supported is color-mix(), and Cory LaViska has the story on using it for this job:

Using color-mix(), we can adjust the tint/shade based on the background color, meaning we don’t need to manually select lighter/darker colors for those states. And because we’re using OKLCH, the variations will be perceptually uniform, unlike HSL.

By mixing white and black into colors, and doing it in the OKLCH color space, we can essentially tint and shade the colors and know that we’re doing it evenly across any color we have. This is as opposed to the days when a lot of us tried to use darken() and such in Sass only to find extremely different results across colors.

How are the final values of Custom Properties calculated?

Stephanie Eckles:

Custom properties – aka “CSS variables” – seem fairly straightforward. However, there are some behaviors to be aware of regarding how the browser computes the final values. A misunderstanding of this process may lead to an unexpected or missing value and difficulty troubleshooting and resolving the issue.

Custom Properties follow the cascade and are computed at runtime, for one thing, which is the whole reason that they cannot be preprocessed ahead of time. But it’s more complex than that. What if the value is valid for a custom property (most anything is), but not valid for the way you are trying to use it?

This is a real head scratcher:

html { color: red; }

p { color: blue; }

.card { --color: #notacolor; }

.card p { color: var(--color); }

Turns out .card p will actually be red (I would have guessed blue), but Stephanie explains:

The .card p will be the inherited color value of red as provided by the body. It is unable to use the cascaded value of blue due to the browser discarding that as a possible value candidate at “parse time” when it is only evaluating syntax.

How do you accommodate people who struggle with transparent interfaces?

Adam Argyle explains it can be like this, using this media query you may or may not have heard of:

.example {
  --opacity: .5;

  background: hsl(200 100% 50% / var(--opacity));

  @media (prefers-reduced-transparency: reduce) {
    --opacity: .95;
  }
}

Adam had lots of practical examples in the post, and does consider that word reduced, and how it doesn’t mean absolutely none ever.

What units should you use for spacing properties?

Me, I just use rem usually as that’s what I use for nearly everything else. But Ashlee M Boyer argues that while stuff like text makes good sense to use relative units, spacing need not scale at that same rate:

When a user is customizing their viewing experience, they thing that’s most important to them and their task at hand is the content. Spacing isn’t often vital for a user to perform their task, so it doesn’t need to grow or scale at the same rate as the content itself.

When spacing between content grows, it eats up vital real estate and becomes harder to manage.

Ashlee proves it with a before video and an after video, after being moving relative units to absolute units for spacing.

How do you make every Sass file automatically include common imports?

This one hits home for me, as someone with a codebase with easily hundreds of Sass files that all start with something like @import "@codepen/variables"; Wouldn’t it be cool if Sass could just assume we wanted to do that for every file?

Austin Gil covered this a while back on doing it with Vite. When you define your Vite config, you can like:

  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/assets/_shared.scss";`
      }
    }
  }

I see webpack can do it too, but I’m not sure if Sass alone can be configured to do it, although I wish it could.

The Complex But Awesome CSS border-image Property

Category Image 052

The border-image property is nothing new. Even deprecated Internet Explorer supports it, so you know we’re treading well-charted territory. At the same time, it’s not exactly one of those properties you likely keep close at hand, and its confusing concepts of “slicing” and “outsets” don’t make it the easiest property to use.

I’m here to tell you that border-image is not only capable of producing some incredibly eye-catching UI designs but is also able to accomplish some other common patterns we often turn to other properties and approaches for.

In this article, my plan is to dispel the confusing Aspects of border-image without getting into a bunch of theoretical explanations and technical jargon. Instead, we’re going to have fun with the property, using it to create shapes and put a different spin on things like range sliders, tooltips, and title decorations.

By the end of this, I hope that border-image becomes your new favorite property just as it has become mine!

The Concept of Image Borders

There are a few specific Aspects of border-image that I think are crucial for understanding how it works.

It’s Easy To Accidentally Override A Border Image

The CSS Backgrounds and Border Module Level 3 specification says border-image should replace any regular border you define, but it’s not always the case. Try the code below, and all you will see is a red border.

/* All I see is a red border */
.element {
  border-image: linear-gradient(blue, red) 1;
  border: 5px solid red;
}

That’s because we’re technically declaring border after border-image. Order really does matter when working with border-image!

/* 👍 */
.element {
  border: 5px solid red;
  border-image: linear-gradient(blue, red) 1;
}

You can already see how this could be confusing for anyone jumping into border-image, especially for the first time. You will also notice that our gradient border has a thickness equal to 5px, which is the border-width we defined.

I make it a personal habit not to use border and border-image together because it helps me avoid overriding the border image I’m trying to create and be able to control the border decoration using only one property (even if both can be used together). So, if you get a strange result, don’t forget to check if you have a border declared somewhere.

It Is Painted Over Backgrounds And Box Shadows

The second tip I want to offer is that border-image is painted above the element’s background and box-shadow but below the element’s content. This detail is important for some of the tricks we will use in later examples. The following Pen demonstrates how a border image is applied in that order:

If we were to translate the figure above into code using the provided variables as values, it would look like this:

border-image:
  linear-gradient(...)
  s-top s-right s-bottom s-left / 
  w-top w-right w-bottom w-left /
  o-top o-right o-bottom o-left;

By default, border-image considers the boundaries of the element (illustrated with the blue dotted border in Figure 1) as its area to paint the gradient, but we can change this using the <outset> to increase that area and create an overflow. This is super useful to have “outside” decorations.

Then, the <width> is used to split the area into nine regions, and the <slice> is used to split the source (i.e., the gradient) into nine slices as well. From there, we assign each slice to its corresponding region. Each slice is stretched to fill its assigned region, and if they don’t share equal dimensions, the result is typically a distorted image slice. Later on, we will learn how to control that and prevent distortion.

The middle region is kept empty by default. That said, it is totally possible to use the fill keyword to do what it says and fill the middle region with slice nine (which is always the center slice).

border-image: linear-gradient(...) fill
  s-top s-right s-bottom s-left / 
  w-top w-right w-bottom w-left /
  o-top o-right o-bottom o-left;

I know this was a pretty fast primer on border-image, but I think it’s all we need to do some pretty awesome stuff. Let’s jump into the fun and start experimenting with effects.

Gradient Overlay

Our first experiment is to add a gradient overlay above an existing background. This is a fairly common pattern to improve the legibility of text by increasing the contrast between the text color and the background color.

There are several well-known approaches to setting an overlay between text and content. Here’s one from Chris Coyier back in 2013. And that isn’t even the most widely-used approach, which is likely using pseudo-elements.

But border-image gives us a one-line way to pull it off:

.overlay {
  border-image: fill 0 linear-gradient(#0003,#000); 
}

That’s all! No extra element, no pseudo-element, and no need to modify the background property.

Well, guess what? The border-image property can pull it off with one line of code:

.full-background {
  border-image: conic-gradient(pink 0 0) fill 0//0 100vw;
}

If you compare what we just did with the gradient overlay example, the <outset> is the only difference between the implementations. Other than that, we have a single slice placed in the middle region that covers the entire area we extended to the edge of the screen.

We are not limited to a solid color, of course, since we are working with gradients.

Fancy Headings

Another thing we can use border-image for is decorating headings with fancy borders. Let’s start with the exact same implementation we used for the full-width backgrounds. Only this time, we’re replacing the conic-gradient() with a linear-gradient():

.full-background {
  border-image: linear-gradient(0deg, #1095c1 5px, lightblue 0) fill 0//0 100vw;
}

Now we apply this to an <h1> element:

So, that’s two different ways to get the same effect using the same border-image syntax. We can actually get this a third way as well:

.full-line {
  border-image: conic-gradient(#1095c1 0 0) 0 0 1 0/0 0 8px 0/0 100vw 0 0;
}

This time, I have defined a bottom slice equal to 1 (unitless values are computed as pixels), which produces two slices, the seventh (bottom center) and the ninth (center). From there, I have set the seventh region to a height of 8px. Note that I am not using the fill keyword this time, so the middle region is not filled like it was last time. Instead, we only fill the seventh region that takes up 100% of the boder-image’s area and 8px of its height.

You’re wondering why I am defining a slice equal to 1, right? The goal is to have only two slices: the seventh (bottom center) and the ninth (middle), and since we are applying a solid color, the size doesn't matter. That’s why I used 1; a small positive value. Any value will work (e.g., 0.5, 2, 100, 50%, 76%, and so on); it’s just that 1 is shorter to type. Remember that the slice will get stretched within its region, so 1px is enough to fill the whole region.

Here’s the deal: The slice value doesn’t really matter when working with a solid coloration. In most cases, the value winds up being 0 (empty) or 1 (filled). You can think of it as binary logic.

We could do this a fourth way!

.full-line {
  border-image: conic-gradient(#1095c1 0 0) 0 1 0 0/calc(100% - 8px) 100% 0 0/0 100vw 0 0;
}

I’ll let you do the work to figure out how the above CSS works. It’s a good opportunity to get a feel for slicing elements. Take a pen and paper and try to identify which slices we are using and which regions will be filled.

One thing that makes border-image a complex property is all the different ways to achieve the same result. You can wind up with a lot of different combinations, and when all of them produce the same result, it’s tough to form a mental model for understanding how all of the values work together.

Even though there is no single “right” way to do these heading borders, I prefer the second syntax because it allows me to simply change one color value to establish a “real” gradient instead of a solid color.

.full-line {
  border-image: repeating-linear-gradient(...) fill 0 /
    calc(100% - var(--b)) 0 0/0 100vw 0 0 repeat;
}

Let’s try another syntax for the same effect:

h2 {
  --s: 3px;   /* the thickness */
  --w: 100px; /* the width */
  --g: 10px;  /* the gap */
  border-image: 
     conic-gradient(red 0 0) 
     0 50%/calc(50% - var(--s)/2) var(--w)/0 calc(var(--w) + var(--g));
}

The top and bottom values of the <slice> are equal to 0, and the left and right ones are equal to 50%. This means that slices six and eight share the gradient. All the other slices — including the center — are empty.

As far as the regions go, the top and bottom regions (consisting of regions 1, 5, and 2 at the top and regions 4, 7, and 3 at the bottom) have a height equal to 50% - var(--s)/2 leaving the --s variable as a height for the remaining regions (6, 8, and 9). The right and the left regions have a width equal to the --w variable. Since slices 6 and 8 are the only ones that are filled, the only regions we need to care about are 6 and 8. Both have a height equal to the border’s thickness, --s, and a width equal to --w.

I think you know how the rest of the story goes.

Notice I am using 50% as a slice. It demonstrated how any value does the job, as we discussed in the last section when I explained why I chose to use a value of 1 but also to prepare for the next effect where I will be using a real gradient:

See the Pen Horizontal lines around your title with gradient coloration by Temani Afif.

When it comes to real gradients, the value of the slice is important, and sometimes you need very precise values. To be honest, this can be very tricky, and I even get lost trying to figure out the right value.

Let’s end this section with more examples of title decorations. When combined with other properties, border-image can make really nice effects.

See the Pen Fancy title divider with one element by Temani Afif.

See the Pen Fancy title divider with one element by Temani Afif.

More Examples

Now that we’ve seen several detailed examples of how border-image, I’m going to drop in several other examples. Rather than explaining them in great detail, try to explain them in your own words by inspecting the CSS, and use these as inspiration for your own work.

Infinite Image Decorations

When it comes to images, border-image can be a lifesaver since we don’t have access to pseudo-elements. Here are some cool infinite decorations where we can have a touch of 3D effect.

See the Pen Infinite image shadow by Temani Afif.

See the Pen Infinite image shadow II by Temani Afif.

See the Pen Infinite image stripes shadow by Temani Afif.

See the Pen 3D trailing shadow for images by Temani Afif.

If you check the code in these examples, you will find they share nearly the same structure. If you have trouble recognizing the pattern, please don’t hesitate to leave a comment at the end of this article, and I would be happy to point it out.

Custom Range Slider

I wrote a detailed article on how to create the following example, and you can refer to it for range slider variations using the same technique.

See the Pen CSS only custom range sliders by Temani Afif.

I used border-image and styled only the “thumb” element. Range inputs are known to have different implementation cross-browser, but the “thumb” is common between all of them.

Ribbon Shapes

In case you missed it, I have created a collection of more than 100 single-element ribbon shapes, and some of them rely on border-image. I call them the “infinite ribbons.”

See the Pen Full screen Ribbon title by Temani Afif.

See the Pen Infinite Ribbon Shapes by Temani Afif.

Heart Shapes

I have written about CSS heart shapes using different approaches, and one of them uses a border-image technique.

.heart {
  width: 200px;
  aspect-ratio: 1;
  border-image: radial-gradient(red 69%,#0000 70%) 84.5%/50%;
  clip-path: polygon(-42% 0,50% 91%, 142% 0);
}

See the Pen Heart shape using border-image by Temani Afif.

The interesting part here is the slice that is equal to 84.5%. That is a bigger value than 50%, so it may seem incorrect since the total exceeds 100%. But it’s perfectly fine because slices are able to overlap one another!

When using values bigger than 50%, the corner slices (1, 2, 3, and 4) will share common parts, but the other slices are considered empty. Logically, when using a slice equal to 100%, we will end with four slices containing the full source.

Here is an example to illustrate the trick:

See the Pen Overview of the slice effect by Temani Afif.

The slider will update the slice from 0% to 100%. On the left, you can see how the corner slices (1-4) grow. Between 0% and 50%, the result is logical and intuitive. Bigger than 50%, you start having the overlap. When reaching 100%, you can see the full circle repeated four times because each slice contains the full gradient, thanks to the overlap.

It can be confusing and not easy to visualize, but overlaps can be really useful to create custom shapes and fancy decorations.

Tooltips

What about a simple tooltip shape with only two properties? Yes, it’s possible!

See the Pen A simple Tooltip using 2 CSS properties by Temani Afif.

.tooltip {
  /* triangle dimension */
  --b: 2em; /* base */
  --h: 1em; /* height*/

  border-image: conic-gradient(#CC333F 0 0) fill 0//var(--h);
  clip-path: 
    polygon(0 100%,0 0,100% 0,100% 100%,
      calc(50% + var(--b)/2) 100%,
      50% calc(100% + var(--h)),
      calc(50% - var(--b)/2) 100%);
}
Filling Border Radius

Unlike most decorative border properties (e.g., box-shadow, outline, border, and so on), border-image doesn’t respect border-radius. The element is still a box, even if we’ve rounded off the corners. Other properties will recognize the visual boundary established by border-radius, but border-image bleeds right through it.

That could be a drawback in some instances, I suppose, but it’s also one of the quirky things about CSS that can be leveraged for other uses like creating images with inner radius:

See the Pen Inner radius to image element by Temani Afif.

Cool, right? Only one line of code makes it happen:

img {
  --c: #A7DBD8;
  --s: 10px; /* the border thickness*/

  border-image: conic-gradient(var(--c) 0 0) fill 0 // var(--s);
}

We can even leave the center empty to get a variation that simply borders the entire element:

See the Pen Rounded images inside squares by Temani Afif.

Conclusion

Did you know border-image property was such a powerful — and flexible — CSS property? Despite the challenge it takes to understand the syntax, there are ways to keep the code clean and simple. Plus, there is often more than one “right” way to get the same result. It’s a complicated and robust CSS feature.

If the concepts of slicing and defining regions with border-image are still giving you problems, don’t worry. That’s super common. It took me a lot of time to fully understand how border-image works and how to use it with different approaches to the syntax. Give yourself plenty of time, too. It helps to re-read things like this article more than once to let the concepts sink in.

Complexities aside, I hope that you will add border-image to your toolbox and create a lot of magic with it. We can do even more with border-image than what was demonstrated here. I actually experiment with this sort of stuff frequently and share my work over at my CSS Tip website. Consider subscribing (RSS) to keep up with the fun and weird things I try.

Special thanks to @SelenIT2, who pushed me to explore this property and wrote an excellent article on it.

A Few Interesting Ways To Use CSS Shadows For More Than Depth

Category Image 052

The world of post-modern web design is one where the light doesn’t cast many shadows. That doesn’t mean CSS shadows are going away. On the contrary, they’ve become more adaptive. Shadows are an incredibly useful design element. We know they add depth to an otherwise two-dimensional web design, but did you know we can stack, animate, and manipulate them in ways that go beyond that?

I’ve been experimenting with shadows. In this article, I’m going to share several “tricks” I’ve discovered along the way and how they can be used to create interesting effects that have little to do with their primary role of adding depth. We’ll look at an effect that works by stacking layers of shadows that transition on hover. After that, I will show you how to make a shadow of a shadow. Lastly, we’ll play with shadows on text as an alternative to color.

Ready for some fun? Let’s start with an interesting hover effect.

The Introspective Shadow Hover Effect

Most of us are familiar with the inset keyword. It’s an optional value of the CSS box-shadow property.

When inset is specified, the shadow is cast inside the element, directed inward. It’s commonly used to make it look as if an element has been stamped into the surface of the web page. We are going to push that shadow further, both metaphorically and literally, to create an overlay hover effect for image transitions.

Just as we can control the shadow’s blur radius — how far the shadow spreads outward — we can choose to apply no blur at all to the shadow. We can combine that with the fact that inset shadows are painted over an element’s background (unlike default shadows that are cast beneath the element) to create what I call a “veil” that sits on top of an element.

Let’s start with a single div in the HTML:

<div class="item"></div>

There’s nothing to see yet. So, let’s add some dimensions, a background color, and a border radius to make a green circle.

.item {
  width: 250px;
  height: 250px;
  background: green;
  border-radius: 50%;
}

This is nothing fancy so far. I merely want to demonstrate that we can essentially cover the green background with a red inset box-shadow:

.item {
  width: 250px;
  height: 250px;
  background: green;
  border-radius: 50%;
  box-shadow: inset 250px 250px 0 red;
}

Now we have a red circle with a green background beneath it. We can remove the red inset shadow on hover to reveal the green background:

.item:hover {
  box-shadow: none;
}

See the Pen Inward Shadow Pt. 1 [forked] by Preethi Sam.

Since shadows can be layered and are supported by CSS transitions, let’s incorporate that for a more fluid design. First, I’m going to update the HTML a bit by adding a span inside the .item:

    <div class="item">
      <span>The New York Times</span>
    </div>
    <!-- more items -->

For the CSS, it’s the same idea as before. We want a circle with an inset shadow and a background:

.item {
  width: 300px;
  height: 300px;
  background-image: url('nytimes.svg');
  border-radius: 50%;
  box-shadow: inset -300px -300px 0 black,
}

The difference so far is that I am using a background-image instead of a background-color. They are absolutely interchangeable for the hover effect we’re working on.

Next, I’m going to do two things. First, I’m going to stack more inset shadows inside the .item. Then I’m changing the text color to white, but only for a moment so the background image shows all the way through.

.item {
  width: 300px;
  height: 300px;
  background-image: url('nytimes.svg');
  border-radius: 50%;
  box-shadow:
    inset -300px -300px 0 black,
    inset 300px -300px 0 green,
    inset -300px 300px 0 blue,
    inset 300px 300px 0 yellow,
    0 0 20px silver; /* standard outset shadow */
  color: white;
  }

Even after we add those four extra shadows, we still are left with only a black circle that says “The New York Times” on it in white. The trick is to remove those shadows on hover, change the color of the text to transparent, and reveal the logo beneath our stack of inset shadows.

.item:hover {
  box-shadow:
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    0 0 20px silver; /* retain the outset shadow */
  color: transparent;
}

That works! But perhaps we should add a little transition in there to smooth it out:

.item {
  width: 300px;
  height: 300px;
  background-image: url('nytimes.svg');
  border-radius: 50%;
  box-shadow:
    inset -300px -300px 0 black,
    inset 300px -300px 0 green,
    inset -300px 300px 0 blue,
    inset 300px 300px 0 yellow,
    0 0 20px silver; /* standard outset shadow */
  color: white;
  transition:
    box-shadow ease-in-out .6s,
    color ease-in-out .5s;
  }

.item:hover {
  box-shadow:
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    0 0 20px silver; /* keeping the outset shadow */
  color: transparent;
}

The only other thing I think that’s worth calling out is that the outward shadow in the stack is not removed when the .item is hovered. I only want to remove the inset shadows.

Here’s the final result:

See the Pen Inward Shadow Pt. 2 [forked] by Preethi Sam.

I used CSS variables throughout so you can change the colors of the shadows and the size of the element.

Casting A Shadow Of A Shadow

If we learned anything from that last example, it’s that shadows are visually interesting: they can bend, fade, intersect, and transition. But what about a shadow casting another shadow? Can we create a shadow of an element’s shadow?

This is not the same as stacking layers of shadows as we did earlier. Rather, we will be making a silhouette of a shadow. And because we have a second way to add shadows to elements with the CSS drop-shadow() filter, we can do exactly that.

A drop-shadow() is a little different than a box-shadow. Where a box-shadow casts a shadow along the physical edges of the element’s bounding box, a drop-shadow() ignores the box and casts a shadow along the element’s shape.

When drop-shadow() is given to an element with a box-shadow, the shadow from the box-shadow will cast a shadow of its own. We can combine these to make interesting effects, like a Venn diagram shape.

.item {
  box-shadow: 0 0 20px black ;
  filter: drop-shadow(-30px 0 0 blue);
}

See the Pen Shadow of a Shadow Pt. 1 [forked] by Preethi Sam.

This simple combination of box and drop shadows can lead to interesting designs, like shadows that cast shadows. Let’s start with some HTML that includes the same .item element we used in the last section. This time, we’ll place two child elements inside it, another div and an img:

<div class="item">
  <div class="background"></div>
  <img src="image.jpeg" />
</div>

<!-- more items -->

The .item is merely serving as a container this time. The real work happens on the .background child element. The image is purely there for decoration. We’re going to set a box-shadow on the .background element, then add a stack of three drop-shadow() layers to it:

/* third circle in the following demo */
.item > .background {
    box-shadow: 0 0 40px rgb(255 0 0 / .5);
    filter:
      drop-shadow(-20px 0 0 rgb(255 0 0 / .5))
      drop-shadow(20px 0 0 rgb(255 0 0 / .5))
      drop-shadow(20px 0 0 rgb(255 0 0 / .5));
}

We can also use transitions with these effects (as in the middle circle below).

See the Pen Shadow of a Shadow Pt. 2 [forked] by Preethi Sam.

The Textual Shadow

The last effect we’re going to look at involves the CSS text-shadow property. It’s actually less of a complicated “trick” than it is a demonstration of using and showing just the shadow of a text element for color purposes.

Specifically, I’m talking about transparent text with a shadow on it:

/* second column in the below demo */
p {
  color: transparent;
  text-shadow: 1px 1px 0 black;
}

See the Pen Textual Shadow Pt. 2 [forked] by Preethi Sam.

Notice the emoji? Instead of the full-color deal, we normally get, this emoji is more like an icon filled with a solid color. This is one way to make a quick and dirty icon system without drawing them or working with files.

We could have also pulled this off with background-clip: text to clip around the shape of the emoji or apply a drop-shadow(). However, that affects the background, limiting where it can be used. Plus, I like the idea of using text-shadow with text elements since that’s what it’s used for, and emoji are part of the text.

You might think there’s a “gotcha” with underlines. For example, text-shadow ignores the text decoration of links.

See the Pen Text Shadow No Likey Link Underlines [forked] by Geoff Graham.

No big deal. If you need to support underlines, we can reach for the CSS text-decoration and text-underline-offset properties:

p {
  color: transparent;
  text-shadow: 1px 1px 0 black;
  text-decoration-line: underline;
  text-decoration color: black;
  text-underline-offset: 3px;
}

See the Pen Shadow-Only Link With Underline [forked] by Geoff Graham.

Conclusion

That’s a look at three interesting ways to use CSS shadows as more than that thing you use to add depth. We looked at one way that uses inset shadows to hide the contents of an element’s background for a neat hover effect. Then there was the idea of combining box-shadow and drop-shadow() to cast a shadow of another shadow. We capped things off with a quick way to manipulate text and emoji with text-shadow.

I hope these experiments give you the inspiration to do some CSS shadow experiments of your own. Shadows and gradients are perhaps the two most important CSS features for “drawing” with CSS, like many of the examples you’ll see on Lynn Fisher’s A Single Div project. Shadows have incredible browser support, so the options are plentiful as far as what we can do with them.

Further Reading On SmashingMag