Web Standards

Alvaro Montoro: CSS One-Liners to Improve (Almost) Every Project

Css Tricks - Mon, 07/22/2024 - 4:38am

These sorts of roundups always get me. My wife will flip through Zillow photos of the insides of homes for hours because she likes seeing how different people decorate, Feng Shui, or what have you. That’s her little dip into Voyeur-Land. Mine? It could easily be scrolling through CSS snippets that devs keep within arm’s reach.

Alvaro was kind enough to share the trustiest of his trusty CSS:

  1. Limit the content width within the viewport
  2. Increase the body text size
  3. Increase the line between rows of text
  4. Limit the width of images
  5. Limit the width of text within the content
  6. Wrap headings in a more balanced way
  7. Form control colors to match page styles
  8. Easy-to-follow table rows
  9. Spacing in table cells and headings
  10. Reduce animations and movement

Not dropping the snippets in here (it’s worth reading the full post for that). But I do have a couple of my own that I’d tack on. And like Alvaro says up-front about his list, not all of these will be 100% applicable to every project.

Global border-box sizing

No explanation needed here. It’s often the very first thing declared in any given stylesheet on the web.

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

I’m guessing Alvaro uses this, too, and maybe it’s too obvious to list. Or maybe it’s more of a DX enhancement that belongs in a reset more than it is something that improves the website.

System fonts

Default text on the web is just so… so… so blah. I love that Alvaro agrees that 16px is way too small to be the web’s default font-size for text. I would take that one step further and wipe out the Times New Roman default font as well. I’m sure there are sites out there leveraging it (I did on my own personal site for years as an act of brutal minimalism), but a personal preference these days is defaulting to whatever the OS default font is.

body { font-family: system-ui; }

We can be a little more opinionated than that by falling back to either a default serif or sans-serif font.

body { font-family: system-ui, sans-serif; }

There are much, much more robust approaches for sure, but this baseline is a nice starting point for just about any site.

Cut horizontal overflow from the <body>

Oh gosh, I never ever make this mistake. &#x1f61d;

But hypothetically, if I did — and that’s a BIG if — I like preventing it from messing with a visitor’s scrolling experience. Once the <body>‘s intrinsic width is forced outside the viewport, we get horizontal scrolling that might be a very cool thing if it’s intentional but is not-so-bueno when it’s not.

body { overflow-x: hidden; }

I’ll use this as a defensive mechanism but would never want to rely on it as an actual solution to the possible loss of data that comes with overflowing content. This merely masks the problem while allowing an opportunity to fix the root cause without visitors having to deal with the rendered consequences.

Give the <body> some breathing room

Not too much, not too little, but the baby bear porridge just the right amount of space to keep content from hugging right up to the edges.

body { padding-block: 15px; }

To Shared LinkPermalink on CSS-Tricks

Alvaro Montoro: CSS One-Liners to Improve (Almost) Every Project originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Sara Joy: Everybody’s Free (To Write Websites)

Css Tricks - Wed, 07/17/2024 - 8:36am

Sara Joy’s adaptation of the song “Everybody’s Free (To Wear Sunscreen)” (YouTube) originally by Baz Luhrman with lyrics pulled directly from Mary Schmich‘s classic essay, “Wear Sunscreen”. Anyone who has graduated high school since 1999 doesn’t even have to look up the song since it’s become an unofficial-official commencement ceremony staple. If you graduated in ’99, then I’m sorry. You might still be receiving ongoing treatment for the earworm infection from that catchy tune spinning endlessly on radio (yes, radio). Then again, those of us from those late-90’s classes came down with more serious earworm cases from the “I Will Remember You” and “Time of Your Life” outbreaks.

Some choice pieces of Sara’s “web version”:

Don’t feel guilty if you don’t know what you want to do with your site. The most interesting websites don’t even have an introduction, never mind any blog posts. Some of the most interesting web sites I enjoy just are.

Add plenty of semantic HTML.

Clever play on words and selectors:

Enjoy your <body>. Style it every way you can. Don’t be afraid of CSS, or what other people think of it. It’s the greatest design tool you’ll ever learn.

The time’s they are a-changin’:

Accept certain inalienable truths: connection speeds will rise, techbros will grift, you too will get old— and when you do, you’ll fantasize that when you were young websites were light-weight, tech founders were noble and fonts used to be bigger.

And, of course:

Respect the W3C.

Oh, and remember: Just build websites.

To Shared LinkPermalink on CSS-Tricks

Sara Joy: Everybody’s Free (To Write Websites) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS Selectors

Css Tricks - Mon, 07/15/2024 - 6:13am

CSS is really good at many things, but it’s really, really good at two specific things: selecting elements and styling them. That’s the raison d’être for CSS and why it’s a core web language. In this guide, we will cover the different ways to select elements — because the styles we write are pretty much useless without the ability to select which elements to apply them to.

The source of truth for CSS selectors is documented in the Selectors Module Level 4 specification. With one exception (which we’ll get to), all of the selectors covered here are well-covered by browsers across the board, and most certainly by all modern browsers.

In addition to selectors, this guide also looks at CSS combinators. If selectors identify what we are selecting, you might think of combinators as how the styles are applied. Combinators are like additional instructions we give CSS to select a very particular element on the page, not totally unlike the way we can use filters in search engines to find the exact result we want.

Quick reference Common Selectors /* Universal */ * { box-sizing: border-box; } /* Type or Tag */ p { margin-block: 1.5rem; } /* Classname */ .class { text-decoration: underline; } /* ID */ #id { font-family: monospace; } /* Relational */ li:has(a) { display: flex; } Common Combinators /* Descendant */ header h1 { /* Selects all Heading 1 elements in a Header element. */ } /* Child */ header > h1 { /* Selects all Heading 1 elements that are children of Header elements. */ } /* General sibling */ h1 ~ p { /* Selects a Paragraph as long as it follows a Heading 1. */ } /* Adjacent sibling */ h1 + p { /* Selects a Paragraph if it immediately follows a Heading 1 */ } /* Chained */ h1, p { /* Selects both elements. */ } General Selectors

When we talk about CSS selectors, we’re talking about the first part of a CSS ruleset:

/* CSS Ruleset */ selector { /* Style rule */ property: value; }

See that selector? That can be as simple as the HTML tag we want to select. For example, let’s select all <article> elements on a given page.

/* Select all <article> elements... */ article { /* ... and apply this background-color on them */ background-color: hsl(25 100% 50%); }

That’s the general process of selecting elements to apply styles to them. Selecting an element by its HTML tag is merely one selector type of several. Let’s see what those are in the following section.

Element selectors

Element selectors are exactly the type of selector we looked at in that last example: Select the element’s HTML tag and start styling!

That’s great and all, but consider this: Do you actually want to select all of the <article> elements on the page? That’s what we’re doing when we select an element by its tag — any and all HTML elements matching that tag get the styles. The following demo selects all <article> elements on the page, then applies a white (#fff) background to them. Notice how all three articles get the white background even though we only wrote one selector.

CodePen Embed Fallback

I’ve tried to make it so the relevant for code for this and other demos in this guide is provided at the top of the CSS tab. Anything in a @layer can be ignored. And if you’re new to @layer, you can learn all about it in our CSS Cascade Layers guide.

But maybe what we actually want is for the first element to have a different background — maybe it’s a featured piece of content and we need to make it stand out from the other articles. That requires us to be more specific in the type of selector we use to apply the styles.

Let’s turn our attention to other selector types that allow us to be more specific about what we’re selecting.

ID selectors

ID selectors are one way we can select one element without selecting another of the same element type. Let’s say we were to update the HTML in our <article> example so that the first article is “tagged” with an ID:

<article id="featured"> <!-- Article 1 --> </article> <article> <!-- Article 2 --> </article> <article> <!-- Article 3 --> </article>

Now we can use that ID to differentiate that first article from the others and apply styles specifically to it. We prepend a hashtag character (#) to the ID name when writing our CSS selector to properly select it.

/* Selects all <article> elements */ article { background: #fff; } /* Selects any element with id="featured" */ #featured { background: hsl(35 100% 90%); border-color: hsl(35 100% 50%); }

There we go, that makes the first article pop a little more than the others!

CodePen Embed Fallback

Before you go running out and adding IDs all over your HTML, be aware that IDs are considered a heavy-handed approach to selecting. IDs are so specific, that it is tough to override them with other styles in your CSS. IDs have so much specificity power than any selector trying to override it needs at least an ID as well. Once you’ve reached near the top of the ladder of this specificity war, it tends to lead to using !important rules and such that are in turn nearly impossible to override.

Let’s rearrange our CSS from that last example to see that in action:

/* Selects any element with id="featured" */ #featured { background: hsl(35 100% 90%); border-color: hsl(35 100% 50%); } /* Selects all <article> elements */ article { background: #fff; }

The ID selector now comes before the element selector. According to how the CSS Cascade determines styles, you might expect that the article elements all get a white background since that ruleset comes after the ID selector ruleset. But that’s not what happens.

CodePen Embed Fallback

So, you see how IDs might be a little too “specific” when it comes to selecting elements because it affects the order in which the CSS Cascade applies styles and that makes styles more difficult to manage and maintain.

The other reason to avoid IDs as selectors? We’re technically only allowed to use an ID once on a page, per ID. In other words, we can have one element with #featured but not two. That severely limits what we’re able to style if we need to extend those styles to other elements — not even getting into the difficulty of overriding the ID’s styles.

A better use case for IDs is for selecting items in JavaScript — not only does that prevent the sort of style conflict we saw above, but it helps maintain a separation of concerns between what we select in CSS for styling versus what we select in JavaScript for interaction.

Another thing about ID selectors: The ID establishes what we call an “anchor” which is a fancy term for saying we can link directly to an element on the page. For example, if we have an article with an ID assigned to it:

<article id="featured">...</article>

…then we can create a link to it like this:

<a href="featured">Jump to article below ⬇️</a> <!-- muuuuuuch further down the page. --> <article id="featured">...</article>

Clicking the link will navigate you to the element as though the link is anchored to that element. Try doing exactly that in the following demo:

CodePen Embed Fallback

This little HTML goodie opens up some pretty darn interesting possibilities when we sprinkle in a little CSS. Here are a few articles to explore those possibilities.

Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Class selectors

Class selectors might be the most commonly used type of CSS selector you will see around the web. Classes are ideal because they are slightly more specific than element selectors but without the heavy-handedness of IDs. You can read a deep explanation of how the CSS Cascade determines specificity, but the following is an abbreviated illustration focusing specifically (get it?!) on the selector types we’ve looked at so far.

That’s what makes class selectors so popular — they’re only slightly more specific than elements, but keep specificity low enough to be manageable if we need to override the styles in one ruleset with styles in another.

The only difference when writing a class is that we prepend a period (.) in front of the class name instead of the hashtag (#).

/* Selects all <article> elements */ article { background: #fff; } /* Selects any element with class="featured" */ .featured { background: hsl(35 100% 90%); border-color: hsl(35 100% 50%); }

Here’s how our <article> example shapes up when we swap out #featured with .featured.

CodePen Embed Fallback

Same result, better specificity. And, yes, we can absolutely combine different selector types on the same element:

<article id="someID" class="featured">...</article>

Do you see all of the possibilities we have to select an <article>? We can select it by:

  • Its element type (article)
  • Its ID (#someID)
  • Its class (.featured)

The following articles will give you some clever ideas for using class selectors in CSS.

Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier

But we have even more ways to select elements like this, so let’s continue.

Attribute selectors

ID and class selectors technically fall into this attribute selectors category. We call them “attributes” because they are present in the HTML and give more context about the element. All of the following are attributes in HTML:

<!-- ID, Class, Data Attribute --> <article id="#id" class=".class" data-attribute="attribute"> </article> <!-- href, Title, Target --> <a href="https://css-tricks.com" title="Visit CSS-Tricks" target="_blank"></a> <!-- src, Width, Height, Loading --> <img src="star.svg" width="250" height="250" loading="laxy" > <!-- Type, ID, Name, Checked --> <input type="checkbox" id="consent" name="consent" checked /> <!-- Class, Role, Aria Label --> <div class="buttons" role="tablist" aria-label="Tab Buttons">

Anything with an equals sign (=) followed by a value in that example code is an attribute. So, we can technically style all links with an href attribute equal to https://css-tricks.com:

a[href="https://css-tricks.com"] { color: orangered; }

Notice the syntax? We’re using square brackets ([]) to select an attribute instead of a period or hashtag as we do with classes and IDs, respectively.

CodePen Embed Fallback

The equals sign used in attributes suggests that there’s more we can do to select elements besides matching something that’s exactly equal to the value. That is indeed the case. For example, we can make sure that the matching selector is capitalized or not. A good use for that could be selecting elements with the href attribute as long as they do not contain uppercase letters:

/* Case sensitive */ a[href*='css-tricks' s] {}

The s in there tells CSS that we only want to select a link with an href attribute that does not contain uppercase letters.

<!-- &#x1f44e; No match --> <a href="https://CSS-Tricks.com">...</a> <!-- &#x1f44d; Match! --> <a href="https://css-tricks.com">...</a>

If case sensitivity isn’t a big deal, we can tell CSS that as well:

/* Case insensitive */ a[href*='css-tricks' i] {}

Now, either one of the link examples will match regardless of there being upper- or lowercase letters in the href attribute.

<!-- &#x1f44d; I match! --> <a href="https://CSS-Tricks.com">...</a> <!-- &#x1f44d; I match too! --> <a href="https://css-tricks.com">...</a>

There are many, many different types of HTML attributes. Be sure to check out our Data Attributes guide for a complete rundown of not only [data-attribute] but how they relate to other attributes and how to style them with CSS.

Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Jakob E Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Universal selector

CSS-Tricks has a special relationship with the Universal Selector — it’s our logo!

That’s right, the asterisk symbol (*) is a selector all unto itself whose purpose is to select all the things. Quite literally, we can select everything on a page — every single element — with that one little asterisk. Note I said every single element, so this won’t pick up things like IDs, classes, or even pseudo-elements. It’s the element selector for selecting all elements.

/* Select ALL THE THINGS! &#x1f4a5; */ * { /* Styles */ }

Or, we can use it with another selector type to select everything inside a specific element.

/* Select everything in an <article> */ article * { /* Styles */ }

That is a handy way to select everything in an <article>, even in the future if you decide to add other elements inside that element to the HTML. The times you’ll see the Universal Selector used most is to set border-sizing on all elements across the board, including all elements and pseudo-elements.

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

There’s a good reason this snippet of CSS winds up in so many stylesheets, which you can read all about in the following articles.

Article on Jul 22, 2024 CSS Selectors Sara Cope Article on Jul 22, 2024 CSS Selectors Marie Mosley Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier

Sometimes the Universal Selector is implied. For example, when using a pseudo selector at the start of a new selector. These are selecting exactly the same:

*:has(article) { } :has(article) { } Pseudo-selectors

Pseudo-selectors are for selecting pseudo-elements, just as element selectors are for selecting elements. And a pseudo-element is just like an element, but it doesn’t actually show up in the HTML. If pseudo-elements are new to you, we have a quick explainer you can reference.

Every element has a ::before and ::after pseudo-element attached to it even though we can’t see it in the HTML.

<div class="container"> <!-- ::before psuedo-element here --> <div>Item</div> <div>Item</div> <div>Item</div> <!-- ::after psuedo-element here --> </div>

These are super handy because they’re additional ways we can hook into an element an apply additional styles without adding more markup to the HTML. Keep things as clean as possible, right?!

We know that ::before and ::after are pseudo-elements because they are preceded by a pair of colons (::). That’s how we select them, too!

.container::before { /* Styles */ }

The ::before and ::after pseudo-elements can also be written with a single colon — i.e., :before and :after — but it’s still more common to see a double colon because it helps distinguish pseudo-elements from pseudo-classes.

But there’s a catch when using pseudo-selectors: they require the content property. That’s because pseudos aren’t “real” elements but ones that do not exist as far as HTML is concerned. That means they need content that can be displayed… even if it’s empty content:

.container::before { content: ""; }

Of course, if we were to supply words in the content property, those would be displayed on the page.

CodePen Embed Fallback Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Habdul Hazeez Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Geoff Graham Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Robin Rendle Article on Jul 22, 2024 CSS Selectors Dan Wilson Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Complex selectors

Complex selectors may need a little marketing help because “complex” is an awfully scary term to come across when you’re in the beginning stages of learning this stuff. While selectors can indeed become complex and messy, the general idea is super straightforward: we can combine multiple selectors in the same ruleset.

Let’s look at three different routes we have for writing these “not-so-complex” complex selectors.

Listing selectors

First off, it’s possible to combine selectors so that they share the same set of styles. All we do is separate each selector with a comma.

.selector-1, .selector-2, .selector-3 { /* We share these styles! &#x1f917; */ }

You’ll see this often when styling headings — which tend to share the same general styling except, perhaps, for font-size.

h1, h2, h3, h4, h5, h6 { color: hsl(25 80% 15%); font-family: "Poppins", system-ui; }

Adding a line break between selectors can make things more legible. You can probably imagine how complex and messy this might get. Here’s one, for example:

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

Ummmm, okay. No one wants this in their stylesheet. It’s tough to tell what exactly is being selected, right?

The good news is that we have modern ways of combining these selectors more efficiently, such as the :is() pseudo selector. In this example, notice that we’re technically selecting all of the same elements. If we were to take out the four section, article, aside, and nav element selectors and left the descendants in place, we’d have this:

h1, h2, h3, h4, h5, h6, h1, h2, h3, h4, h5, h6, h1, h2, h3, h4, h5, h6, h1, h2, h3, h4, h5, h6, { color: #BADA55; }

The only difference is which element those headings are scoped to. This is where :is() comes in handy because we can match those four elements like this:

:is(section, article, aside, nav) { color: #BADA55; }

That will apply color to the elements themselves, but what we want is to apply it to the headings. Instead of listing those out for each heading, we can reach for :is() again to select them in one fell swoop:

/* Matches any of the following headings scoped to any of the following elements. */ :is(section, article, aside, nav) :is(h1, h2, h3, h4, h5, h6) { color: #BADA55; }

While we’re talking about :is() it’s worth noting that we have the :where() pseudo selector as well and that it does the exact same thing as :is(). The difference? The specificity of :is() will equal the specificity of the most specific element in the list. Meanwhile, :where() maintains zero specificity. So, if you want a complex selector like this that’s easier to override, go with :where() instead.

Article on Jul 22, 2024 CSS Selectors Geoff Graham Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Chris Coyier Article on Jul 22, 2024 CSS Selectors Šime Vidas Article on Jul 22, 2024 CSS Selectors Mojtaba Seyedi

Nesting selectors

That last example showing how :is() can be used to write more efficient complex selectors is good, but we can do even better now that CSS nesting is a widely supported feature.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

DesktopChromeFirefoxIEEdgeSafari120117No12017.2Mobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS Safari12612712617.2

CSS nesting allows us to better see the relationship between selectors. You know how we can clearly see the relationship between elements in HTML when we indent descendant elements?

<!-- Parent --> <article> <!-- Child --> <img src="" alt="..."> <!-- Child --> <div class="article-content"> <!-- Grandchild --> <h2>Title</h2> <!-- Grandchild --> <p>Article content.</p> </div> </article>

CSS nesting is a similar way that we can format CSS rulesets. We start with a parent ruleset and then embed descendant rulesets inside. So, if we were to select the <h2> element in that last HTML example, we might write a descendant selector like this:

article h2 { /* Styles */ }

With nesting:

article { /* Article styles */ h2 { /* Heading 2 styles */ } }

You probably noticed that we can technically go one level deeper since the heading is contained in another .article-content element:

article { /* Article styles */ .article-content { /* Container styles */ h2 { /* Heading 2 styles */ } } }

So, all said and done, selecting the heading with nesting is the equivalent of writing a descendant selector in a flat structure:

article .article-content h2 { /* Heading 2 styles */ }

You might be wondering how the heck it’s possible to write a chained selector in a nesting format. I mean, we could easily nest a chained selector inside another selector:

article { /* Article styles */ h2.article-content { /* Heading 2 styles */ } }

But it’s not like we can re-declare the article element selector as a nested selector:

article { /* Article styles */ /* Nope! &#x1f44e; */ article.article-element { /* Container styles */ /* Nope! &#x1f44e; */ h2.article-content { /* Heading 2 styles */ } } }

Even if we could do that, it sort of defeats the purpose of a neatly organized nest that shows the relationships between selectors. Instead, we can use the ampersand (&) symbol to represent the selector that we’re nesting into. We call this the nesting selector.

article { &.article-content { /* Equates to: article.article-content */ } } Compounding selectors

We’ve talked quite a bit about the Cascade and how it determines which styles to apply to matching selectors using a specificity score. We saw earlier how an element selector is less specific than a class selector, which is less specific than an ID selector, and so on.

article { /* Specificity: 0, 0, 1 */ } .featured { /* Specificity: 0, 1, 0 */ } #featured { /* Specificity: 1, 0, 0 */ }

Well, we can increase specificity by chaining — or “compounding” — selectors together. This way, we give our selector a higher priority when it comes to evaluating two or more matching styles. Again, overriding ID selectors is incredibly difficult so we’ll work with the element and class selectors to illustrate chained selectors.

We can chain our article element selector with our .featured class selector to generate a higher specificity score.

article { /* Specificity: 0, 0, 1 */ } .featured { /* Specificity: 0, 1, 0 */ } articie.featured { /* Specificity: 0, 1, 1 */ }

This new compound selector is more specific (and powerful!) than the other two individual selectors. Notice in the following demo how the compound selector comes before the two individual selectors in the CSS yet still beats them when the Cascade evaluates their specificity scores.

CodePen Embed Fallback

Interestingly, we can use “fake” classes in chained selectors as a strategy for managing specificity. Take this real-life example:

.wp-block-theme-button .button:not(.specificity):not(.extra-specificity) { }

Whoa, right? There’s a lot going on there. But the idea is this: the .specificity and .extra-specificity class selectors are only there to bump up the specificity of the .wp-block-theme .button descendant selector. Let’s compare the specificity score with and without those artificial classes (that are :not() included in the match).

.wp-block-theme-button .button { /* Specificity: 0, 2, 0 */ } .wp-block-theme-button .button:not(.specificity) { /* Specificity: 0, 3, 0 */ } .wp-block-theme-button .button:not(.specificity):not(.extra-specificity { /* Specificity: 0, 4, 0 */ }

Interesting! I’m not sure if I would use this in my own CSS but it is a less heavy-handed approach than resorting to the !important keyword, which is just as tough to override as an ID selector.


If selectors are “what” we select in CSS, then you might think of CSS combinators as “how” we select them. they’re used to write selectors that combine other selectors in order to target elements. Inception!

The name “combinator” is excellent because it accurately conveys the many different ways we’re able to combine selectors. Why would we need to combine selectors? As we discussed earlier with Chained Selectors, there are two common situations where we’d want to do that:

  • When we want to increase the specificity of what is selected.
  • When we want to select an element based on a condition.

Let’s go over the many types of combinators that are available in CSS to account for those two situations in addition to chained selectors.

Descendant combinator

We call it a “descendant” combinator because we use it to select elements inside other elements, sorta like this:

/* Selects all elements in .parent with .child class */ .parent .child {}

…which would select all of the elements with the .child class in the following HTML example:

<div class="parent"> <div class="child"></div> <div class="child"></div> <div class="friend"></div> <div class="child"></div> <div class="child"></div> </div>

See that element with the .friend classname? That’s the only element inside of the .parent element that is not selected with the .parent .child {} descendant combinator since it does not match .child even though it is also a descendant of the .parent element.

Child combinator

A child combinator is really just an offshoot of the descendant combinator, only it is more specific than the descendant combinator because it only selects direct children of an element, rather than any descendant.

Let’s revise the last HTML example we looked at by introducing a descendant element that goes deeper into the family tree, like a .grandchild:

<div class="parent"> <div class="child"></div> <div class="child"> <div class="grandchild"></div> </div> <div class="child"></div> <div class="child"></div> </div>

So, what we have is a .parent to four .child elements, one of which contains a .grandchild element inside of it.

Maybe we want to select the .child element without inadvertently selecting the second .child element’s .grandchild. That’s what a child combinator can do. All of the following child combinators would accomplish the same thing:

/* Select only the "direct" children of .parent */ .parent > .child {} .parent > div {} .parent > * {}

See how we’re combining different selector types to make a selection? We’re combinating, dangit! We’re just doing it in slightly different ways based on the type of child selector we’re combining.

/* Select only the "direct" children of .parent */ .parent > #child { /* direct child with #child ID */ .parent > .child { /* direct child with .child class */ } .parent > div { /* direct child div elements */ } .parent > * { /* all direct child elements */ }

It’s pretty darn neat that we not only have a way to select only the direct children of an element, but be more or less specific about it based on the type of selector. For example, the ID selector is more specific than the class selector, which is more specific than the element selector, and so on.

General sibling combinator

If two elements share the same parent element, that makes them siblings like brother and sister. We saw an example of this in passing when discussing the descendant combinator. Let’s revise the class names from that example to make the sibling relationship a little clearer:

<div class="parent"> <div class="brother"></div> <div class="sister"></div> </div>

This is how we can select the .sister element as long as it is preceded by a sibling with class .brother.

/* Select .sister only if follows .brother */ .brother ~ .sister { }

The Tilda symbol (~) is what tells us this is a sibling combinator.

It doesn’t matter if a .sister comes immediately after a .brother or not — as long as a .sister comes after a brother and they share the same parent element, it will be selected. Let’s see a more complicated HTML example:

<main class="parent"> <!-- .sister immediately after .brother --> <div class="brother"></div> <div class="sister"></div> <!-- .sister immediately after .brother --> <div class="brother"></div> <div class="sister"></div> <!-- .sister immediately after .sister --> <div class="sister"></div> <!-- .cousin immediately after .brother --> <div class="brother"></div> <div class="cousin"> <!-- .sister contained in a .cousin --> <div class="sister"></div> </div> </main>

The sibling combinator we wrote only selects the first three .sister elements because they are the only ones that come after a .brother element and share the same parent — even in the case of the third .sister which comes after another sister! The fourth .sister is contained inside of a .cousin, which prevents it from matching the selector.

Let’s see this in context. So, we can select all of the elements with an element selector since each element in the HTML is a div:

CodePen Embed Fallback

From there, we can select just the brothers with a class selector to give them a different background color:

CodePen Embed Fallback

We can also use a class selector to set a different background color on all of the elements with a .sister class:

CodePen Embed Fallback

And, finally, we can use a general sibling combinator to select only sisters that are directly after a brother.

CodePen Embed Fallback

Did you notice how the last .sister element’s background color remained green while the others became purple? That’s because it’s the only .sister in the bunch that does not share the same .parent as a .brother element.

Adjacent combinator

Believe it or not, we can get even more specific about what elements we select with an adjacent combinator. The general sibling selector we just looked at will select all of the .sister elements on the page as long as it shares the same parent as .brother and comes after the .brother.

What makes an adjacent combinator different is that it selects any element immediately following another. Remember how the last .sister didn’t match because it is contained in a different parent element (i.e., .cousin)? Well, we can indeed select it by itself using an adjacent combinator:

/* Select .sister only if directly follows .brother */ .brother + .sister { }

Notice what happens when we add that to our last example:

CodePen Embed Fallback

The first two .sister elements changed color! That’s because they are the only sisters that come immediately after a .brother. The third .sister comes immediately after another .sister and the fourth one is contained in a .cousin which prevents both of them from matching the selection.

Learn more about CSS selectors Table of contents References

The vast majority of what you’re reading here is information pulled from articles we’ve published on CSS-Tricks and those are linked up throughout the guide. In addition to those articles, the following resources were super helpful for putting this guide together.

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

“If” CSS Gets Inline Conditionals

Css Tricks - Tue, 07/09/2024 - 5:18am

A few sirens went off a couple of weeks ago when the CSS Working Group (CSSWG) resolved to add an if() conditional to the CSS Values Module Level 5 specification. It was Lea Verou’s X post that same day that caught my attention:

A historical day for CSS &#x1f600;&#x1f389;

If you write any components used and/or styled by others, you know how huge this is!

background: if(style(–variant: success), var(–green));

Even if you don’t, this will allow things like:
padding: if(var(–2xl), 1em, var(–xl) or var(–m),… pic.twitter.com/cXeqwBuXvK

— Lea Verou (@LeaVerou) June 13, 2024

Lea is the one who opened the GitHub issue leading to the discussion and in a stroke of coincidence — or serendipity, perhaps — the resolution came in on her birthday. That had to be quite a whirlwind of a day! What did you get for your birthday? “Oh, you know, just an accepted proposal to the CSS spec.” Wild, just wild.

The accepted proposal is a green light for the CSSWG to work on the idea with the intent of circulating a draft specification for further input and considerations en route to, hopefully, become a recommended CSS feature. So, it’s gonna be a hot minute before any of this is baked, that is, if it gets fully baked.

But the idea of applying styles based on a conditional requirement is super exciting and worth an early look at the idea. I scribbled some notes about it on my blog the same day Lea posted to X and thought I’d distill those here for posterity while rounding up more details that have come up since then.

This isn’t a new idea

Many proposals are born from previously rejected proposals and if() is no different. And, indeed, we have gained several CSS features in recent days that allow for conditional styling — :has() and Container Style Queries being two of the more obvious examples. Lea even cites a 2018 ticket that looks and reads a lot like the accepted proposal.

The difference?

Style queries had already shipped, and we could simply reference the same syntax for conditions (plus media() and supports() from Tab’s @when proposal) whereas in the 2018 proposal how conditions would work was largely undefined.

Lea Verou, “Inline conditionals in CSS?”

I like how Lea points out that CSS goes on to describe how CSS has always been a conditional language:

Folks… CSS had conditionals from the very beginning. Every selector is essentially a conditional!

Lea Verou, “Inline conditionals in CSS?”

True! The Cascade is the vehicle for evaluating selectors and matching them to HTML elements on a page. What if() brings to the table is a way to write inline conditions with selectors.


It boils down to this:

<if()> = if( <container-query>, [<declaration-value>]{1, 2} )


  • Values can be nested to produce multiple branches.
  • If a third argument is not provided, it becomes equivalent to an empty token stream.

All of this is conceptual at the moment and nothing is set in stone. We’re likely to see things change as the CSSWG works on the feature. But as it currently stands, the idea seems to revolve around specifying a condition, and setting one of two declared styles — one as the “default” style, and one as the “updated” style when a match occurs.

.element { background-color: /* If the style declares the following custom property: */ if(style(--variant: success), var(--color-green-50), /* Matched condition */ var(--color-blue-50); /* Default style */ ); }

In this case, we’re looking for a style() condition where a CSS variable called --variant is declared and is set to a value of success, and:

  • …if --variant is set to success, we set the value of success to --color-green-50 which is a variable mapped to some greenish color value.
  • …if --variant is not set to success, we set the value of the success to --color-blue-50 which is a variable mapped to some bluish color value.

The default style would be optional, so I think it can be omitted in some cases for slightly better legibility:

.element { background-color: /* If the style declares the following custom property: */ if(style(--variant: success), var(--color-green-50) /* Matched condition */ ); }

The syntax definition up top mentions that we could support a third argument in addition to the matched condition and default style that allows us to nest conditions within conditions:

background-color: if( style(--variant: success), var(--color-success-60), if(style(--variant: warning), var(--color-warning-60), if(style(--variant: danger), var(--color-danger-60), if(style(--variant: primary), var(--color-primary) ) ), ) );

Oomph, looks like some wild inception is happening in there! Lea goes on to suggest a syntax that would result in a much flatter structure:

<if()> = if( [ <container-query>, [<declaration-value>]{2} ]#{0, }, <container-query>, [<declaration-value>]{1, 2} )

In other words, nested conditions are much more flat as they can be declared outside of the initial condition. Same concept as before, but a different syntax:

background-color: if( style(--variant: success), var(--color-success-60), style(--variant: warning), var(--color-warning-60), style(--variant: danger), var(--color-danger-60), style(--variant: primary), var(--color-primary) );

So, rather than one if() statement inside another if() statement, we can lump all of the possible matching conditions into a single statement.

This is all related to style queries

We’re attempting to match an if() condition by querying an element’s styles. There is no corresponding size() function for querying dimensions — container queries implicitly assume size:

.element { background: var(--color-primary); /* Condition */ @container parent (width >= 60ch) { /* Applied styles */ background: var(--color-success-60); } }

And container queries become style queries when we call the style() function instead:

.element { background: orangered; /* Condition */ @container parent style(--variant: success) { /* Applied styles */ background: dodgerblue; } }

Style queries make a lot more sense to me when they’re viewed in the context of if(). Without if(), it’s easy to question the general usefulness of style queries. But in this light, it’s clear that style queries are part of a much bigger picture that goes beyond container queries alone.

There’s still plenty of things to suss out with the if() syntax. For example, Tab Atkins describes a possible scenario that could lead to confusion between what is the matched condition and default style parameters. So, who knows how this all shakes out in the end!

Conditions supporting other conditions

As we’ve already noted, if() is far from the only type of conditional check already provided in CSS. What would it look like to write an inline conditional statement that checks for other conditions, such as @supports and @media?

In code:

background-color: if( supports( /* etc. */ ), @media( /* etc. */ ) );

The challenge would be container supporting size queries. As mentioned earlier, there is no explicit size() function; instead it’s more like an anonymous function.

@andruud has a succinctly describes the challenge in the GitHub discussion:

I don’t see why we couldn’t do supports() and media(), but size queries would cause cycles with layout that are hard/impossible to even detect. (That’s why we needed the restrictions we currently have for size CQs in the first place.

“Can’t we already do this with [X] approach?”

When we were looking at the syntax earlier, you may have noticed that if() is just as much about custom properties as it is about conditionals. Several workarounds have emerged over the years to mimic what we’d gain if() we could set a custom property value conditionally, including:

  • Using custom properties as a Boolean to apply styles or not depending on whether it is equal to 0 or 1. (Ana has a wonderful article on this.)
  • Using a placeholder custom property with an empty value that’s set when another custom property is set, i.e. “the custom property toggle trick” as Chris describes it.
  • Container Style Queries! The problem (besides lack of implementation) is that containers only apply styles to their descendants, i.e., they cannot apply styles to themselves when they meet a certain condition, only its contents.

Lea gets deep into this in a separate post titled “Inline conditional statements in CSS, now?” that includes a table that outlines and compares approaches, which I’ll simply paste below. The explanations are full of complex CSS nerdery but are extremely helpful for understanding the need for if() and how it compares to the clever “hacks” we’ve used for years.

MethodInput valuesOutput valuesProsConsBinary Linear InterpolationNumbersQuantitativeCan be used as part of a valueLimited output rangeTogglesvar(--alias) (actual values are too weird to expose raw)AnyCan be used in part of a valueWeird values that need to be aliasedPaused animationsNumbersAnyNormal, decoupled declarationsTakes over animation property

Cascade weirdnessType GrindingKeywordsAny value supported by the syntax descriptorHigh flexibility for exposed APIGood encapsulationMust insert CSS into light DOM

Tedious code (though can be automated with build tools)

No Firefox support (though that’s changing)Variable animation nameKeywordsAnyNormal, decoupled declarationsImpractical outside of Shadow DOM due to name clashes

Takes over animation property

Cascade weirdness Happy birthday, Lea!

Belated by two weeks, but thanks for sharing the spoils of your big day with us! &#x1f382;


To Shared LinkPermalink on CSS-Tricks

“If” CSS Gets Inline Conditionals originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Poppin’ In

Css Tricks - Wed, 06/26/2024 - 6:37am

Oh, hey there! It’s been a hot minute, hasn’t it? Thought I’d pop in and say hello. &#x1f44b;

Speaking of “popping” in, I’ve been playing with the Popover API a bit. We actually first noted it wayyyyy back in 2018 when Chris linked up some information about the <dialog> element. But it’s only been since April of this year that we finally have full Popover API support in modern browsers.

There was once upon a time that we were going to get a brand-new <popover> element in HTML for this. Chromium was working on development as recently as September 2021 but reached a point where it was dropped in favor of a popover attribute instead. That seems to make the most sense given that any element can be a popover — we merely need to attach it to the attribute to enable it.

<div popover> <!-- Stuff --> </div>

This is interesting because let’s say we have some simple little element we’re using as a popover:


If this is all the markup we have and we do absolutely nothing in the CSS, then the waving emoji displays as you might expect.

CodePen Embed Fallback

Add that popover attribute to the mix, however, and it’s gone!

CodePen Embed Fallback

That’s perhaps the first thing that threw me off. Most times something disappears and I assume I did something wrong. But cracking open DevTools shows this is exactly what’s supposed to happen.

The element is set to display: none by default.

There may be multiple popovers on a page and we can differentiate them with IDs.

<div popover id="tooltip"> <!-- Stuff --> </div> <div popover id="notification"> <!-- Stuff --> </div>

That’s not enough, as we also need some sort of “trigger” to make the popover, well, pop! We get another attribute that turns any button (or <input>-flavored button) into that trigger.

<button popovertarget="wave">Say Hello!</button> <div popover id="wave">&#x1f44b;</div>

Now we have a popover “targeted ” to a <button>. When the button is clicked, the popover element toggles visibility.

CodePen Embed Fallback

This is where stuff gets really fun because now that CSS is capable of handling logic to toggle visibility, we can focus more on what happens when the click happens.

Like, right now, the emoji is framed by a really thick black border when it is toggled on. That’s a default style.

Notice that the border sizing in the Box Model diagram.

A few other noteworthy things are going on in DevTools there besides the applied border. For example, notice that the computed width and height behave more like an inline element than a block element, even though we are working with a straight-up <div> — and that’s true even though the element is clearly computing as display: block. Instead, what we have is an element that’s sized according to its contents and it’s placed in the dead center of the page. We haven’t even added a single line of CSS yet!

Speaking of CSS, let’s go back to removing that default border. You might think it’s possible by declaring no border on the element.

/* Nope &#x1f44e; */ #wave { border: 0; }

There’s actually a :popover-open pseudo-class that selects the element specifically when it is in an “open” state. I’d love this to be called :popover-popped but I digress. The important thing is that :popover-open only matches the popover element when it is open, meaning these styles are applied after those declared on the element selector, thus overriding them.

CodePen Embed Fallback

Another way to do this? Select the [popover] attribute:

/* Select all popovers on the page */ [popover] { border: 0; } /* Select a specific popover: */ #wave[popover] { border: 0; } /* Same as: */ #wave:popover-open { border: 0; }

With this in mind, we can, say, attach an animation to the #wave in its open state. I’m totally taking this idea from one of Jhey’s demos.

CodePen Embed Fallback

Wait, wait, there’s more! Popovers can be a lot like a <dialog> with a ::backdrop if we need it. The ::backdrop pseudo-element can give the popover a little more attention by setting it against a special background or obscuring the elements behind it.

I love this example that Mojtaba put together for us in the Almanac, so let’s go with that.

CodePen Embed Fallback

Can you imagine all the possibilities?! Like, how much easier will it be to create tooltips now that CSS has abstracted the visibility logic? Much, much easier.

CodePen Embed Fallback

Michelle Barker notes that this is probably less of a traditional “tooltip” that toggles visibility on hover than it is a “toggletip” controlled by click. That makes a lot of sense. But the real reason I mention Michelle’s post is that she demonstrates how nicely the Popover API ought to work with CSS Anchor Positioning as it gains wider browser support. That will help clean out the magic numbers for positioning that are littering my demo.

Here’s another gem from Jhey: a popover doesn’t have to be a popover. Why not repurpose the Popover API for other UI elements that rely on toggled visibility, like a slide-out menu?

CodePen Embed Fallback

Oh gosh, look at that: it’s getting late. There’s a lot more to the Popover API that I’m still wrapping my head around, but even the little bit I’ve played with feels like it will go a long way. I’ll drop in a list of things I have bookmarked to come back to. For now, though, thanks for letting me pop back in for a moment to say hi.

Poppin’ In originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Ask LukeW: PDF Parsing with Vision Models

LukeW - Tue, 06/25/2024 - 2:00pm

Over the years, I've given more than 300 presentations on design. Most of these have been accompanied by a slide deck to illustrate my points and guide the narrative. But making the content in these decks work well with the Ask Luke conversational interface on this site has been challenging. So now I'm trying a new approach with AI vision models.

To avoid application specific formats (Keynote, PowerPoint), I've long been making my presentation slides available for download as PDF documents. These files usually consist of 100+ pages and often don't include a lot of text, leaning instead on visuals and charts to communicate information. To illustrate, here's of few of these slides from my Mind the Gap talk.

In an earlier article on how we built the Ask Luke conversational interface, I outlined the issues with extracting useful information from these documents. I wanted the content in these PDFs to be available when answering people's design questions in addition to the blog articles, videos and audio interviews that we were already using.

But even when we got text extraction from PDFs working well, running the process on any given PDF document would create many content embeddings of poor quality (like the one below). These content chunks would then end up influencing the answers we generated in less than helpful ways.

To prevent these from clogging up our limited context (how much content we can work with to create an answer) with useless results, we set up processes to remove low quality content chunks. While that improved things, the content in these presentations was no longer accessible to people asking questions on Ask Luke.

So we tried a different approach. Instead of extracting text from each page of a PDF presentation, we ran it through an AI vision model to create a detailed description of the content on the page. In the example below, the previous text extraction method (on the left) gets the content from the slide. The new vision model approach (on the right) though, does a much better job creating useful content for answering questions.

Here's another example illustrating the difference between the PDF text extraction method used before and the vision AI model currently in use. This time instead of a chart, we're generating a useful description of a diagram.

This change is now rolled out across all the PDFs the Ask Luke conversational interface can reference to answer design questions. Gone are useless content chunks and there's a lot more useful content immediately available.

Thanks to Yangguang Li for the dev help on this change.

<p>CSS Meditation #8:<code> .work +

Css Tricks - Mon, 06/24/2024 - 8:28am

CSS Meditation #8: .work + .life { border: 10px solid #000; }

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

<p>CSS Meditation #7: Nobody is perf

Css Tricks - Mon, 06/24/2024 - 8:27am

CSS Meditation #7: Nobody is perf-ect.

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

<p>CSS Meditation #6: The color space

Css Tricks - Mon, 06/24/2024 - 8:27am

CSS Meditation #6: The color space is always calc(rgb(0 255 0)+er) on the other side of the fence.

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

<p>CSS Meditation #5: <code>:where(:is(

Css Tricks - Mon, 06/24/2024 - 8:26am

CSS Meditation #5: :where(:is(.my-mind))

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

<p>CSS Meditation #4: Select, style,

Css Tricks - Mon, 06/24/2024 - 8:26am

CSS Meditation #4: Select, style, adjust. Select, style, adjust. Select, sty…

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

<p>CSS Meditation #3: A pseudo is as a

Css Tricks - Mon, 06/24/2024 - 8:25am

CSS Meditation #3: A pseudo is as a pseudo does.

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

<p>CSS Meditation #2: Who gives a

Css Tricks - Mon, 06/24/2024 - 8:25am

CSS Meditation #2: Who gives a flying frick what constitutes a “programming” language.

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

<p>CSS Meditation #1: If the code works

Css Tricks - Mon, 06/24/2024 - 8:22am

CSS Meditation #1: If the code works as expected and it fits your mental model, then it’s perfect.

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

Ask LukeW: Text Generation Differences

LukeW - Thu, 06/20/2024 - 2:00pm

As the number of highly capable large language models (LLMs) released continues to quickly increase, I added the ability to test new models when they become available in the Ask Luke conversational interface on this site.

For context there's a number of places in the Ask Luke pipeline that make use of AI models to transform, clean, embed, retrieve, generate content and more. I put together a short video that explains how this pipeline is constructed and why if you're interested.

Specifically for the content generation step, once the right content is found, ranked, and assembled into a set of instructions, I can select which large language model to send these instructions to. Every model gets the same instructions unless they can support a larger context window. In which case they might get more ranked results than a model with a smaller context size.

Despite the consistent instructions, switching LLMs can have a very big impact on answer generation. I'll leave you to guess which of these two answers is powered by OpenAI's GPT-4 and which one comes from Antrhopic's new (this week) Claude 3.5 Sonnet.

Some of you might astutely point out that the instruction set could be altered in specific ways when changing models. Recently, we've found the most advanced LLMs to be more interchangeable than before. But there's still differences in how they generate content as you can clearly see in the example above. Which one is best though... could soon be a matter of personal preference.

Thanks to Yangguang Li and Sam for the dev help on this feature.

CSS Container Queries

Css Tricks - Mon, 06/10/2024 - 6:12am

Container queries are often considered a modern approach to responsive web design where traditional media queries have long been the gold standard — the reason being that we can create layouts made with elements that respond to, say, the width of their containers rather than the width of the viewport.

.parent { container-name: hero-banner; container-type: inline-size; /* or container: hero-banner / inline-size; */ } } .child { display: flex; flex-direction: column; } /* When the container is greater than 60 characters... */ @container hero-banner (width > 60ch) { /* Change the flex direction of the .child element. */ .child { flex-direction: row; } } Why care about CSS Container Queries?
  1. When using a container query, we give elements the ability to change based on their container’s size, not the viewport.
  1. They allow us to define all of the styles for a particular element in a more predictable way.
  1. They are more reusable than media queries in that they behave the same no matter where they are used. So, if you were to create a component that includes a container query, you could easily drop it into another project and it will still behave in the same predictable fashion.
  1. They introduce new types of CSS length units that can be used to size elements by their container’s size.
Table of Contents Registering Elements as Containers .cards { container-name: card-grid; container-type: inline-size; /* Shorthand */ container: card-grid / inline-size; }

This example registers a new container named card-grid that can be queried by its inline-size, which is a fancy way of saying its “width” when we’re working in a horizontal writing mode. It’s a logical property. Otherwise, “inline” would refer to the container’s “height” in a vertical writing mode.

  • The container-name property is used to register an element as a container that applies styles to other elements based on the container’s size and styles.
  • The container-type property is used to register an element as a container that can apply styles to other elements when it meets certain conditions.
  • The container property is a shorthand that combines the container-name and container-type properties into a single declaration.
Some Possible Gotchas Querying a Container @container my-container (width > 60ch) { article { flex-direction: row; } }
  • The @container at-rule property informs the browser that we are working with a container query rather than, say, a media query (i.e., @media).
  • The my-container part in there refers to the container’s name, as declared in the container’s container-name property.
  • The article element represents an item in the container, whether it’s a direct child of the container or a further ancestor. Either way, the element must be in the container and it will get styles applied to it when the queried condition is matched.
Some Possible Gotchas Container Queries Properties & Values Container Queries Properties & Values container-name container-name: none | <custom-ident>+; Value Descriptions
  • none: The element does not have a container name. This is true by default, so you will likely never use this value, as its purpose is purely to set the property’s default behavior.
  • <custom-ident>: This is the name of the container, which can be anything, except for words that are reserved for other functions, including default, none, at, no, and or. Note that the names are not wrapped in quotes.
Open in Almanac
  • Initial value: none
  • Applies to: All elements
  • Inherited: No
  • Percentages: N/A
  • Computed value: none or an ordered list of identifiers
  • Canonical order: Per grammar
  • Animation: Not animatable
container-type container-type: normal | size | inline-size; Value Descriptions
  • normal: This indicates that the element is a container that can be queried by its styles rather than size. All elements are technically containers by default, so we don’t even need to explicitly assign a container-type to define a style container.
  • size: This is if we want to query a container by its size, whether we’re talking about the inline or block direction.
  • inline-size: This allows us to query a container by its inline size, which is equivalent to width in a standard horizontal writing mode. This is perhaps the most commonly used value, as we can establish responsive designs based on element size rather than the size of the viewport as we would normally do with media queries.
Open in Almanac
  • Initial value: normal
  • Applies to: All elements
  • Inherited: No
  • Percentages: N/A
  • Computed value: As specified by keyword
  • Canonical order: Per grammar
  • Animation: Not animatable
container container: <'container-name'> [ / <'container-type'> ]? Value Definitons

If <'container-type'> is omitted, it is reset to its initial value of normalwhich defines a style container instead of a size container. In other words, all elements are style containers by default, unless we explicitly set the container-type property value to either size or inline-size which allows us to query a container’s size dimensions.

Open in Almanac
  • Initial value: none / normal
  • Applies to: All elements
  • Inherited: No
  • Percentages: N/A
  • Computed value: As specified
  • Canonical order: Per grammar
  • Animation: Not animatable
Container Length Units Container Width & Height Units UnitNameEquivalent to…cqwContainer query width1% of the queried container’s widthcqhContainer query height1% of the queried container’s height Container Logical Directions UnitNameEquivalent to…cqiContainer query inline size1% of the queried container’s inline size, which is its width in a horizontal writing mode.cqbContainer query block size1% of the queried container’s inline size, which is its height in a horizontal writing mode. Container Minimum & Maximum Lengths UnitNameEquivalent to…cqminContainer query minimum sizeThe value of cqi or cqb, whichever is smaller.cqmaxContainer query maximum sizeThe value of cqi or cqb, whichever is larger. Container Style Queries

Container Style Queries is another piece of the CSS Container Queries puzzle. Instead of querying a container by its size or inline-size, we can query a container’s CSS styles. And when the container’s styles meet the queried condition, we can apply styles to other elements. This is the sort of “conditional” styling we’ve wanted on the web for a long time: If these styles match over here, then apply these other styles over there.

CSS Container Style Queries are only available as an experimental feature in modern web browsers at the time of this writing, and even then, style queries are only capable of evaluating CSS custom properties (i.e., variables).

Browser Support

The feature is still considered experimental at the time of this writing and is not supported by any browser, unless enabled through feature flags.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

DesktopChromeFirefoxIEEdgeSafari129NoNo126TPMobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS Safari126No12618.0 Registering a Style Container article { container-name: card; }

That’s really it! Actually, we don’t even need the container-name property unless we need to target it specifically. Otherwise, we can skip registering a container altogether.

And if you’re wondering why there’s no container-type declaration, that’s because all elements are already considered containers. It’s a lot like how all elements are position: relative by default; there’s no need to declare it. The only reason we would declare a container-type is if we want a CSS Container Size Query instead of a CSS Container Style Query.

So, really, there is no need to register a container style query because all elements are already style containers right out of the box! The only reason we’d declare container-name, then, is simply to help select a specific container by name when writing a style query.

Using a Style Container Query @container style(--bg-color: #000) { p { color: #fff; } }

In this example, we’re querying any matching container (because all elements are style containers by default).

Notice how the syntax it’s a lot like a traditional media query? The biggest difference is that we are writing @container instead of @media. The other difference is that we’re calling a style() function that holds the matching style condition. This way, a style query is differentiated from a size query, although there is no corresponding size() function.

In this instance, we’re checking if a certain custom property named --bg-color is set to black (#000). If the variable’s value matches that condition, then we’re setting paragraph (p) text color to white (#fff).

Custom Properties & Variables .card-wrapper { --bg-color: #000; } .card { @container style(--bg-color: #000) { /* Custom CSS */ } } Nesting Style Queries @container style(--featured: true) { article { grid-column: 1 / -1; } @container style(--theme: dark) { article { --bg-color: #000; --text: #fff; } } } Specification

CSS Container Queries are defined in the CSS Containment Module Level 3 specification, which is currently in Editor’s Draft status at the time of this writing.

Browser Support

Browser support for CSS Container Size Queries is great. It’s just style queries that are lacking support at the time of this writing.

  • Chrome 105 shipped on August 30, 2022, with support.
  • Safari 16 shipped on September 12, 2022, with support.
  • Firefox 110 shipped on February 14, 2023, with support.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

DesktopChromeFirefoxIEEdgeSafari106110No10616.0Mobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS Safari12612712616.0 Demos!

Many, many examples on the web demonstrate how container queries work. The following examples are not unique in that regard in that they illustrate the general concept of applying styles when a container element meets a certain condition.

You will find plenty more examples listed in the References at the end of this guide, but check out Ahmad Shadeed’s Container Queries Lab for the most complete set of examples because it also serves as a collection of clever container query use cases.

Card Component

In this example, a “card” component changes its layout based on the amount of available space in its container.

CodePen Embed Fallback Call to Action Panel

This example is a lot like those little panels for signing up for an email newsletter. Notice how the layout changes three times according to how much available space is in the container. This is what makes CSS Container Queries so powerful: you can quite literally drop this panel into any project and the layout will respond as it should, as it’s based on the space it is in rather than the size of the browser’s viewport.

CodePen Embed Fallback Stepper Component

This component displays a series of “steps” much like a timeline. In wider containers, the stepper displays steps horizontally. But if the container becomes small enough, the stepper shifts things around so that the steps are vertically stacked.

CodePen Embed Fallback Icon Button

Sometimes we like to decorate buttons with an icon to accentuate the button’s label with a little more meaning and context. And sometimes we don’t know just how wide that button will be in any given context, which makes it tough to know when exactly to hide the icon or re-arrange the button’s styles when space becomes limited. In this example, an icon is displayed to the right edge of the button as long as there’s room to fit it beside the button label. If room runs out, the button becomes a square tile that stacks the icons above the label. Notice how the border-radius is set in container query units, 4cqi, which is equal to 4% of the container’s inline-size (i.e. width) and results in rounder edges as the button grows in size.

CodePen Embed Fallback Pagination

Pagination is a great example of a component that benefits from CSS Container Queries because, depending on the amount of space we have, we can choose to display links to individual pages, or hide them in favor of only two buttons, one to paginate to older content and one to paginate to newer content.

CodePen Embed Fallback Articles & Tutorials General Information Article on Jun 14, 2024 CSS Container Queries Robin Rendle Article on Jun 14, 2024 CSS Container Queries Robin Rendle Article on Jun 14, 2024 CSS Container Queries Geoff Graham Article on Jun 14, 2024 CSS Container Queries Chris Coyier Article on Jun 14, 2024 CSS Container Queries Chris Coyier Article on Jun 14, 2024 CSS Container Queries Una Kravets Article on Jun 14, 2024 CSS Container Queries Geoff Graham Article on Jun 14, 2024 CSS Container Queries Chris Coyier Article on Jun 14, 2024 CSS Container Queries Chris Coyier Article on Jun 14, 2024 CSS Container Queries Mathias Hülsbusch

Container Size Query Tutorials Article on Jun 14, 2024 CSS Container Queries Chris Coyier Article on Jun 14, 2024 CSS Container Queries Jhey Tompkins Article on Jun 14, 2024 CSS Container Queries Dan Christofi Article on Jun 14, 2024 CSS Container Queries Chris Coyier Article on Jun 14, 2024 CSS Container Queries Geoff Graham Article on Jun 14, 2024 CSS Container Queries Geoff Graham Article on Jun 14, 2024 CSS Container Queries Chris Coyier Article on Jun 14, 2024 CSS Container Queries Chris Coyier

Container Style Queries Article on Jun 14, 2024 CSS Container Queries Geoff Graham Article on Jun 14, 2024 CSS Container Queries Geoff Graham

Almanac References Article on Jun 14, 2024 CSS Container Queries Geoff Graham Article on Jun 14, 2024 CSS Container Queries Geoff Graham Article on Jun 14, 2024 CSS Container Queries Geoff Graham

Related Guides Article on Jun 14, 2024 CSS Container Queries Andrés Galante Article on Jun 14, 2024 CSS Container Queries Chris Coyier


CSS Container Queries originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Ask LukeW: Dynamic Preview Cards

LukeW - Thu, 05/30/2024 - 2:00pm

After adding the Ask Luke feature to my site last year, I began sharing interesting questions people asked and their answers. But doing so manually meant creating an image in Photoshop and attaching it to posts on Twitter, LinkedIn, etc. Now with dynamic Open Graph previews, these preview cards get created on the fly- pretty sweet.

Ask Luke is an AI-powered conversational interface that uses the thousands of articles, videos, audio files, and PDFs I've created over the years to answer people's questions about digital product design. Every time the system answers a question, it does so dynamically. So technically, each answer is unique.

To make each question and answer pair sharable, the first step was to enable creating a unique link to it. The second was to use Vercel's image generation library to create a preview card each time someone makes a link.

The dynamic preview card for each question and answer pair includes as much of the question we can in addition to a bit of the response. It also adapts to varying question and answer lengths since it is generated dynamically.

When shared on Twitter, LinkedIn, Apple Messages, Slack, and any other application that supports Open Graph previews, an image with the question and answer is displayed providing a sense of what the link leads to.

Thanks to Yangguang Li, Thanh Tran, and Sam for the tips and help with this.

Bolting on AI Features

LukeW - Tue, 05/28/2024 - 2:00pm

As more companies embrace new AI-enabled capabilities, a commonly held position is that established players will "win" by integrating AI features into their existing platforms and products. But the more established these companies are... the more competing interests they face when doing so.

Consider Microsoft's Web browser Edge and its start-up experience. Edge is now your AI-powered browser. But it's also your way to browse, shop, find, create, game, protect, learn, pin, personalize, sign-in, import, sync on the go, and discover. In other words, any new AI feature faces stiff competition from all the other existing features that are still vying for people's attention and use. (Lots of internal team objectives to hit at Microsoft)

Sure, the AI features are mentioned first but they're likely tuned out and skipped over like all the other browser features being promoted during setup. After years of being asked to adopt sign-in, shopping, syncing, personalization and more, people have learned to ignore and dismiss marketing messages especially when they come as fast and furious as they do on Windows.

And it's not just setup. Once you start using Edge, the right side-panel is loaded with icons. Of course, the most brightly colored icon is the AI feature but it's right there alongside search, shopping, tools, games, Microsoft 365, Outlook, Drop, Browser essentials, Collections, and more. Once again the effect is to tune it all out. Too much, too often. And the new AI features fare the same as all the existing features.

Looking beyond Edge, the same issues persist across Windows. Yes, there's a new AI feature icon but it's competing with Windows Start, Microsoft Start, and god knows what else (after a while I was afraid to click on anything else).

Contrast this situation with new products and companies that start with AI capabilities at their core. They don't have a laundry list of pre-AI features competing for attention. They are not beholden to the revenue streams and teams behind those features. They can build from the ground up and use AI-based capabilities to build the core of their offering leading to new paradigms and value adds.

Of course, new entrants don't have massive user bases to leverage. But often a large existing user base is a disadvantage, because adoption of new features isn't earned. Bolt on an AI feature to existing user base and some subset will try it or use it. The numbers "look good" but even turkeys fly in hurricanes.

Building from the ground up means you have to earn each user by providing value not just promotions. But it also means you're creating something valuable if people decide to come and especially if they stay. When you're integrating features, it's often harder to tell.

Mind the Gap

LukeW - Tue, 05/07/2024 - 2:00pm

Despite good intentions, lots of user-centered design isn’t actually user-centered. Learn what drives these gaps and how your organization can align business and customer needs to deliver the kind of user experiences we all want to have online. With data informed insights, “live” redesigns, and more Luke will give you the tools and information you need to close the gap between customers and companies.


Hello everyone, today I'll talk through the mobile opportunity and how we're doing delivering actually user-centric design and products to the world.

I often like to say mobile is a planet-scale opportunity. But what do I mean by planet-scale. Well to start, there's 7.7 billion people on our planet. Of those, not everyone can use the software and services we make. Some are too young, some are too old, so we need to slice this number down a bit. It's not perfect, but 14 plus is one way of doing that.

Of the 7.7 billion people on the planet, about 5 billion have a mobile plan. Some with data, others not, but they're subscribed to something on their device. And last but not least, there's about 4 billion active smartphones.

While these smartphones vary in their capabilities, they're effectively as powerful as personal, even supercomputers of the past. They're pocket-sized and connected to the global internet pretty much all the time. With 4 billion of them active today, that's quite the achievement and quite the opportunity for all of us.

So when these 4 billion pocket-sized supercomputers visit the vast network of information we've built over the past 30 years, what kind of experience are they getting?

There's a high degree of likelihood that when they visit one of the many websites out there, they'll start by encountering something like a newsletter sign-up dialogue, or maybe a confounding cookie consent policy, perhaps a full-screen app download interstitial.

If they're really lucky, they might get three app download prompts, or two with a cookie consent dialogue and an account sign-up prompt, or maybe one of each with a fixed position ad thrown in for good measure.

Now before you say, that's only content publisher sites that do that, let's look at an example from e-commerce. Now you might argue, well that's just in the US, right? Doesn't seem that way. Or it's not a problem for big companies. Eh, it seems the resources they have enable them to do this type of stuff as well. And while we all probably dislike encountering webpages like this, most of them are designed this way because these techniques work, right?

I mean, app install ads. They happen everywhere. They're all over, from e-commerce to publishing and beyond. While they come in many shapes and sizes, they're there to get people to download native mobile applications. So how effective are they?

Well as usual, it depends on what we mean by effective. It turns out, if you put a full-screen interstitial with a big button in front of people, some portion of them will click. In this example we published from Google, about 9% of people click the big Get the App button. Success, right? It's a pretty good conversion right there.

But the other half of the story is that when we tested removing the full-page interstitial, app downloads only dropped 2%, but daily active users on the web went up 17%. What's the last A-B test you did that had that kind of impact? Perhaps removing things is a great idea for many of our tests instead of just adding stuff.

So if you're measuring active users instead of conversion on app install button clicks, the definition of what's good quickly changes.

When we observe people using our sites, we find app install banners can also have a lot of negative, unintended consequences. In this example, this user is trying to purchase some rosy pink shimmer. And though they've already selected the product they want, they can't seem to find something really important. So they scroll down, they scroll up, they begin to scroll to the left and to the right and back again, searching for that elusive Add to Cart button.

After all, once they have a product they'd like to purchase, the next step is actually checking out. But try as they might, nowhere on the screen is an Add to Cart button to be found. Scrolling doesn't seem to turn it up. So where could it be? Going down again, and down further, coming back up, still nothing. You'd expect it to be somewhere right around here, wouldn't you?

Perhaps they'll tap the little almost cart-like icon at the top. No, nothing there either. Well coming back again, perhaps they'll be able to find it. Let's see how that works. No, still not there. Nothing in the cart, nothing on the page. Out of desperation, what this person decides to do is tap the little X down by the Sephora app ad. And there, lo and behold, an Add to Basket option.

In examples like this and others, app install banners were the direct and sole cause of shopping cart abandonment. In Baymard Institute's testing, 53% of sites displayed one of these banners.

Here's another example. Let's say you want to take a look at this shelved ottoman a little closer. So you tap to zoom, and then you, well, unless you close the app install banner, you can't actually get back to the page where you purchased it.

Which again, if you ask most e-commerce company what metrics they care about, sales conversion is pretty high on the list. So having a user experience that negatively affects that seems like a pretty big deal. And as a result, it's probably worth asking, how do we end up with issues like these?

How can these prevalent app install banners be the direct and sole cause of abandonment when abandonment is the opposite of what we're looking for? Is this a user experience design problem? Maybe it's because these companies aren't investing in user experience.

But when I did a quick job search, I found that not only do they have user experience design teams, but pretty much all of them tout the importance of user-centered design on their business in these listings. Not only that, the job descriptions are filled with superlatives on the impact and importance of great user experience design. So what's going on?

Because like you, I don't find these web experiences to be very user-centric at all. In fact, I'd characterize most of these as user hostile. And it's not just these companies. And I really don't want to single out anyone in particular. But you don't have to look very far to encounter these kinds of experiences on the web today.

Often I hear all this is because business goals are outweighing design goals. PM made me do it. Legal made me do it. But as we saw with app and initial banners, important business goals like daily active users and e-commerce conversions are taking a hit with these approaches.

So why would the business side of the team be forcing us to do this stuff?

To answer this question, we're going to have to go back in time a bit to the Rhode Island School of Design. If you've ever taken a design class like the ones at RISD, you've experienced a design critique. This is where a class goes over people's work and talks about the choices they made and hopefully offers constructive feedback to improve those choices.

One of the things most design critiques have in common is that they're long and they take place in design studios. Most of the students sit on the hard floor or on uncomfortable seats for hours. At RISD, one student noticed this and realized many of his classmates' butts were actually quite sore by the end of these sessions. This is him over there, 26-year-old Joe Gebbia.

Joe experienced this pain firsthand, which led him to search for a solution. He designed a cushion and named it Crit Buns to tackle the problem and plunged full-time into his new business after graduation. Lots of skeptical retailers rejected his cushions until he finally sold 200 units to the Museum of Modern Art in New York, after which orders took off.

Joe was able to build a very user-centered product that addressed real pain points because he was the one experiencing the pain. His butt hurt like his classmates' butts, so he had an intimate knowledge of the problem he set out to solve.

Later, when Joe was in San Francisco, he was encouraged to start another project based on his experience with Crit Buns. He convinced his classmate from RISD, Brian Chesky, to move up to San Francisco to work on what's next.

The rent in San Francisco was high, and the two needed some money. So Joe sent Brian an email outlining a plan to turn their apartment into a bed and breakfast during a design conference, but they only had an air mattress. So they launched a site titled Air Bed and Breakfast, which brought in three guests.

They hosted them, made them breakfast, and earned $80 per head. After that first weekend, they began receiving emails from people around the world asking when the site would be available for other destinations, like London or Japan. From there, Airbnb was born.

Today it serves 2 million guests a night around the world. So Joe and Brian weren't the only ones with the problem they tried to solve. Once again, Joe and Brian had an intimate knowledge of the issue. They needed money. They did the hosting. They made the breakfasts.

When the user is the maker, there's no gap between who is building the product and who is using it. So user-centered design is easy, because you're the user. You feel the same pain, share the same perspective, and can address the problem. As a company grows, though, a gap between the customer and the company starts to open up.

When Brian and Joe recruited their former roommate Nathan to help build the next version of the Air Bed and Breakfast website, he hadn't hosted any guests with them. He didn't make them breakfast. He lacked the intimate knowledge of experiencing the problem. So Joe and Brian probably had to direct him and explain why the service needed to be the way they asked.

But we don't just have development teams in our companies. We might also hire some designers to help develop the UI, the brand, and style our sites.

As the company grows, it's likely we need a product management organization to coordinate the work of all these developers and designers, and make sure someone's thinking about the business impact, the timelines, the market dynamics, and more.

And the functions are likely to continue expanding and growing. Maybe a legal department, perhaps a security team, a growth team, or any number of distinct organizations within the larger one.

As we add these teams and they grow, we're creating distance between decision makers and our customers. The parts of the company closest to our end users aren't the ones making the decisions anymore. They're focused on running the company, keeping the organizations in place, and often rearranging those organizations. Leadership focuses on broad topics like infrastructure and portfolio management.

So this growth begins to create a gap. Let's call it the company and customer gap, or the distance between an organization and its end users. Leadership now has many levels between it and the customer, making awareness and insights more difficult to come by.

If you've ever played telephone, where you send messages down the line, you know things get scrambled pretty quickly, especially when the people playing the game have incentives to change the message as it goes down the line. They may adapt it, consciously or not, to suit the resources they need, or their particular agenda at the time. It happens.

And gradually, these competing agendas and perspectives start creeping into the products we make. That's where the company-customer gap starts showing up in product designs.

Let's look at this simple contact us form. The requirements were just to have customers contact us. So we needed a way to get back to them, find out who they are, and give them a chance to voice their message. Simple form with a name, a way to contact them that's flexible, and a submit button should probably suffice.

But once the sales team hears about this, they want to make sure that these leads have more information so they can route to the appropriate people inside of their team. They probably want an address, a city, and which department or subject is most appropriate for which sales rep.

As engineering discovers they need to build this contact form, they make the point that usernames are actually stored with first name, last name, and streets need separate fields for number, city, and zip code. So the requirements continue to grow.

Marketing finds out that we're talking to customers and of course they have some demographic questions to ask so they can segment our users appropriately and send the right messages to them through all their marketing channels. So gender, date of birth, and a toggle to allow those marketing messages to be sent pop up.

Once legal hears about all the information we're collecting, they definitely are going to require terms of use and a privacy policy acknowledgement. All together, it quickly adds up.

And this isn't a new phenomenon. In fact, it was articulated quite a long time ago in 1967 by Melvin Conway. Conway basically says organizations that produce designs are going to reflect their organizational structure in those designs. In other words, everybody ships their org chart.

So as organizations grow, decision making moves further from end users and the structure of our organization starts to show up in product designs. Our org charts sometimes become so evident in the user-centered experiences we make, you can smell the departments on what we ship. And while that's a problem in of itself, it leads to additional problems as well.

Which brings us to gap number two and a chair. With a chair, what it is and why it's there is a very small leap. You see it and you know what it's for, sitting down. There's very little gap between its form and its function. The design of the product immediately fills that gap. So the purpose of the product aligns tightly with its final form. It's designed to be sat in and the intention is clear.

Now let's look at another product experience, mostly because it's been quite popular, scooter sharing. Let's say you come upon a scooter in your town and want to ride it somewhere. What's that experience like? Well, it starts pretty typically.

Splash screen, a tutorial, sign up form, which then continues to permission dialogue, probably some terms and service agreements, back to some more permissions, followed by filling an account with some funds. Once you do that, we are back to another tutorial probably. And just to wrap it all up, let's throw in another permission dialogue. Phew, you're off.

Now there's reasons for each of these things to exist in this design, but are they all really user centric? Because when you add them up as a full experience, it's not hard to see why someone might call this painful. Well, let's look at some of these reasons and how they're part of growing the second gap.

We'll switch to another scooter sharing service, just to keep things consistent. Here, Hello Bike starts with a splash screen, followed by a sign up form, and then terms and conditions, of which you have no choice but to scroll and scroll and scroll and scroll and scroll and scroll and scroll and scroll and scroll and scroll and scroll and scroll and scroll. Finally, you get to agree to everything you just looked through.

Probably everybody can agree that 15 screens of legal copy isn't the most user centric experience. Even if you make the case that people benefit from knowing what they're signing up for, this format probably isn't the best way to do that. So when I talk to teams about why they would have this type of experience in their product, I usually hear something like, legal made me do it.

While this may seem like a valid reason, it starts opening up a gap between what something is and why it exists. Instead of the form of the product reflecting why it exists, it now starts to reflect some organizational structure. In this case, requirements, which come from siloed parts of the organization, with different perspectives and views of the world.

So the product experience isn't what directly benefits the customer, but instead what legal or PM or fill in the blanks told us to do. In other words, we start doing things for reasons other than our customers.

Let's look at another one of these reasons and through the lens of another scooter sharing company. Once again, splash screen, few permission dialogues and a tour, which is often justified by saying, everybody's doing it. But what does that mean?

Those of you that have worked at a software design company know it's pretty common to kick things off with what's known as a competitive analysis. That is, you look at what other sites or apps are doing for a specific feature, you print them out, put them on the walls and compare what you see.

In the case of scooter sharing companies, we can look at the onboarding experiences of jump, spin, ofo, bird, lime, and we see across most of them that there's an intro tour explaining the service to people. So the result of this competitive analysis is that intro tours are probably a good idea because everybody else has one, right?

But if you actually take the time to test some of these things, like the music service Vevo did, they looked at how people were using their intro tour through user testing and analytics and they found most people were just skipping through the tutorial without reading any of the copy. So if they're skipping this, what would happen if they just got rid of the tour?

Turns out in a 28-day experiment with over 160,000 participants, the total number of people who got into the app increased, removing the tutorial didn't affect engagement or retention metrics, and more people actually completed signup.

You can see similar principles at work in the evolution of several Google products as well. Google Photos, for instance, used to have an intro tour, an animated tour, and an introduction to its Android app. Following a series of tests, the team ended up with a much reduced experience. Away went the spinning logo, the get started screen, the animated tour, all which were sources of drop-off. All that was left was a redesigned version of the turn on auto backup screen, which was overlaid on top of people's photo galleries.

This step was critical to getting the value out of Google Photos. It's a service that backs up and makes your photos refindable easily. Little in the app works without this step. So the team made it the first and only focus of onboarding.

It's a great illustration of the principle of getting people to product value as fast as possible, but not faster. That is, ask the user for the minimum amount of information you need to get them the most valuable experience. In the case of Google Photos, that's turning on auto backup.

As we saw before, when we start to do things for reasons other than our customers, the gap between what our products are and why they exist expands. This time, our rationale is everybody's doing it. It's a pattern. The competitive analysis showed it's widely used. Yet again, we're doing things for reasons other than our customers.

Coming back to Ofo's scooter sharing service, we can see after sign up there's a promo to try their subscription service. However, looking at the design, there doesn't seem to be any way not to take them up on their offer. Tapping, try it free, goes to two paid plan options. But it turns out if you tap the little arrow in the upper left, you get taken to a map where you can unlock a bike ride without the subscription plan. Not very clear in design.

I have no insider information, but I suspect this was a pretty well performing A-B test. Lots of people hit that try it free button. You've probably heard a lot of people talk about the importance of A-B testing and the impact they can have on conversion. But once again, we need to think about what are we measuring?

The classic A-B testing example is changing the color of a button and seeing results. In this example, 9% more clicks. When test results come back showing one item outperformed the other for a specific metric, it's pretty natural to want to implement that. So we make a product design choice because the data made us do it.

Isn't this how we improve user experiences by testing and seeing how user behavior improves? Yes, but it matters how you define and measure improves. Many companies have results that look like the button color example. In isolation, they show great short-term gains. But when you look at the long-term impact, the numbers tell a different story.

Multiple successful A-B tests you'd think would give you cumulative results much larger than what most companies end up seeing. One of the most common reasons behind this is that we're not using tests with enough contrast. Looking at the impact of a button color change is a pretty low contrast comparison.

A more significant contrast would be to change the action altogether, to do something like promoting a native payment solution by default on specific platforms. The reason the button change is a low contrast change is it doesn't really impact what happens after someone clicks on it. They still go into the same checkout flow, the same forms.

The payment method change is higher contrast because it can completely alter the buying flow. In this case, shifting it from a multi-step form-based process to a single double tap with biometric authentication. So one way of making good use of testing is to try bigger, bolder ideas, ones that have higher risk-reward ratios.

The other way of using testing is basic good hygiene in product launches. Using experiments to check outcomes when making changes, adding new features, and even fixing bugs. This gives you a way to measurably vet any updates and avoid causing problems by monitoring and being able to turn off new changes.

Back to scooter sharing to illustrate yet another way we make decisions for reasons other than our customers. In an effort to scale the impact of design teams, many companies are now investing in design systems or common components to make it easy for teams to apply similar solutions. And nowadays, it's common for me to hear refrains like, well, the design is like that because I was just following the guidelines. But pulling a few off-the-shelf design components from a library is not the same thing as creating a good user experience.

For example, JetRadar, a flight search engine, makes use of material design guidelines in their product. They've used material design input fields in the design of their form and material design floating action buttons for their primary call to action.

But you don't have to be a UX expert to see that the end result is not particularly great. Label and input text is duplicated. What looks like inputs are actually hint text. What looks like hint text is actually labels. Elements are scattered across the page. And the primary action, frankly, just looks like a bug.

JetRadar's most recent design is much more approachable to people, though I could quibble with some of what they do. The point is, simply applying a style guide or design components doesn't ensure your product design works well. In fact, it could have the opposite effect.

Now in fairness, material design actually has updated both of the guidelines I showed earlier to try and cover cases like this. Always be learning.

But the point still stands. There's more to making a holistic user experience than applying guidelines to mockups. And while design systems have great aims, they can quickly become another reason for applying a specific solution for the sake of consistency. And as we just saw, just because something's consistent doesn't necessarily mean it's good.

Too often, the reason for making product decisions is about consistency with guidelines versus customer context and needs. It gets worse when those decisions are made without a deep understanding of the problem space.

But whether it's design guidelines, testing results, competitive analysis, or some other rationale, the more product decisions that are made for reasons other than our end users, the more the gap between what something is and why it exists expands.

To come back to scooter sharing one last time. You can come to a scooter and have a pretty clear sense that you can ride it based on the hardware design. The gap between what it is and why it exists is minimal. With software, we really seem to struggle with closing this gap.

So here's a quick way we might be able to address that. Assume you see a scooter and want to ride. The instructions point you to open your camera and point it at the QR code. From there, it's one tap to a native payment solution with some authentication and you're off riding. Effectively, this makes the process much faster.

It's also worth noting that we're using the web to make this happen. No need for a native mobile app.

Of course, we can quibble on how achievable the design I suggested is, but the point is it really tries to bridge the gap between what something is and why it exists by getting you riding a scooter as soon as possible. This is especially evident when you compare it to the first example we looked at. And in fact, most of the examples we looked at.

So how did they all get that way?

When the distance between the company and the customer increases, people start to do things for reasons other than the end user and that creates a gap between what something is and why it exists. In other words, it's evident in the product design.

But why do we care that there's a gap between what something is and why it exists? This is just design nerdery, right? Well we care because it creates the third gap, which really starts to affect the bottom line and growth of companies. With a big gap between what something is and why it exists, people's path to getting its value increases in length and difficulty.

To illustrate, let's look at an example in e-commerce. I ended up on this one because it made a top 10 e-commerce designs list somewhere. When I followed the link though, I only found two things. An app install banner and a full screen email newsletter promo. Not a great start.

So I did what most people do and dismissed the pop-up, revealing a promotional banner, an icon only navigation system, and a feature carousel. Encouraged by how my dismissal of the free shipping interstitial began to reveal more useful content, I tried removing the two promos at the top and something really interesting happened. I got to a list of categories, which doesn't seem all that compelling until you consider the impact of this UI.

In a few tests, Growth Rock compared standard e-commerce feature carousel based layouts with ones that included a few top level categories, making it clear what kind of products are available on the site. The result was a 5% increase in completed orders. Note the metric we're tracking here. Not clicks on the links, but actual impact on meaningful things, like completed orders.

There is also evidence they ran a similar experiment in another vertical, in this case for an ice cream retailer. Listing their categories up front led to a similar jump in category page views and in this case, a 29% increase in completed orders. Another example comes from Google's mobile optimization efforts, where they saw a similar outcome. Edgars is a large fashion retailer in South Africa.

They removed the animated banners, introduced some high level categories near the top of their screen and saw an increase in revenue per visitor of about 13%. So it seems like getting the categories on the site to be more visible is a good idea, especially if we are tracking impactful metrics like sales.

But there's more we can do here to help people get the value of this product and close that third gap. So next we'll tackle the icon based navigation system. It's worth mentioning that even the icons we take most for granted, like the search icon, are not as universal as we'd like to believe. So let's clarify the search function a little bit.

Instead of using just icons for a critical function like search, we're going to be more explicit in our product UI and close the gap between what something is and why it exists, with a search bar. This also gives us a chance to call out popular items and again reinforce what the site has to offer. I specifically call search out as critical because exposing it by default can also help with conversions.

In this case, boosting the number of searches as the conversion rate for users who search is usually higher than for users who don't interact with it, probably because they have specific intent. So now we have a pulled out search area, category links exposed, and well how else can we make it easier for people to get to the value of this product?

It turns out if we drop the featured image, which probably doesn't drive that much in the way of core metrics, we can show some of the actual products this site sells. Imagine that, showing popular or trending products on an e-commerce site.

But let's not just show two, let's center this module to get more content on the screen and make the images run off the side a bit so people can scroll for more, right where the thumb is for easy one-handed scrolling. This puts the ability to browse top products in a comfortable to reach zone on large screen sizes. Should make all our one-handed millennial users super happy. Because they'll scroll.

Pinterest found that even removing core features like the number of pins and likes in onboarding increased the number of photos they could show people at any given time, which increased the likelihood they'd find content they like and thereby become an active Pinterest user. Same principle applies here.

Overall, I think we've made progress on getting people to experience the value of this site a bit more directly. We could do even better maybe by putting the products up top and categories next. The goal is to get people from the state of, huh, I think I want to get out of here, to I get it, looks like my kind of thing, but you may say, Luke, what about that free shipping promo?

They were making a really big deal out of that, so it must be important, right? Indeed, the top reason for abandoning a shopping cart after browsing is shipping costs, taxes, etc. So free shipping is a winner and people should know about it. I'm not against that.

I just contend that there's probably a better time and place for it. Perhaps on the product page or the actual checkout experience when total cost is on most people's minds. The tricky but very valuable thing that makes mobile design hard is finding the right time and place for things.

It usually isn't right away on the homepage with everything. You can do this all day, but I'll add just one more consideration to this redesign. It's quite possible when someone looks at this design, they could say, but what about the brand? Now, I hope it comes through in the fonts, colors, and especially the products.

What people mean when they say that is something more like this, some aspirational imagery that reflects the personality of the company, serves as a hook for people to dive in, like this edgy Mad Max style look. And I agree, our site design is looking a little too plain.

So we can add in some brand imagery to bring back some soul. But even with this addition, I'd argue we still retain a lot of the functional benefits we've been adding or rather emphasizing by removing other things. Just be mindful that the reasons we're adding the brand imagery are tied to customer needs and not just the agenda of some department, like brand marketing. Else you'll end up back at the product experience that mashes up the agenda of multiple teams, which is increasingly the norm out there.

Now I focused a lot on free people, but they're certainly not alone. Looking at a number of other e-commerce sites, you see they're all doing similar stuff. But the end result is our third gap, the gap between people's first time experience and becoming a happy, satisfied customer. Because I alliterated the first two gaps, I had to do the same here. So we'll call this one the first to fandom gap.

What the first to fandom gap effectively means is that when there's a large gap between what something is and why it exists, it becomes much harder for people to get to its value. It takes longer and more people fall off. And as I mentioned earlier, this really starts to affect the bottom line and your growth as a company.

We've talked about a lot of stuff now. So let's try to summarize things really concisely.

When companies grow, decision making moves further away from users. And people within these organizations start to do things for reasons other than the customer, which means a bunch of things get added to the user experience, which get in the way of people experiencing the real value and purpose of what we make.

Kind of a bummer. So what can we do? How do we improve the situation? How can we bridge these gaps and actually deliver user-centric experiences instead of just saying we're doing so and acting quite differently?

We talked about three gaps. So I'm going to talk about three ways to close them.

Since we saw just how related these gaps are, these techniques actually apply to bridging all of them. The first thing we can do is be mindful that these gaps exist. When the voice of the customer is missing in critical discussions, we need to bring it back. When requirements are conflicting with a holistic product experience, we need to push back on them. When our most important experiences are underperforming, we need to learn why. Awareness is the first step to improving the situation.

Next, metrics. I work on company-wide metrics at Google. Why? Because I believe you are what you measure. Spending the time to get the right metrics affects so many things down the line. Let's say we decide to measure app downloads. Well, we start with a small promo, and then we test out another one. Oh, conversions on it are better. Well, we better keep both of them. Then we add another promo, and installs went up again. So why not drop in more?

Ooh, and things get even better when we make them bigger. Pretty soon, you've become what you measure, a giant app install ad. So please, spend the time working through what metrics to measure and why. Real quick, how to choose metrics.

First, we need to decide what change we actually want to see happen in the world. Next, we gotta figure out how could we possibly measure that change. For each of these possibilities, write down what you think is gonna happen if you start measuring it. What behaviors will you change? What actions will you take?

Next, rank that list by where you see the clearest impact. Then start actually tracking data for your top few, and see if it actually delivers the outcomes you thought it would. When you find a few that actually work for you, make sure to regularly and visibly track those.

Finally, and most importantly, spend time with customers. To illustrate this, I wanna come back to the Airbnb story I started this talk off with. Back in 2009, the Airbnb service wasn't growing. At the behest of Paul Graham, Joe and Brian went out to New York and stayed with a bunch of Airbnb hosts. There they saw firsthand the listings these folks had.

The photos made them look quite poor. They realized this was a problem they could fix, so they rented a camera, took pictures of their hosts' homes, and the next week, the revenue in New York doubled. We used to travel and actually stay with our customers, said Gebbia.

It was the ultimate enlightened empathy. You were so close to the people you were designing for that it informed you in a way that you know an online survey never would. Wise words that actually had real impact.

Based on the success of their New York experience, the Airbnb team created a photography program to scale the process, and from there, they were off to the races. Joe attributes all of this to being closer to his customers, which is really the same experience he had at RISD with design critiques and crit buns.

Getting as close as you can to the problem helps inform how to solve for it. It's not just upstart companies that can make these kind of insights happen. When I worked at eBay back in 2004, we launched a program called Visits that got people within the company into our users' homes.

These programs exist across companies, but people get really caught up in organizational objectives, their own workload, or even documents they're working on, and they don't make time. See the first and second gaps we talked about.

There's many ways you can bring user voice into your organization. We could have a whole talk outlining them. At Google, I organized a weekly meeting titled, What Did We Learn This Week? It had leads from engineering, marketing, PM, UX, and more come together for an hour every day to hear what we learned from quantitative and qualitative research across all the products in our group. It quickly became people's favorite meeting. I mean, look how excited they are in this meeting room, right?

Bottom line is there's no substitute for spending time with customers. Do it regularly, do it often. If the word user research or usability or whatever scares you, don't call it that. Just call it spending time with customers.

It really boils down to staying close to the people using your product and making sure your team directly gets that info as often as they can. Because it's only through this kind of direct empathy, through really seeing the world through our customer's eyes that we can make good on the planet-size opportunity that is mobile.

There's four billion networked pocket-sized supercomputers online right now able to access all the experiences we make for them. Let's make the kind of experiences we want to have and we want our friends and family to have.

The seams we talked about today can open up really quickly. So we need to be vigilant.

When companies grow, decision-making moves further from users. People within these organizations, good people, start to do things for reasons other than the customer. This means a bunch of stuff gets added to product designs, which get in the way of people actually getting to the purpose of what we're making.

Know and apply the simple techniques of knowing what's happening, awareness, taking time to set the right measures, and spending time with your end users.

These are pretty basic principles, but with them we can make a better web for everyone and still have successful businesses, as hopefully I've illustrated.

Please help me make this happen. If not for your customer's sakes, then for your own, because we all want a web that we can enjoy.


Making Product Value Obvious

LukeW - Mon, 04/29/2024 - 2:00pm

The most valuable contribution product designers can make to software is making the core value of a product clear to the people using it. Yet over and over again... this seemingly simple objective doesn’t get met. Why?

Making the value of a product clear through use (esp. first time use) is how to create customers. You can tell people all you want about your product’s benefits through marketing, onboarding, or a rigorous sales processes but it’s only when they experience the value for themselves that things click. They know why your product matters to them and they tell other people about it. In an ideal state, everyone that should (not all products are for all people) be able to get to this state with your product, does.

But life is not ideal and many factors get in the way of making product value obvious to people. So many, in fact, that I put together a 90min presentation on the most common ones. To summarize, an always increasing set of objectives gets in the way: adherence to a design system or technical architecture, the alignment of different team perspectives, mimicking what competitors are doing, and so on. These dynamics end up becoming the requirements that drive a design process instead of a relentless focus on making product value clear.

Many of these objectives drive us toward applying solutions instead of understanding the problem in depth. For instance, I’m sure many people read what I wrote above and thought: “oh he’s talking about onboarding”. And from there come the inevitable splash screens, tutorials, and tours that have become synonymous with user onboarding. It’s a great example of jumping to answers instead of getting so seeped in the problem that the most obvious solution emerges.

"True simplicity is, well, you just keep on going and going until you get to the point where you go... Yeah, well, of course.” —Jonathan Ive

When you go deep into a problem, solutions like tutorials and tours are not what you end up with. You communicate what a product does for people through the interactions, organization, and visual presentation of the product itself. The two become inseparable and the design feels inevitable unlike intro tours that feel bolted-on or like band-aids.

At this point I imagine folks asking for examples. So here’s two short videos from my afore-mentioned Mind the Gap talk. In the first, I redesign the front page of an e-commerce site to make its value clear up front. The process includes both visual and interaction design changes and pushing back on internal requirements.

In the second, I walk through some examples of onboarding and illustrate how the right design for "first use" will differ significantly between products because of their unique value.

Last but not least, how obvious you need to make things is often... not obvious. Teams building products talk about the value they hope to provide all the time. They've all internalized it (hopefully through regular use of the product they're building) but everyone else has not. What seems obvious to someone on the inside is not obvious to those on the outside. So being clearer than you think is needed is, in fact, needed.

I was recently reminded of this when at a Starbucks. Their food menu was labeled: All-Day Breakfast, Anytime Bakery, and All-Day Lunch. I started thinking they probably started with Breakfast, Bakery, and Lunch as labels but customers kept assuming that breakfast was only available in the mornings. So that label got changed to "All-Day Breakfast". But if breakfast is all-day, when can I get bakery items? And the label changed to "Anytime Bakery". At first blush these labels might seem excessively wordy but they're probably intentionally more obvious. And probably more obvious than originally thought necessary.

For even more on making product value obvious, check out my full Mind the Gap presentation.

Syndicate content
©2003 - Present Akamai Design & Development.