CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Fri, 02 Aug 2024 16:49:18 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 CSS-Tricks https://css-tricks.com 32 32 45537868 (Hyper) Links About (Hyper) Links https://css-tricks.com/hyper-links-about-hyper-links/ https://css-tricks.com/hyper-links-about-hyper-links/#respond Fri, 02 Aug 2024 16:41:54 +0000 https://css-tricks.com/?p=379420 Heydon on the virtues of hyperlinking hypertext in an anchor element:

Sometimes, the <a> is referred to as a hyperlink, or simply a link. But it is not one of these and people who say it is one are


(Hyper) Links About (Hyper) Links originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Heydon on the virtues of hyperlinking hypertext in an anchor element:

Sometimes, the <a> is referred to as a hyperlink, or simply a link. But it is not one of these and people who say it is one are technically wrong (the worst kind of wrong).

[…]

An <a> is an interactive element (well, it is if it has an href). The text inside an interactive element is sometimes referred to as a label since it should tell you what the element does. Since anchors take you places on the web, the text should tell you where you would be going or what you can do there.

[…]

Web developers and content editors, the world over, make the mistake of not making text that describes a link actually go inside that link. This is collosally [sic] unfortunate, given it’s the main thing to get right when writing hypertext.

As far as where that anchor hyperlinks to, Jim Nielsen back in 2003 discussed a bunch of considerations that go into designing URLs. More recently, he’s mused on the the potential of well-designed URLs to change — or more accurately, the potential of humans to change things:

If a slug is going to be human-friendly, i.e. human-readable, then it’s going to contain information that is subject to change because humans make errors.

Swapping the contents of a URL is a breaking change. If we were to start with a wonderful URL like, say:

<a href=“css-tricks.com/almanac”>

…but decide that we now like “Docs” instead of “Almanac” then we might do this:

<a href=“css-tricks.com/docs”>

Naturally, we’d drop some sorta redirect on the server so that anyone attempting to hit /almanac is automatically directed to /docs instead. But now we’ve got a form of technical debt to maintain that may not be any more dangerous than walking and chewing gum at the same time, but could become a mouthful much later. We’ve got a gazillion redirects on CSS-Tricks for a gazillion different reasons, most often for totally human reasons like typos. Remember the CSS-Tricks Chronicles we used to write? Botching the Roman numeral numbering system on those was standard fare. Look at the very last edition from 2001, titled “CSS-Tricks Choronicles XLI” and its URL:

https://css-tricks.com/css-tricks-chronicle-xxxxi/

🥸

I’ve been thinking about this a lot while attempting to organize the 7,000 some-odd articles on this site. For years, we’ve maintained a “flat” structure in the sense that the title of an article becomes the URL (after, perhaps, with some light editing):

<a href=“css-tricks.com/geoff-is-on-another-dumb-rant”>

But I’m starting to think about the content on this site in terms of type rather than title alone. For example, we’ve always had “articles” on this site with a smattering of “links” sprinkled in alongside Almanac “entries” and “guides” among other categories of content. We’ve just never reflected that in our URLs because, well, the design is flat. Adding another layer for the type of content borks the original URL!

<a href=“css-tricks.com/soapbox/geoff-is-on-another-dumb-rant”>

Jay Hoffman has been thinking about this, too.

A dead link may not seem like it means very much, even in the aggregate. But they are. One-way links, the way they exist on the web where anyone can link to anything, is what makes the web universal. In fact, the first name for URL’s was URI’s, or Universal Resource Identifier. It’s right there in the name. And as Berners-Lee once pointed out, “its universality is essential.”

[…]

Time and time again, when the web goes into crisis and part of it is lost, the Internet Archive and similar efforts come to the rescue. But even the Internet Archive is having a hard time protecting against a barrage of link rot we can’t seem to get away from.

All of this dovetails into recent reporting that Google has decided to sunset its URL shortener. All of those goo.gl URLs accumulated since the shortener was introduced in 2018?

Any developers using links built with the Google URL Shortener in the form https://goo.gl/* will be impacted, and these URLs will no longer return a response after August 25th, 2025. We recommend transitioning these links to another URL shortener provider.

There’s some minutiae of consolation for Google itself:

Note that goo.gl links generated via Google apps (such as Maps sharing) will continue to function.

To be clear, this move is less a form of link rot than it is a straight-up pruning to cut things off. If link rot is akin to allowing your hair to go gray, then deprecating Google’s URL shortener is a total head shave. Nick Heer believes there’s a good side to it, however:

In principle, I support this deprecation because it is confusing and dangerous for Google’s own shortened URLs to have the same domain as ones created by third-party users. But this is a Google-created problem because it designed its URLs poorly. It should have never been possible for anyone else to create links with the same URL shortener used by Google itself. 

I tend to agree. The whole situation is a Rosemary’s Baby predicament presenting two terribly uncomfortable choices. The right uncomfortable decision was made, but we still have to deal with the repercussions of wiping out part of the web’s context.

Heydon’s post led me down this rabbit trail, so I’ll link it up here for you to take a hike with it.

To Shared LinkPermalink on CSS-Tricks


(Hyper) Links About (Hyper) Links originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/hyper-links-about-hyper-links/feed/ 0 379420
HTML Web Components Make Progressive Enhancement and CSS Encapsulation Easier! https://css-tricks.com/html-web-components-make-progressive-enhancement-and-css-encapsulation-easier/ https://css-tricks.com/html-web-components-make-progressive-enhancement-and-css-encapsulation-easier/#comments Thu, 01 Aug 2024 13:21:37 +0000 https://css-tricks.com/?p=379335 I have to thank Jeremy Keith and his wonderfully insightful article from late last year that introduced me to the concept of HTML Web Components. This was the “a-ha!” moment for me:

When you wrap some existing markup in a


HTML Web Components Make Progressive Enhancement and CSS Encapsulation Easier! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I have to thank Jeremy Keith and his wonderfully insightful article from late last year that introduced me to the concept of HTML Web Components. This was the “a-ha!” moment for me:

When you wrap some existing markup in a custom element and then apply some new behaviour with JavaScript, technically you’re not doing anything you couldn’t have done before with some DOM traversal and event handling. But it’s less fragile to do it with a web component. It’s portable. It obeys the single responsibility principle. It only does one thing but it does it well.

Until then, I’d been under the false assumption that all web components rely solely on the presence of JavaScript in conjunction with the rather scary-sounding Shadow DOM. While it is indeed possible to author web components this way, there is yet another way. A better way, perhaps? Especially if you, like me, advocate for progressive enhancement. HTML Web Components are, after all, just HTML.

While it’s outside the exact scope of what we’re discussing here, Adny Bell has a recent write-up that offers his (excellent) take on what progressive enhancement means.

Let’s look at three specific examples that show off what I think are the key features of HTML Web Components — CSS style encapsulation and opportunities for progressive enhancement — without being forced to depend on JavaScript to work out of the box. We will most definitely use JavaScript, but the components ought to work without it.

The examples can all be found in my Web UI Boilerplate component library (built using Storybook), along with the associated source code in GitHub.

Example 1: <webui-disclosure>

Storybook render of webui-disclosure Web Component.q
Live demo

I really like how Chris Ferdinandi teaches building a web component from scratch, using a disclosure (show/hide) pattern as an example. This first example extends his demo.

Let’s start with the first-class citizen, HTML. Web components allow us to establish custom elements with our own naming, which is the case in this example with a <webui-disclosure> tag we’re using to hold a <button> designed to show/hide a block of text and a <div> that holds the <p> of text we want to show and hide.

<webui-disclosure
  data-bind-escape-key
  data-bind-click-outside
>
  <button
    type="button"
    class="button button--text"
    data-trigger
    hidden
  >
    Show / Hide
  </button>

  <div data-content>
    <p>Content to be shown/hidden.</p>
  </div>
</webui-disclosure>

If JavaScript is disabled or doesn’t execute (for any number of possible reasons), the button is hidden by default — thanks to the hidden attribute on it— and the content inside of the div is simply displayed by default.

Nice. That’s a really simple example of progressive enhancement at work. A visitor can view the content with or without the <button>.

I mentioned that this example extends Chris Ferdinandi’s initial demo. The key difference is that you can close the element either by clicking the keyboard’s ESC key or clicking anywhere outside the element. That’s what the two [data-attribute]s on the <webui-disclosure tag are for.

We start by defining the custom element so that the browser knows what to do with our made-up tag name:

customElements.define('webui-disclosure', WebUIDisclosure);

Custom elements must be named with a dashed-ident, such as <my-pizza> or whatever, but as Jim Neilsen notes, by way of Scott Jehl, that doesn’t exactly mean that the dash has to go between two words.

I typically prefer using TypeScript for writing JavaScript to help eliminate stupid errors and enforce some degree of “defensive” programming. But for the sake of simplicity, the structure of the web component’s ES Module looks like this in plain JavaScript:

default class WebUIDisclosure extends HTMLElement {
  constructor() {
    super();
    this.trigger = this.querySelector('[data-trigger]');
    this.content = this.querySelector('[data-content]');
    this.bindEscapeKey = this.hasAttribute('data-bind-escape-key');
    this.bindClickOutside = this.hasAttribute('data-bind-click-outside');
    
    if (!this.trigger || !this.content) return;
    
    this.setupA11y();
    this.trigger?.addEventListener('click', this);
  }

  setupA11y() {
    // Add ARIA props/state to button.
  }

  // Handle constructor() event listeners.
  handleEvent(e) {
    // 1. Toggle visibility of content.
    // 2. Toggle ARIA expanded state on button.
  }
  
  // Handle event listeners which are not part of this Web Component.
  connectedCallback() {
    document.addEventListener('keyup', (e) => {
      // Handle ESC key.
    });
  
    document.addEventListener('click', (e) => {
      // Handle clicking outside.
    });
  }

  disconnectedCallback() {
    // Remove event listeners.
  }
}

Are you wondering about those event listeners? The first one is defined in the constructor() function, while the rest are in the connectedCallback() function. Hawk Ticehurst explains the rationale much more eloquently than I can.

This JavaScript isn’t required for the web component to “work” but it does sprinkle in some nice functionality, not to mention accessibility considerations, to help with the progressive enhancement that allows the <button> to show and hide the content. For example, JavaScript injects the appropriate aria-expanded and aria-controls attributes enabling those who rely on screen readers to understand the button’s purpose.

That’s the progressive enhancement piece to this example.

For simplicity, I have not written any additional CSS for this component. The styling you see is simply inherited from existing global scope or component styles (e.g., typography and button).

However, the next example does have some extra scoped CSS.

Example 2: <webui-tabs>

That first example lays out the progressive enhancement benefits of HTML Web Components. Another benefit we get is that CSS styles are encapsulated, which is a fancy way of saying the CSS doesn’t leak out of the component. The styles are scoped purely to the web component and those styles will not conflict with other styles applied to the current page.

Let’s turn to a second example, this time demonstrating the style encapsulating powers of web components and how they support progressive enhancement in user experiences. We’ll be using a tabbed component for organizing content in “panels” that are revealed when a panel’s corresponding tab is clicked — the same sort of thing you’ll find in many component libraries.

Storybook render of the webui-tabs web component.
Live demo

Starting with the HTML structure:

<webui-tabs>
  <div data-tablist>
    <a href="#tab1" data-tab>Tab 1</a>
    <a href="#tab2" data-tab>Tab 2</a>
    <a href="#tab3" data-tab>Tab 3</a>
  </div>

  <div id="tab1" data-tabpanel>
    <p>1 - Lorem ipsum dolor sit amet consectetur.</p>
  </div>

  <div id="tab2" data-tabpanel>
    <p>2 - Lorem ipsum dolor sit amet consectetur.</p>
  </div>

  <div id="tab3" data-tabpanel>
    <p>3 - Lorem ipsum dolor sit amet consectetur.</p>
  </div>
</webui-tabs>

You get the idea: three links styled as tabs that, when clicked, open a tab panel holding content. Note that each [data-tab] in the tab list targets an anchor link matching a tab panel ID, e.g., #tab1, #tab2, etc.

We’ll look at the style encapsulation stuff first since we didn’t go there in the last example. Let’s say the CSS is organized like this:

webui-tabs {
  [data-tablist] {
    /* Default styles without JavaScript */
  }
  
  [data-tab] {
    /* Default styles without JavaScript */
  }

  [role='tablist'] {
    /* Style role added by JavaScript */
  }
  
  [role='tab'] {
    /* Style role added by JavaScript */
  }
  
  [role='tabpanel'] {
    /* Style role added by JavaScript */
  }
}

See what’s happening here? We have two style rules — [data-tablist] and [data-tab] — that contain the web component’s default styles. In other words, these styles are there regardless of whether JavaScript loads or not. Meanwhile, the other three style rules are selectors that are injected into the component as long as JavaScript is enabled and supported. This way, the last three style rules are only applied if JavaScript plops the **role** attribute on those elements in the HTML. Right there, we’re already supplying a touch of progressive enhancement by setting styles only when JavasScript is needed.

All these styles are fully encapsulated, or scoped, to the <webui-tabs> web component. There is no “leakage” so to speak that would bleed into the styles of other web components, or even to anything else on the page within the global scope. We can even choose to forego classnames, complex selectors, and methodologies like BEM in favour of simple descendent selectors for the component’s children, allowing us to write styles more declaratively on semantic elements.

Quickly: “Light” DOM versus Shadow DOM

For most web projects, I generally prefer to bundle CSS (including the web component Sass partials) into a single CSS file so that the component’s default styles are available in the global scope, even if the JavaScript doesn’t execute.

However, it is possible to import a stylesheet via JavaScript that is only consumed by this web component if JavaScript is available:

import styles from './styles.css';

class WebUITabs extends HTMLElement {
  constructor() {
    super();
    this.adoptedStyleSheets = [styles];
  }
}

customElements.define('webui-tabs', WebUITabs);

Alternatively, we could inject a <style> tag containing the component’s styles instead:

class WebUITabs extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' }); // Required for JavaScript access
    this.shadowRoot.innerHTML = `
      <style> <!-- styles go here --> </style>
      // etc.
    `;
  }
}
    
customElements.define('webui-tabs', WebUITabs);

Whichever method you choose, these styles are scoped directly to the web component, preventing component styles from leaking out, but allowing global styles to be inherited.

Now consider this simple example. Everything we write in between the component’s opening and closing tags is considered part of the “Light” DOM.

<my-web-component>
  <!-- This is Light DOM -->
  <div>
    <p>Some content... styles are inherited from the global scope</p>
  </div>

  ----------- Shadow DOM Boundary -------------
  | <!-- Anything injected by JavaScript -->  |
  ---------------------------------------------
</my-web-component>

Dave Rupert has an excellent write-up that makes it really easy to see how external styles are able to “pierce” the Shadow DOM and select an element in the Light DOM. Notice how the <button> element that is written in between the custom element’s tags receives the button selector’s styles in the global CSS, while the <button> injected via JavaScript is left untouched.

If we want to style the Shadow DOM <button> we’d have to do that with internal styles like the examples above for importing a stylesheet or injecting an inline <style> block.

That doesn’t mean that all CSS style properties are blocked by the Shadow DOM. In fact, Dave outlines 37 properties that web components inherit, mostly along the lines of text, list, and table formatting.

Progressively enhance the tabbed component with JavaScript

Even though this second example is more about style encapsulation, it’s still a good opportunity to see the progressive enhancement we get practically for free from web components. Let’s step into the JavaScript now so we can see how we can support progressive enhancement. The full code is quite lengthy, so I’ve abbreviated things a bit to help make the points a little clearer.

default class WebUITabs extends HTMLElement {
  constructor() {
    super();
    this.tablist = this.querySelector('[data-tablist]');
    this.tabpanels = this.querySelectorAll('[data-tabpanel]');
    this.tabTriggers = this.querySelectorAll('[data-tab]');

    if (
      !this.tablist ||
      this.tabpanels.length === 0 ||
      this.tabTriggers.length === 0
    ) return;
    
    this.createTabs();
    this.tabTriggers.forEach((tabTrigger, index) => {
      tabTrigger.addEventListener('click', (e) => {
        this.bindClickEvent(e);
      });
      tabTrigger.addEventListener('keydown', (e) => {
        this.bindKeyboardEvent(e, index);
      });
    });
  }

  createTabs() {
    // 1. Hide all tabpanels initially.
    // 2. Add ARIA props/state to tabs & tabpanels.
  }

  bindClickEvent(e) {
    e.preventDefault();
    // Show clicked tab and update ARIA props/state.
  }
  bindKeyboardEvent(e, index) {
    e.preventDefault();
    // Handle keyboard ARROW/HOME/END keys.
  }
}

customElements.define('webui-tabs', WebUITabs);

The JavaScript injects ARIA roles, states, and props to the tabs and content blocks for screen reader users, as well as extra keyboard bindings so we can navigate between tabs with the keyboard; for example, the TAB key is reserved for accessing the component’s active tab and any focusable content inside the active tabpanel, and the tabs can be traversed with the ARROW keys. So, if JavaScript fails to load, the default experience is still an accessible one where the tabs still anchor link to their respective panels, and those panels naturally stack vertically, one on top of the other.

And if JavaScript is enabled and supported? We get an enhanced experience, complete with updated accessibility considerations.

Example 3: <webui-ajax-loader>

Storybook render of webui-ajax-loader web component.
Live demo

This final example differs from the previous two in that it is entirely generated by JavaScript, and uses the Shadow DOM. This is because it is only used to indicate a “loading” state for Ajax requests, and is therefore only needed when JavaScript is enabled.

The HTML markup is just the opening and closing component tags:

<webui-ajax-loader></webui-ajax-loader>

The simplified JavaScript structure:

default class WebUIAjaxLoader extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <svg role="img" part="svg">
        <title>loading</title>
        <circle cx="50" cy="50" r="47" />
      </svg>
    `;
  }
}

customElements.define('webui-ajax-loader',WebUIAjaxLoader);

Notice right out of the gate that everything in between the <webui-ajax-loader> tags is injected with JavaScript, meaning it’s all in the Shadow DOM, encapsulated from other scripts and styles not directly bundled with the component.

But also notice the part attribute that’s set on the <svg> element. Here’s where we’ll zoom in:

<svg role="img" part="svg">
  <!-- etc. -->
</svg>

That’s yet another way we have to style the custom element: named parts. Now we can style that SVG from outside of the template literal we used to establish the element. There’s a ::part pseudo-selector to make that happen:

webui-ajax-loader::part(svg) {
  // Shadow DOM styles for the SVG...
}

And here’s something cool: that selector can access CSS custom properties, whether they are scoped globally or locally to the element.

webui-ajax-loader {
  --fill: orangered;
}

webui-ajax-loader::part(svg) {
  fill: var(--fill);
}

As far as progressive enhancement goes, JavaScript supplies all of the HTML. That means the loader is only rendered if JavaScript is enabled and supported. And when it is, the SVG is added, complete with an accessible title and all.

Wrapping up

That’s it for the examples! What I hope is that you now have the same sort of epiphany that I had when reading Jeremy Keith’s post: HTML Web Components are an HTML-first feature.

Of course, JavaScript does play a big role, but only as big as needed. Need more encapsulation? Want to sprinkle in some UX goodness when a visitor’s browser supports it? That’s what JavaScript is for and that’s what makes HTML Web Components such a great addition to the web platform — they rely on vanilla web languages to do what they were designed to do all along, and without leaning too heavily on one or the other.


HTML Web Components Make Progressive Enhancement and CSS Encapsulation Easier! originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/html-web-components-make-progressive-enhancement-and-css-encapsulation-easier/feed/ 3 379335
CSS Functions and Mixins Module Notes https://css-tricks.com/css-functions-and-mixins-module-notes/ https://css-tricks.com/css-functions-and-mixins-module-notes/#respond Wed, 31 Jul 2024 20:25:35 +0000 https://css-tricks.com/?p=378995 Most days, I’m writing vanilla CSS. Thanks to CSS variables and nesting, I have fewer reasons to reach for Sass or any other preprocessor. The times I reach for Sass tend to be when I need a @mixin to loop …


CSS Functions and Mixins Module Notes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Most days, I’m writing vanilla CSS. Thanks to CSS variables and nesting, I have fewer reasons to reach for Sass or any other preprocessor. The times I reach for Sass tend to be when I need a @mixin to loop through a list of items or help keep common styles DRY.

That could change for me in the not-so-distant future since a new CSS Functions and Mixins Module draft was published in late June after the CSSWG resolved to adopt the proposal back in February.

Notice the module’s name: Functions and Mixins. There’s a distinction between the two.

This is all new and incredibly unbaked at the moment with plenty of TODO notes in the draft and points to consider in future drafts. The draft spec doesn’t even have a definition for mixins yet. It’ll likely be some time before we get something real to work and experiment with, but I like trying to wrap my mind around these sorts of things while they’re still in early days, knowing things are bound to change.

In addition to the early draft spec, Miriam Suzanne published a thorough explainer that helps plug some of the information gaps. Miriam’s an editor on the spec, so I find anything she writes about this to be useful context.

There’s a lot to read! Here are my key takeaways…

Custom functions are advanced custom properties

We’re not talking about the single-purpose, built-in functions we’ve come to love in recent years — e.g., calc(), min(), max(), etc. Instead, we’re talking about custom functions defined with an @function at-rule that contains logic for returning an expected value.

That makes custom functions a lot like a custom property. A custom property is merely a placeholder for some expected value that we usually define up front:

:root {
  --primary-color: hsl(25 100% 50%);
}

Custom functions look pretty similar, only they’re defined with @function and take parameters. This is the syntax currently in the draft spec:

@function <function-name> [( <parameter-list> )]? {
  <function-rules>

  result: <result>;
}

The result is what the ultimate value of the custom function evaluates to. It’s a little confusing to me at the moment, but how I’m processing this is that a custom function returns a custom property. Here’s an example straight from the spec draft (slightly modified) that calculates the area of a circle:

@function --circle-area(--r) {
  --r2: var(--r) * var(--r);

  result: calc(pi * var(--r2));
}

Calling the function is sort of like declaring a custom property, only without var() and with arguments for the defined parameters:

.element {
  inline-size: --circle-area(--r, 1.5rem); /* = ~7.065rem */
}

Seems like we could achieve the same thing as a custom property with current CSS features:

:root {
  --r: 1rem;
  --r2: var(--r) * var(--r);
  --circle-area: calc(pi * var(--r2));
}

.element {
  inline-size: var(--circle-area, 1.5rem);
}

That said, the reasons we’d reach for a custom function over a custom property are that (1) they can return one of multiple values in a single stroke, and (2) they support conditional rules, such as @supports and @media to determine which value to return. Check out Miriam’s example of a custom function that returns one of multiple values based on the inline size of the viewport.

/* Function name */
@function --sizes(
  /* Array of possible values */
  --s type(length),
  --m type(length),
  --l type(length),
  /* The returned value with a default */
) returns type(length) {
  --min: 16px;

  /* Conditional rules */
  @media (inline-size < 20em) {
    result: max(var(--min), var(--s, 1em));
  }
  @media (20em < inline-size < 50em) {
    result: max(var(--min), var(--m, 1em + 0.5vw));
  }
  @media (50em < inline-size) {
    result: max(var(--min), var(--l, 1.2em + 1vw));
  }
}

Miriam goes on to explain how a comma-separated list of parameters like this requires additional CSSWG work because it could be mistaken as a compound selector.

Mixins help maintain DRY, reusable style blocks

Mixins feel more familiar to me than custom functions. Years of writing Sass mixins will do that to you, and indeed, is perhaps the primary reason I still reach for Sass every now and then.

Mixins sorta look like the new custom functions. Instead of @function we’re working with @mixin which is exactly how it works in Sass.

/* Custom function */
@function <function-name> [( <parameter-list> )]? {
  <function-rules>
  result: <result>;
}

/* CSS/Sass mixin */
@mixin <mixin-name> [( <parameter-list> )]? {
  <mixin-rules>
}

So, custom functions and mixins are fairly similar but they’re certainly different:

  • Functions are defined with @function; mixins are defined with @mixin but are both named with a dashed ident (e.g. --name).
  • Functions result in a value; mixins result in style rules.

This makes mixins ideal for abstracting styles that you might use as utility classes, say a class for hidden text that is read by screenreaders:

.sr-text {
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

In true utility fashion, we can sprinkle this class on elements in the HTML to hide the text.

<a class="sr-text">Skip to main content</a>

Super handy! But as any Tailwind-hater will tell you, this can lead to ugly markup that’s difficult to interpret if we rely on many utility classes. Screereader text isn’t in too much danger of that, but a quick example from the Tailwind docs should illustrate that point:

<div class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">

It’s a matter of preference, really. But back to mixins! The deal is that we can use utility classes almost as little CSS snippets to build out other style rules and maintain a clearer separation between markup and styles. If we take the same .sr-text styles from before and mixin-erize them (yep, I’m coining this):

@mixin --sr-text {
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

Instead of jumping into HTML to apply the styles, we can embed them in other CSS style rules with a new @apply at-rule:

header a:first-child {
  @apply --sr-text;

  /* Results in: */
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

Perhaps a better example is something every project seems to need: centering something!

@mixin --center-me {
  display: grid;
  place-items: center;
}

This can now be part of a bigger ruleset:

header {
  @apply --center-me;
  /*
    display: grid;
    place-items: center;
  */

  background-color: --c-blue-50;
  color: --c-white;
  /* etc. */
}

That’s different from Sass which uses @include to call the mixin instead of @apply. We can even return larger blocks of styles, such as styles for an element’s ::before and ::after pseudos:

@mixin --center-me {
  display: grid;
  place-items: center;
  position: relative;

  &::after {
    background-color: hsl(25 100% 50% / .25);
    content: "";
    height: 100%;
    position: absolute;
    width: 100%;
  }
}

And, of course, we saw that mixins accept argument parameters just like custom functions. You might use arguments if you want to loosen up the styles for variations, such as defining consistent gradients with different colors:

@mixin --gradient-linear(--color-1, --color-2, --angle) {
  /* etc. */
}

We’re able to specify the syntax for each parameter as a form of type checking:

@mixin --gradient-linear(
  --color-1 type(color),
  --color-2 type(color),
  --angle type(angle),
) {
  /* etc. */
}

We can abstract those variables further and set default values on them:

@mixin --gradient-linear(
  --color-1 type(color),
  --color-2 type(color),
  --angle type(angle),
) {
  --from: var(--color-1, orangered);
  --to: var(--from-color, goldenrod);
  --angle: var(--at-angle, to bottom right);

  /* etc. */
}

…then we write the mixin’s style rules with the parameters as variable placeholders.

@mixin --gradient-linear(
  --color-1 type(color),
  --color-2 type(color),
  --angle type(angle),
) {
  --from: var(--color-1, orangered);
  --to: var(--from-color, goldenrod);
  --angle: var(--at-angle, to bottom right);

  background: linear-gradient(var(--angle), var(--from), var(--to));
}

Sprinkle conditional logic in there if you’d like:

@mixin --gradient-linear(
  --color-1 type(color),
  --color-2 type(color),
  --angle type(angle),
) {
  --from: var(--color-1, orangered);
  --to: var(--from-color, goldenrod);
  --angle: var(--at-angle, to bottom right);

  background: linear-gradient(var(--angle), var(--from), var(--to));

  @media (prefers-contrast: more) {
    background: color-mix(var(--from), black);
    color: white;
  }
}

This is all set to @apply the mixin in any rulesets we want:

header {
  @apply --gradient-linear;
  /* etc. */
}

.some-class {
  @apply --gradient-linear;
  /* etc. */
}

…and combine them with other mixins:

header {
  @apply --gradient-linear;
  @apply --center-me;
  /* etc. */
}

This is all very high level. Miriam gets into the nuances of things like:

  • Applying mixins at the root level (i.e., not in a selector)
  • Working with Container Queries with the limitation of having to set global custom properties on another element than the one that is queried.
  • The possibility of conditionally setting mixin parameters with something like @when/@else in the mixin. (Which makes me wonder about the newly-proposed if() function and whether it would be used in place of @when.)
  • Why we might draw a line at supporting loops the same way Sass does. (CSS is a declarative language and loops are imperative flows.)
  • Scoping mixins (@layer? scope? Something else?)

Miriam has an excellent outline of the open questions and discussions happening around mixins.

That’s, um, it… at least for now.

Gah, this is a lot for my blonde brain! Anytime I’m neck-deep in CSS specification drafts, I have to remind myself that the dust is still settling. The spec authors and editors are wrestling with a lot of the same questions we have — and more! — so it’s not like a cursory read of the drafts is going to make experts out of anyone. And that’s before we get to the fact that things can, and likely will, change by the time it all becomes a recommended feature for browsers to implement.

This will be an interesting space to watch, which is something you can do with the following resources:


CSS Functions and Mixins Module Notes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-functions-and-mixins-module-notes/feed/ 0 378995
Where You Can Still Get A Book Apart Titles https://css-tricks.com/where-you-can-still-get-a-book-apart-titles/ https://css-tricks.com/where-you-can-still-get-a-book-apart-titles/#comments Wed, 31 Jul 2024 14:52:44 +0000 https://css-tricks.com/?p=379392 It’s been a few months out since A Book Apart closed shop. I’m sad about it, of course. You probably are, too, if you have one of their many brightly-colored paperbacks sitting on a bookshelf strategically placed as a backdrop …


Where You Can Still Get A Book Apart Titles originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
It’s been a few months out since A Book Apart closed shop. I’m sad about it, of course. You probably are, too, if you have one of their many brightly-colored paperbacks sitting on a bookshelf strategically placed as a backdrop for your video calls.

It looked for a bit like the books would still be available for purchase through third-party distributors who could print them on demand or whatever. And then a redaction on A Book Apart’s original announcement:

UPDATE: Ownership and publishing rights for all books have been given back to their respective authors. Many authors are continuing to offer their work for free or in new editions. Our hope is that these books will continue to live on forever. A Book Apart no longer sells or distributes books, please reach out to authors for information about availability.

Oh, snap. The books are on the loose and several authors are making sure they’re still available. Eric Meyer, for example, says he and co-author Sara Wachter-Boettcher still figuring out what’s next for their Design for Real Life title:

One of the things Sara and I have decided to do is to eventually put the entire text online for free, as a booksite. That isn’t ready yet, but it should be coming somewhere down the road.

In the meantime, we’ve decided to cut the price of print and e-book copies available through Ingram. [Design for Real Life] was the eighteenth book [A Book Apart] put out, so we’ve decided to make the price of both the print and e-book $18, regardless of whether those dollars are American, Canadian, or Australian.

Ethan Marcotte has followed suit by listing his three titles on his personal website and linking up where they can be purchased at a generous discount off the original price tag, including his latest, You Deserve a Tech Union.

Others have quickly responded with free online versions of their books. Mat Marquis has offered JavaScript for Web Designers free online for a long time. He helped Chris Coyier do the same with Practical SVG this past week. Jeremy Keith put out one of my personal ABA faves (and the first ever ABA-published book) for free, HTML5 for Web Designers.

What about all the other titles? I dunno. A Book Apart simply doesn’t sell or distribute them anymore. Rachel McConnell sells Leading Content Design directly. Every other book I checked seems to be a link back to A Book Apart. We’ll have to see where the proverbial dust settles. The authors now hold all the rights to their works and may or may not decide to re-offer them. Meanwhile, many of the titles are listed in places like Goodreads, Amazon, Barnes & Noble, etc.

A couple of folks have even started tracking the books on their personal sites, like Ryan Trimble and Alan Dalton. (Thanks for the tip, Chris!)

Thanks for all the great reads and years, A Book Apart! You’ve helped man, many people become better web citizens, present company included.


Where You Can Still Get A Book Apart Titles originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/where-you-can-still-get-a-book-apart-titles/feed/ 3 379392
Smashing Hour With Dave Rupert https://css-tricks.com/smashing-hour-with-dave-rupert/ https://css-tricks.com/smashing-hour-with-dave-rupert/#respond Tue, 30 Jul 2024 14:16:49 +0000 https://css-tricks.com/?p=379381 Smashing Magazine invited me to sit down for a one-on-one with “Uncle” Dave Rupert to discuss web components, yes, but also check in on Dave’s new Microsoft gig and what the ShopTalk co-host is working on these days.

I first …


Smashing Hour With Dave Rupert originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Smashing Magazine invited me to sit down for a one-on-one with “Uncle” Dave Rupert to discuss web components, yes, but also check in on Dave’s new Microsoft gig and what the ShopTalk co-host is working on these days.

I first met Dave in 2015 when CSS Dev Conf took place in my backyard, Long Beach. It’s not like we’ve been in super close touch between then and now — we may have only chatted one-on-one like that a couple other times — but talking with Dave each time feels like hanging with a close friend ands this time was no different. Good, good vibes and web nerdery.

To Shared LinkPermalink on CSS-Tricks


Smashing Hour With Dave Rupert originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/smashing-hour-with-dave-rupert/feed/ 0 https://www.youtube.com/embed/-hXmRkM7dsQ Smashing Hour With Dave Rupert | CSS-Tricks nonadult 379381
Letter Spacing is Broken and There’s Nothing We Can Do About It… Maybe https://css-tricks.com/letter-spacing-is-broken-and-theres-nothing-we-can-do-about-it-maybe/ https://css-tricks.com/letter-spacing-is-broken-and-theres-nothing-we-can-do-about-it-maybe/#comments Mon, 29 Jul 2024 16:41:34 +0000 https://css-tricks.com/?p=379318 This post came up following a conversation I had with Emilio Cobos — a senior developer at Mozilla and member of the CSSWG — about the last CSSWG group meeting. I wanted to know what he thought were the …


Letter Spacing is Broken and There’s Nothing We Can Do About It… Maybe originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
This post came up following a conversation I had with Emilio Cobos — a senior developer at Mozilla and member of the CSSWG — about the last CSSWG group meeting. I wanted to know what he thought were the most exciting and interesting topics discussed at their last meeting, and with 2024 packed with so many new or coming flashy things like masonry layout, if() conditionals, anchor positioning, view transitions, and whatnot, I thought his answers had to be among them.

He admitted that my list of highlights was accurate on what is mainstream in the community, especially from an author’s point of view. However, and to my surprise, his favorite discussion was on something completely different: an inaccuracy on how the letter-spacing property is rendered across browsers. It’s a flaw so ingrained on the web that browsers have been ignoring the CSS specification for years and that can’t be easily solved by a lack of better options and compatibility issues.

Emilios’s answer makes sense — he works on Gecko and rendering fonts is an art in itself. Still, I didn’t get what the problem is exactly, why he finds it so interesting, and even why it exists in the first place since letter-spacing is a property as old as CSS. It wasn’t until I went into the letter-spacing rabbit hole that I understood how amazingly complex the issue gets and I hope to get you as interested as I did in this (not so) simple property.

What’s letter spacing?

The question seems simple: letter spacing is the space between letters. Hooray! That was easy, for humans. For a computer, the question of how to render the space between letters has a lot more nuance. A human just writes the next letter without putting in much thought. Computers, on the other hand, need a strategy on how to render that space: should they add the full space at the beginning of the letter, at the end, or halve it and add it on both sides of the letter? Should it work differently from left-to-right (LTR) languages, like English, to right-to-left (RTL) like Hebrew? These questions are crucial since choosing one as a standard shapes how text measurement and line breaks work across the web.

Which of the three strategies is used on the web? Depends on who you ask. The implementation in the CSS specifications completely differs from what the browsers do, and there is even incompatibility between browsers rendering engines, like Gecko (Firefox), Blink (Chrome, Brave, Opera, etc.), and WebKit (Safari).

What the CSS spec says

Let’s backpedal a bit and first know how the spec says letter spacing should work. At the time of writing, letter-spacing:

Specifies additional spacing between typographic character units. Values may be negative, but there may be implementation-dependent limits.

The formal specification has more juice to it, but this one gives us enough to understand how the CSS spec wants letter-spacing to behave. The keyword is between, meaning that the letter spacing should only affect the space between characters. I know, sounds pretty obvious.

So, as the example given on the spec, the following HTML:

<p>a<span>bb</span>c</p>

…with this CSS:

p {
  letter-spacing: 1em;
}

span {
  letter-spacing: 2em;
}

…should give an equal space between the two “b” letters:

Letter spacing on paper. The letter spacing is only applied between the letters "b"s

However, if we run the same code on any browser (e.g., Chrome, Firefox, or Safari), we’ll see the spacing isn’t contained between the “b” letters, but also at the end of the complete word.

Letter spacing on browsers. The letter spacing is applied between the letters "b"s and on the right-hand side of the last letter "b"

What browsers do

I thought it was normal for letter-spacing to attach spacing at the end of a character and didn’t know the spec said otherwise. However, if you think about it, the current behavior does seem off… it’s just that we’re simply used to it.

Why would browsers not follow the spec on this one?

As we saw before, letter spacing isn’t straightforward for computers since they must stick to a strategy for where spacing is applied. In the case of browsers, the standard has been to apply an individual space at the end of each character, ignoring if that space goes beyond the full word. It may have not been the best choice, but it’s what the web has leaned into, and changing it now would result in all kinds of text and layout shifts across the web.

This leaves a space at the end of elements with bigger letter spacing, which is somewhat acceptable for LTR text, but it leaves a hole at the beginning of the text in an RTL writing mode.

The issue is more obvious with centered text, where the ending space pushes the text away from the element’s dead center. You’ve probably had to add padding on the opposite side of an element to make up for any letter-spacing you’ve applied to the text at least one time, like on a button.

As you can see, the blue highlight creates a symmetrical pyramid which our text sadly doesn’t follow.

What’s worse, the “end of each character” means something different to browsers, particularly when working in an RTL writing mode. Chrome and Safari (Blink/WebKit) say the end of a character is always on the right-hand side. Firefox (Gecko), on the other hand, adds space to the “reading” end — which in Hebrew and Arabic is the left-hand side. See the difference yourself:

Side-by-side comparison of letter spacing on Gecko and Blink/Webkit

Can this be fixed?

The first thought that comes to mind is to simply follow what the spec says and trim the unnecessary space at the ending character, but this (anti) solution brings compatibility risks that are simply too big to even consider; text measurement and line breaks would change, possibly causing breakage on lots of websites. Pages that have removed that extra space with workarounds probably did it by offsetting the element’s padding/margin, which means changing the behavior as it currently stands makes those offsets obsolete or breaking.

There are two real options for how letter-spacing can be fixed: reworking how the space is distributed around the character or allowing developers an option to choose where we want the ending space.

Option 1: Reworking the space distribution

The first option would be to change the current letter-spacing definition so it says something like this:

Specifies additional spacing applied to each typographic character unit except those with zero advance. The additional spacing is divided equally between the inline-start and -end sides of the typographic character unit. Values may be negative, but there may be implementation-dependent limits.

Simply put, instead of browsers applying the additional space at the end of the character, they would divide it equally at the start and end, and the result is symmetrical text. This would also change text measurements and line breaks, albeit to a lesser degree.

Letter spacing with the symmetrical fix. The letter spacing is equally applied around the letters "b"s

Now text that is center-aligned text is correctly aligned to the center:

Different examples of letter spacing being distributed between letters and achieving a symmetrical look

Option 2: Allowing developers an option to choose

Even if the offset is halved, it could still bring breaking layout shifts to pages which to some is still (rightfully) unacceptable. It’s a dilemma: most pages need, or at least would benefit, from leaving letter-spacing as-is, while new pages would enjoy symmetrical letter spacing. Luckily, we could do both by giving developers the option to choose how the space is applied to characters. The syntax is anybody’s guess, but we could have a new property to choose where to place the spacing:

letter-spacing-justify: [ before | after | left | right | between | around];

Each value represents where the space should be added, taking into account the text direction:

  • before: the spacing is added at the beginning of the letter, following the direction of the language.
  • after: the spacing is added at the end of the letter, following the direction of the language.
  • left: the spacing is added at the left of the letter, ignoring the direction of the language.
  • right: the spacing is added at the right of the letter, ignoring the direction of the language.
  • between: the spacing is added between characters, following the spec.
  • around: the spacing is divided around the letter.

Logically, the current behavior would be the default to not break anything and letter-spacing would become a shorthand for both properties (length and placing).

letter-spacing: 1px before;
letter-spacing: 1px right;
letter-spacing: 1px around;

letter-spacing: 1px;
/* same as: */
letter-spacing: 1px before;

What about a third option?

And, of course, the third option is to leave things as they are. I’d say this is unlikely since the CSSWG resolved to take action on the issue, and they’ll probably choose the second option if I had to bet the nickel in my pocket on it.

Now you know letter-spacing is broken… and we have to live with it, at least for the time being. But there are options that may help correct the problem down the road.


Letter Spacing is Broken and There’s Nothing We Can Do About It… Maybe originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/letter-spacing-is-broken-and-theres-nothing-we-can-do-about-it-maybe/feed/ 6 379318
Pop(over) the Balloons https://css-tricks.com/popover-the-balloons/ https://css-tricks.com/popover-the-balloons/#comments Thu, 25 Jul 2024 13:31:46 +0000 https://css-tricks.com/?p=379281 I’ve always been fascinated with how much we can do with just HTML and CSS. The new interactive features of the Popover API are yet another example of just how far we can get with those two languages alone.

You …


Pop(over) the Balloons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I’ve always been fascinated with how much we can do with just HTML and CSS. The new interactive features of the Popover API are yet another example of just how far we can get with those two languages alone.

You may have seen other tutorials out there showing off what the Popover API can do, but this is more of a beating-it-mercilessly-into-submission kind of article. We’ll add a little more pop music to the mix, like with balloons… some literal “pop” if you will.

What I’ve done is make a game — using only HTML and CSS, of course — leaning on the Popover API. You’re tasked with popping as many balloons as possible in under a minute. But be careful! Some balloons are (as Gollum would say) “tricksy” and trigger more balloons.

I have cleverly called it Pop(over) the Balloons and we’re going to make it together, step by step. When we’re done it’ll look something like (OK, exactly like) this:

Handling the popover attribute

Any element can be a popover as long as we fashion it with the popover attribute:

<div popover>...</div>

We don’t even have to supply popover with a value. By default, popover‘s initial value is auto and uses what the spec calls “light dismiss.” That means the popover can be closed by clicking anywhere outside of it. And when the popover opens, unless they are nested, any other popovers on the page close. Auto popovers are interdependent like that.

The other option is to set popover to a manual value:

<div popover=“manual”>...</div>

…which means that the element is manually opened and closed — we literally have to click a specific button to open and close it. In other words, manual creates an ornery popup that only closes when you hit the correct button and is completely independent of other popovers on the page.

Using the <details> element as a starter

One of the challenges of building a game with the Popover API is that you can’t load a page with a popover already open… and there’s no getting around that with JavaScript if our goal is to build the game with only HTML and CSS.

Enter the <details> element. Unlike a popover, the <details> element can be open by default:

<details open>
  <!-- rest of the game -->
</details>

If we pursue this route, we’re able to show a bunch of buttons (balloons) and “pop” all of them down to the very last balloon by closing the <details>. In other words, we can plop our starting balloons in an open <details> element so they are displayed on the page on load.

This is the basic structure I’m talking about:

<details open>
  <summary>🎈</summary>
  <button>🎈</button>
  <button>🎈</button>
  <button>🎈</button>
</details>

In this way, we can click on the balloon in <summary> to close the <details> and “pop” all of the button balloons, leaving us with one balloon (the <summary> at the end (which we’ll solve how to remove a little later).

You might think that <dialog> would be a more semantic direction for our game, and you’d be right. But there are two downsides with <dialog> that won’t let us use it here:

  1. The only way to close a <dialog> that’s open on page load is with JavaScript. As far as I know, there isn’t a close <button> we can drop in the game that will close a <dialog> that’s open on load.
  2. <dialog>s are modal and prevent clicking on other things while they’re open. We need to allow gamers to pop balloons outside of the <dialog> in order to beat the timer.

Thus we will be using a <details open> element as the game’s top-level container and using a plain ol’ <div> for the popups themselves, i.e. <div popover>.

All we need to do for the time being is make sure all of these popovers and buttons are wired together so that clicking a button opens a popover. You’ve probably learned this already from other tutorials, but we need to tell the popover element that there is a button it needs to respond to, and then tell the button that there is a popup it needs to open. For that, we give the popover element a unique ID (as all IDs should be) and then reference it on the <button> with a popovertarget attribute:

<!-- Level 0 is open by default -->
<details open>
  <summary>🎈</summary>
  <button popovertarget="lvl1">🎈</button>
</details>

<!-- Level 1 -->
<div id="lvl1" popover="manual">
  <h2>Level 1 Popup</h2>
</div>

This is the idea when everything is wired together:

Opening and closing popovers

There’s a little more work to do in that last demo. One of the downsides to the game thus far is that clicking the <button> of a popup opens more popups; click that same <button> again and they disappear. This makes the game too easy.

We can separate the opening and closing behavior by setting the popovertargetaction attribute (no, the HTML spec authors were not concerned with brevity) on the <button>. If we set the attribute value to either show or hide, the <button> will only perform that one action for that specific popover.

<!-- Level 0 is open by default -->
<details open>
  <summary>🎈</summary>
  <!-- Show Level 1 Popup -->
  <button popovertarget="lvl1" popovertargetaction="show">🎈</button>
  <!-- Hide Level 1 Popup -->
  <button popovertarget="lvl1" popovertargetaction="hide">🎈</button>
</details>

<!-- Level 1 -->
<div id="lvl1" popover="manual">
  <h2>Level 1 Popup</h2>
  <!-- Open/Close Level 2 Poppup -->
  <button popovertarget="lvl2">🎈</button>
</div>

<!-- etc. -->

Note, that I’ve added a new <button> inside the <div> that is set to target another <div> to pop open or close by intentionally not setting the popovertargetaction attribute on it. See how challenging (in a good way) it is to “pop” the elements:

Styling balloons

Now we need to style the <summary> and <button> elements the same so that a player cannot tell which is which. Note that I said <summary> and not <details>. That’s because <summary> is the actual element we click to open and close the <details> container.

Most of this is pretty standard CSS work: setting backgrounds, padding, margin, sizing, borders, etc. But there are a couple of important, not necessarily intuitive, things to include.

  • First, there’s setting the list-style-type property to none on the <summary> element to get rid of the triangular marker that indicates whether the <details> is open or closed. That marker is really useful and great to have by default, but for a game like this, it would be better to remove that hint for a better challenge.
  • Safari doesn’t like that same approach. To remove the <details> marker here, we need to set a special vendor-prefixed pseudo-element, summary::-webkit-details-marker to display: none.
  • It’d be good if the mouse cursor indicated that the balloons are clickable, so we can set cursor: pointer on the <summary> elements as well.
  • One last detail is setting the user-select property to none on the <summary>s to prevent the balloons — which are simply emoji text — from being selected. This makes them more like objects on the page.
  • And yes, it’s 2024 and we still need that prefixed -webkit-user-select property to account for Safari support. Thanks, Apple.

Putting all of that in code on a .balloon class we’ll use for the <button> and <summary> elements:

.balloon {
  background-color: transparent;
  border: none;
  cursor: pointer;
  display: block;
  font-size: 4em;
  height: 1em;
  list-style-type: none;
  margin: 0;
  padding: 0;
  text-align: center;
  -webkit-user-select: none; /* Safari fallback */
  user-select: none;
  width: 1em;
}

One problem with the balloons is that some of them are intentionally doing nothing at all. That’s because the popovers they close are not open. The player might think they didn’t click/tap that particular balloon or that the game is broken, so let’s add a little scaling while the balloon is in its :active state of clicking:

.balloon:active {
  scale: 0.7;
  transition: 0.5s;
}

Bonus: Because the cursor is a hand pointing its index finger, clicking a balloon sort of looks like the hand is poking the balloon with the finger. 👉🎈💥

The way we distribute the balloons around the screen is another important thing to consider. We’re unable to position them randomly without JavaScript so that’s out. I tried a bunch of things, like making up my own “random” numbers defined as custom properties that can be used as multipliers, but I couldn’t get the overall result to feel all that “random” without overlapping balloons or establishing some sort of visual pattern.

I ultimately landed on a method that uses a class to position the balloons in different rows and columns — not like CSS Grid or Multicolumns, but imaginary rows and columns based on physical insets. It’ll look a bit Grid-like and is less “randomness” than I want, but as long as none of the balloons have the same two classes, they won’t overlap each other.

I decided on an 8×8 grid but left the first “row” and “column” empty so the balloons are clear of the browser’s left and top edges.

/* Rows */
.r1 { --row: 1; }
.r2 { --row: 2; }
/* all the way up to .r7 */

/* Columns */
.c1 { --col: 1; }
.c2 { --col: 2; }
/* all the way up to .c7 */

.balloon {
  /* This is how they're placed using the rows and columns */
  top: calc(12.5vh * (var(--row) + 1) - 12.5vh);
  left: calc(12.5vw * (var(--col) + 1) - 12.5vw);
}

Congratulating The Player (Or Not)

We have most of the game pieces in place, but it’d be great to have some sort of victory dance popover to congratulate players when they successfully pop all of the balloons in time.

Everything goes back to a <details open> element. Once that element is not open, the game should be over with the last step being to pop that final balloon. So, if we give that element an ID of, say, #root, we could create a condition to hide it with display: none when it is :not() in an open state:

#root:not([open]) {
  display: none;
}

This is where it’s great that we have the :has() pseudo-selector because we can use it to select the #root element’s parent element so that when #root is closed we can select a child of that parent — a new element with an ID of #congrats — to display a faux popover displaying the congratulatory message to the player. (Yes, I’m aware of the irony.)

#game:has(#root:not([open])) #congrats {
  display: flex;
}

If we were to play the game at this point, we could receive the victory message without popping all the balloons. Again, manual popovers won’t close unless the correct button is clicked — even if we close its ancestral <details> element.

Is there a way within CSS to know that a popover is still open? Yes, enter the :popover-open pseudo-class.

The :popover-open pseudo-class selects an open popover. We can use it in combination with :has() from earlier to prevent the message from showing up if a popover is still open on the page. Here’s what it looks like to chain these things together to work like an and conditional statement.

/* If #game does *not* have an open #root 
 * but has an element with an open popover 
 * (i.e. the game isn't over),
 * then select the #congrats element...
 */
#game:has(#root:not([open])):has(:popover-open) #congrats {
  /* ...and hide it */
  display: none;
}

Now, the player is only congratulated when they actually, you know, win.

Conversely, if a player is unable to pop all of the balloons before a timer expires, we ought to inform the player that the game is over. Since we don’t have an if() conditional statement in CSS (not yet, at least) we’ll run an animation for one minute so that this message fades in to end the game.

#fail {
  animation: fadein 0.5s forwards 60s;
  display: flex;
  opacity: 0;
  z-index: -1;
}

@keyframes fadein {
  0% {
    opacity: 0;
    z-index: -1;
  }
  100% {
    opacity: 1;
    z-index: 10;
  }
}

But we don’t want the fail message to trigger if the victory screen is showing, so we can write a selector that prevents the #fail message from displaying at the same time as #congrats message.

#game:has(#root:not([open])) #fail {
  display: none;
}

We need a game timer

A player should know how much time they have to pop all of the balloons. We can create a rather “simple” timer with an element that takes up the screen’s full width (100vw), scaling it in the horizontal direction, then matching it up with the animation above that allows the #fail message to fade in.

#timer {
  width: 100vw;
  height: 1em;
}

#bar {
  animation: 60s timebar forwards;
  background-color: #e60b0b;
  width: 100vw;
  height: 1em;
  transform-origin: right;
}

@keyframes timebar {
  0% {
    scale: 1 1;
  }
  100% {
    scale: 0 1;
  }
}

Having just one point of failure can make the game a little too easy, so let’s try adding a second <details> element with a second “root” ID, #root2. Once more, we can use :has to check that neither the #root nor #root2 elements are open before displaying the #congrats message.

#game:has(#root:not([open])):has(#root2:not([open])) #congrats {
  display: flex;
}

Wrapping up

The only thing left to do is play the game!

Fun, right? I’m sure we could have built something more robust without the self-imposed limitation of a JavaScript-free approach, and it’s not like we gave this a good-faith accessibility pass, but pushing an API to the limit is both fun and educational, right?


I’m interested: What other wacky ideas can you think up for using popovers? Maybe you have another game in mind, some slick UI effect, or some clever way of combining popovers with other emerging CSS features, like anchor positioning. Whatever it is, please share!


Pop(over) the Balloons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/popover-the-balloons/feed/ 2 379281
Alvaro Montoro: CSS One-Liners to Improve (Almost) Every Project https://css-tricks.com/alvaro-montoro-css-one-liners-to-improve-almost-every-project/ https://css-tricks.com/alvaro-montoro-css-one-liners-to-improve-almost-every-project/#comments Mon, 22 Jul 2024 14:38:52 +0000 https://css-tricks.com/?p=379278 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 …


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.

]]>
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. 😝

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.

]]>
https://css-tricks.com/alvaro-montoro-css-one-liners-to-improve-almost-every-project/feed/ 3 379278
CSS Stuff I’m Excited After the Last CSSWG Meeting https://css-tricks.com/css-stuff-im-excited-after-the-last-csswg-meeting/ https://css-tricks.com/css-stuff-im-excited-after-the-last-csswg-meeting/#comments Fri, 19 Jul 2024 17:16:57 +0000 https://css-tricks.com/?p=379180 From June 11–13, the CSS Working Group (CSSWG) held its second face-to-face meeting of the year in Coruña, Spain, with a long agenda of new features and improvements coming to language. If 2023 brought us incredible advances like …


CSS Stuff I’m Excited After the Last CSSWG Meeting originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
From , the CSS Working Group (CSSWG) held its second face-to-face meeting of the year in Coruña, Spain, with a long agenda of new features and improvements coming to language. If 2023 brought us incredible advances like out-of-the-box nesting, container and style queries, or the has: selector, then 2024 is going to be even more packed with even more ground-breaking additions. Whether a new feature like inline conditionals is just starting or long-term projects are wrapping up, 2024 is already filled with exciting developments — and we’re still in July!

I wanted to share what I think are some of the most interesting and significant features coming to CSS that were examined in the meeting. However, I don’t want you to take the following as an exact recap of the discussions. Instead, I want to bring up the broader topics coming to CSS that had a spotlight at the last meeting. In reality, the features examined have been cooking up for even years and the discussions are geared towards specific cases and new enhancements, rather than defining a whole specification; a work that would be impossible in one meeting.

You can see the exact issues discussed on the CSSWG meeting agenda.

Feature 1: What if we get if()?

Since CSS custom properties gained reliable support around 2016, there have been many attempts to apply certain styles depending on a custom property value without, of course, appealing to JavaScript. One of the earliest workarounds for conditional styles was posted by Roman Komarov back in 2016 in “Conditions for CSS Variables”. From there, many other hacks have been documented for making conditional declarations in CSS (including this extremely clever one by Ana Tudor here on CSS-Tricks). In fact, you can find a full list that discusses and compares those workarounds by CSSWG member Lea Verou in her recent article, “Inline conditionals in CSS, now?”.

What’s for sure is that the community has craved a conditional way to apply styles using custom properties. Nowadays, we have a specification for Style Queries that’s capable of the task, but they come with limitations not related to browser support. The biggest of those limitations? We can’t directly style the container that’s queried, so we need some sort of wrapper element around that wrapper in HTML.

<div class="news-container" style="--variant: info">
  <p>Here is some good <strong>news</strong></p>
</div>

…in addition to writing the style query:

.news-container {
  container-name: news-container;
}

@container news-container style(--variant: info) {
  p {
    color: blue;
    border: 1px solid blue;
  }
}

What if() might look like

On the CSSWG side, there have been discussions about adding an if() function as far back as 2018. It was of this year — yes, six years later — that the CSSWG resolved to begin working on if() for CSS. As good as it may look, don’t expect to see if() in a browser in at least two years! (That’s Lea’s unofficial estimate.) We’ll likely need to wait even longer for enough browser support to begin using it reliably in production. The spec draft is only barely getting started and many things have to pass a test first. For context, the CSS variables working draft began in 2012 and only received wide browser support in 2016.

Syntax-wise, if() is probably going to borrow the ternary operator from JavaScript and other programming languages, structured like this:

if(a ? b : c)

…where a is the custom property we are checking and b are c are the possible conditional return values. To check for styles, an inline style(--my-property: value) would be used.

.forecast {
  background-color: if(style(--weather: clouds) ? var(--clouds-color): var(--default-color));
}

Even if ? isn’t used in CSS and : has a different meaning everywhere else, I think this syntax is the one most people are familiar with, not to mention it also allows seamless conditional chaining.

.forecast {
  background-color: if(
    style(--weather: clouds) ? var(--clouds-color): 
    style(--weather: sunny) ? var(--sunny-color);
    style( --weather: rain) ? var(--rain-color): var(--default-color)
  );
}

Future if() improvements

Although these probably won’t make it in the initial release, it’s interesting to see how if() might change between now and sometime further in the future:

  • Support for other inline conditionals. We are supposed to check for custom properties using the style() query, but we may as well check for media features with an inline media() query or if a user agent supports a specific property with an inline support().
.my-element {
  width: if(media(width > 1200px) ? var(--size-l): var(--size-m));
}
  • Using conditional inside other CSS functions. In future drafts, we may use ternaries inside other functions without having to wrap them around if(), e.g. just as we can make calculations without calc() if we are inside a clamp() or round() function.

Feature 2: Cross-document view transitions

Last year, the View Transition API gave us the power to create seamless transitions when navigating between web pages and states. No components or frameworks, no animation libraries — just vanilla HTML and CSS with a light sprinkle of JavaScript. The first implementation of View Transitions was baked into browsers a while back, but it was based on an experimental function defined by Chrome and was limited to transitions between two states (single-page view transitions) without support for transitioning between different pages (i.e., multi-page view transitions), which is what most of us developers are clamoring for. The possibilities for mimicking the behavior of native apps are exciting!

That’s why the CSS View Transitions Module Level 2 is so amazing and why it’s my favorite of all the CSS additions we’re covering in this article. Yes, the feature brings out-of-the-box seamless transitions between pages, but the real deal is it removes the need for a framework to achieve it. Instead of using a library — say React + some routing library — we can backtrack into plain CSS and JavaScript.

Of course, there are levels of complexity where the View Transition API may fall short, but it’s great for countless cases where we just want page transitions without the performance cost of dropping in a framework.

Opting into view transitions

View transitions are triggered when we navigate between two pages from the same-origin. In this context, navigation might be clicking a link, submitting a form, or going back and forth with browser buttons. By contrast, something like using a search bar between same-origin pages won’t trigger a page transition.

Both pages — the one we’re navigating away from and the one we’re navigating to — need to opt into the transition using the @view-transition at-rule and setting the navigation property to auto

@view-transition {
  navigation: auto;
}

When both pages opt into a transition, the browser takes a “snapshot” of both pages and smoothly fades the “before” page into the “after” page.

Transitioning between “snapshots”

In that video, you can see how the old page fades into the new page, and it works thanks to an entire tree of new pseudo-elements that persist through the transition and use CSS animations to produce the effect. The browser will group snapshots of elements with a unique view-transition-name property that sets a unique identifier on the transition that we can reference, and which is captured in the ::view-transition pseudo-element holding all of the transitions on the page.

You can think of ::view-transition as the :root element for all page transitions, grouping all of the parts of a view transition on the same default animation.

::view-transition
├─ ::view-transition-group(name)
│  └─ ::view-transition-image-pair(name)
│     ├─ ::view-transition-old(name)
│     └─ ::view-transition-new(name)
├─ ::view-transition-group(name)
│  └─ ::view-transition-image-pair(name)
│     ├─ ::view-transition-old(name)
│     └─ ::view-transition-new(name)
└─ /* and so one... */

Notice that each transition lives in a ::view-transition-group that holds a ::view-transition-image-pair that, in turn, consists of the “old” and “new” page snapshots. We can have as many groups in there as we want, and they all contain an image pair with both snapshots.

Quick example: let’s use the ::view-transition “root” as a parameter to select all of the transitions on the page and create a sliding animation between the old and new snapshots.

@keyframes slide-from-right {
  from {
    transform: translateX(100vw);
  }
}

@keyframes slide-to-left {
  to {
    transform: translateX(-100vw);
  }
}

::view-transition-old(root) {
  animation: 300ms ease-in both slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both slide-from-right;
}

If we navigate between pages, the entire old page slides out to the left while the entire new page slides in from the right. But we may want to prevent some elements on the page from participating in the transition, where they persist between pages while everything else moves from the “old” snapshot to the “new” one.

That’s where the view-transition-name property is key because we can take snapshots of certain elements and put them in their own ::view-transition-group apart from everything else so that it is treated individually.

nav {
  view-transition-name: navigation;

  /* 
    ::view-transition
    ├─ ::view-transition-group(navigation)
    │  └─ ::view-transition-image-pair(navigation)
    │     ├─ ::view-transition-old(navigation)
    │     └─ ::view-transition-new(navigation)
    └─ other groups...
  */
}

You can find a live demo of it on GitHub. Just note that browser support is limited to Chromium browsers (i.e., Chrome, Edge, Opera) at the time I’m writing this.

There are many things we can look forward to with cross-document view transitions. For example, If we have several elements with a different view-transition-name, we could give them a shared view-transition-class to style their animations in one place — or even customize the view transitions further with JavaScript to check from which URL the page is transitioning and animate accordingly.

Feature 3: Anchor Positioning

Positioning an element relative to another element in CSS seems like one of those no-brainer, straightforward things, but in reality requires mingling with inset properties (top, bottom, left, right) based on a series of magic numbers to get things just right. For example, getting a little tooltip that pops in at the left of an element when hovered might look something like this in HTML:

<p class="text">
  Hover for a surprise
  <span class="tooltip">Surprise! I'm a tooltip</span>
</p>

…and in CSS with current approaches:

.text {
  position: relative;
}


.tooltip {
  position: absolute;
  display: none;

  /* vertical center */
  top: 50%;
  transform: translateY(-50%);

  /* move to the left */
  right: 100%;
  margin-right: 15px; */
}

.text:hover .tooltip {
  display: block;
}

Having to change the element’s positioning and inset values isn’t the end of the world, but it sure feels like there should be an easier way. Besides, the tooltip in that last example is extremely fragile; if the screen is too small or our element is too far to the left, then the tooltip will hide or overflow beyond the edge of the screen.

CSS Anchor Positioning is yet another new feature that was discussed in the CSSWG meetings and it promises to make this sort of thing much, much easier.

Creating an anchor

The basic idea is that we establish two elements:

  • one that acts as an anchor, and
  • one that is a “target” anchored to that element.

This way, we have a more declarative way to associate one element and position it relative to the anchored element.

To begin we need to create our anchor element using a new anchor-name property.

Changing our markup a little:

<p>
  <span class="anchor">Hover for a surprise</span>
  <span class="tooltip">Surprise! I'm a tooltip</span>
</p>

We give it a unique dashed-indent as its value (just like a custom property):

.anchor {
  anchor-name: --tooltip;
}

Then we relate the .tooltip to the .anchor using the position-anchor property with either fixed or absolute positioning.

.toolip {
  position: fixed;
  position-anchor: --tooltip;
}

The .tooltip is currently positioned on top of the .anchor, but we ought to move it somewhere else to prevent that. The easiest way to move the .tooltip is using a new inset-area property. Let’s imagine that the .anchor is placed in the middle of a 3×3 grid and we can position the tooltip inside the grid by assigning it a row and column.

Three by three grid with yellow element in the center tile labeled 'anchor'.

The inset-area property takes two values for the .tooltip‘s in a specific row and column on the grid. It counts with physical values, like left, right, top and bottom, as well logical values depending on the user’s writing mode, like start and end, in addition to a center shared value. It also accepts values referencing x- and y-coordinates, like x-start and y-end. All these value types are ways of representing a space on the 3×3 grid.

For example, if we want the .tooltip to be positioned relative to the top-right edge of the anchor, we can set the inset-area property like this:

.toolip {
  /* physical values */
  inset-area: top right;

  /* logical values */
  inset-area: start end;

  /* mix-n-match values! */
  inset-area: top end;
}

Lastly, if we want our tooltip to span across two regions of the grid, we can use a span- prefix. For example, span-top will place the .tooltip in the grid’s top and center regions. If instead we want to span across an entire direction, we can use the span-all value.

Three by three grid with a yellow element in the center labeled 'anchor' surrounded by three tooltip examples demonstrating how tooltips are placed on the grid, including code examples for each example.

One of the problems with our anchor-less example is that the tooltip can overflow outside the screen. We can solve this using another new property, this time called position-try-options, in combination with a new inset-area() function.

(Yes, there is inset-area the property and inset-area() the function. That’s one we’ll have to commit to memory!)

The position-try-options property accepts a comma-separated list of fallback positions for the .tooltip when it overflows outside the screen. We can provide a list of inset-area() functions, each holding the same values that the inset-area property would. Now, each time the tooltip goes out off-screen, the next declared position is “tried”, and if that position causes an overflow, the next declared position is tried, and so on.

.toolip {
  inset-area: top left;
  position-try-options: inset-area(top), inset-area(top right);
}

This is a pretty wild concept that will take some time to grok. CSSWG member Miriam Suzanne sat down to discuss and tinker with anchor positioning with James Stuckey Weber in a video that’s well worth watching.

Geoff Graham took notes on the video if you’re looking for a TL;DW.

There are still many aspects to anchor positioning we aren’t covering here for brevity, notably the new anchor() function and @try-position at-rule. The anchor() function returns the computed position of the edge of an anchor, which provides more control over a tooltip’s inset properties. The @try-position at-rule is for defining custom positions to set on the position-try-options property.

My hunch is that using inset-area will be plenty robust for the vast majority of use cases.

The CSSWG is a collective effort

Earlier I said that this article wouldn’t be an exact retelling of the discussions that took place at the CSSWG meetings, but rather a broad representation of new specs coming to CSS that, due to their novelty, were bound to come up in those meetings. There are even some features that we simply hadn’t the time to review in this roundup that are still subject to debate (cough, masonry).

One thing is for sure: specs aren’t made in some vacuum over one or two meetings; it takes the joined effort of tens of amazing authors, developers, and user agents to bring to life what we use every day in our CSS work — not to mention the things we will use in the future.

I also had the opportunity to talk with some amazing developers from the CSSWG, and I found it interesting what their biggest takeaways were from the meetings. You might expect if() is at the top of their lists since that’s what is buzzing in socials. But CSSWG member Emilio Cobos told me, for example, that the letter-spacing property is essentially flawed and there isn’t a simple solution for fixing it that’s copasetic with how letter-spacing is currently defined by CSS and used in browsers. That includes the fact that converting normal properties into shorthand properties can be dangerous to a codebase.

Every tiny detail we might think of as trivial is carefully analyzed for the sake of the web and for the love of it. And, like I mentioned earlier, this stuff is not happening in a closed vacuum. If you’re at all interested in the future of CSS — whether that simply keeping up with it or getting actively involved — then consider any of the following resources.


CSS Stuff I’m Excited After the Last CSSWG Meeting originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-stuff-im-excited-after-the-last-csswg-meeting/feed/ 4 https://www.youtube.com/embed/76hIB2L_vs4 CSS Anchor Positioning in Practice - Winging It Live nonadult 379180
Sara Joy: Everybody’s Free (To Write Websites) https://css-tricks.com/sara-joy-everybodys-free-to-write-websites/ https://css-tricks.com/sara-joy-everybodys-free-to-write-websites/#respond Wed, 17 Jul 2024 18:36:53 +0000 https://css-tricks.com/?p=379199 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 …


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.

]]>
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.

]]>
https://css-tricks.com/sara-joy-everybodys-free-to-write-websites/feed/ 0 https://www.youtube.com/embed/v5UsuZ4DS_Q Everybody's Free (to Make Websites) nonadult 379199
CSS Selectors https://css-tricks.com/css-selectors/ https://css-tricks.com/css-selectors/#comments Mon, 15 Jul 2024 16:13:15 +0000 https://css-tricks.com/?p=378745 A complete guide covering all of the various methods we have to select elements in CSS and how to use them for applying styles.


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

]]>

Overview

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.

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!

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.

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:

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.

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.

Showing element, class, and ID selectors in a horizontal row from least specific to most specific.

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.

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.

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.

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.

<!-- 👎 No match -->
<a href="https://CSS-Tricks.com">...</a>

<!-- 👍 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.

<!-- 👍 I match! -->
<a href="https://CSS-Tricks.com">...</a>

<!-- 👍 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.

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! 💥 */
* {
  /* 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.

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.


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! 🤗 */
}

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.

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.

Desktop

ChromeFirefoxIEEdgeSafari
120117No12017.2

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
12712712717.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! 👎 */
  article.article-element {
    /* Container styles */  

    /* Nope! 👎 */
    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.

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.


Combinators

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:

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

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

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

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:

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


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.

]]>
https://css-tricks.com/css-selectors/feed/ 2 378745
“If” CSS Gets Inline Conditionals https://css-tricks.com/if-css-gets-inline-conditionals/ https://css-tricks.com/if-css-gets-inline-conditionals/#comments Tue, 09 Jul 2024 15:18:11 +0000 https://css-tricks.com/?p=379002 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 …


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

]]>
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:

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.

Syntax

It boils down to this:

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

…where:

  • 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.

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 valuesProsCons
Binary Linear InterpolationNumbersQuantitativeCan be used as part of a valueLimited output range
Togglesvar(--alias) (actual values are too weird to expose raw)AnyCan be used in part of a valueWeird values that need to be aliased
Paused animationsNumbersAnyNormal, decoupled declarationsTakes over animation property

Cascade weirdness
Type 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! 🎂

References

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.

]]>
https://css-tricks.com/if-css-gets-inline-conditionals/feed/ 9 379002
Transitioning to Auto Height https://css-tricks.com/transitioning-to-auto-height/ https://css-tricks.com/transitioning-to-auto-height/#comments Fri, 28 Jun 2024 13:44:01 +0000 https://css-tricks.com/?p=378862 I know this is something Chris has wanted forever, so it’s no surprise he’s already got a fantastic write-up just a day after the news broke. In fact, I first learned about it from his post and was unable …


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

]]>
I know this is something Chris has wanted forever, so it’s no surprise he’s already got a fantastic write-up just a day after the news broke. In fact, I first learned about it from his post and was unable to dredge up any sort of announcement. So, I thought I’d jot some notes down because it feels like a significant development.

The news: transitioning to auto is now a thing! Well, it’s going to be a thing. Chrome Canary recently shipped support for it and that’s the only place you’ll find it for now. And even then, we just don’t know if the Chrome Canary implementation will find its way to the syntax when the feature becomes official.

The problem

Here’s the situation. You have an element. You’ve marked it up, plopped in contents, and applied a bunch of styles to it. Do you know how tall it is? Of course not! Sure, we can ask JavaScript to evaluate the element for us, but as far as CSS is concerned, the element’s computed dimensions are unknown.

That makes it difficult to, say, animate that element from height: 0 to height: whatever. We need to know what “whatever” is and we can only do that by setting a fixed height on the element. That way, we have numbers to transition from zero height to that specific height.

.panel {
  height: 0;
  transition: height 0.25s ease-in;

  &.expanded {
    height: 300px;
  }
}

But what happens if that element changes over time? Maybe the font changes, we add padding, more content is inserted… anything that changes the dimensions. We likely need to update that height: 300px to whatever new fixed height works best. This is why we often see JavaScript used to toggle things that expand and contract in size, among other workarounds.

I say this is about the height property, but we’re also talking about the logical equivalent, block-size, as well as width and inline-size. Or any direction for that matter!

Transitioning to auto

That’s the goal, right? We tend to reach for height: auto when the height dimension is unknown. From there, we let JavaScript calculate what that evaluates to and take things from there.

The current Chrome implementation uses CSS calc-size() to do the heavy lifting. It recognizes the auto keyword and, true to its name, calculates that number. In other words, we can do this instead of the fixed-height approach:

.panel {
  height: 0;
  transition: height 0.25s ease-in;

  &.expanded {
    height: calc-size(auto);
  }
}

That’s really it! Of course, calc-size() is capable of more complex expressions but the fact that we can supply it with just a vague keyword about an element’s height is darn impressive. It’s what allows us to go from a fixed value to the element’s intrinsic size and back.

I had to give it a try. I’m sure there are a ton of use cases here, but I went with a floating button in a calendar component that indicates a certain number of pending calendar invites. Click the button, and a panel expands above the calendar and reveals the invites. Click it again and the panel goes back to where it came from. JavaScript is handling the click interaction, triggering a class change that transitions the height in CSS.

A video in case you don’t feel like opening Canary:

This is the relevant CSS:

.invite-panel {
  height: 0;
  overflow-y: clip;
  transition: height 0.25s ease-in;
}

On click, JavaScript sets auto height on the element as an inline style to override the CSS:

<div class="invite-panel" style="height: calc(auto)">

The transition property in CSS lets the browser know that we plan on changing the height property at some point, and to make it smooth. And, as with any transition or animation, it’s a good idea to account for motion sensitivities by slowing down or removing the motion with prefers-reduced-motion.

What about display: none?

This is one of the first questions that popped into my head when I read Chris’s post and he gets into that as well. Transitioning from an element from display: none to its intrinsic size is sort of like going from height: 0. It might seem like a non-displayed element has zero height, but it actually does have a computed height of auto unless a specific height is declared on it.

DevTools showing computed values for an element with display none. The height value shows as auto.

So, there’s extra work to do if we want to transition from display: none in CSS. I’ll simply plop in the code Chris shared because it nicely demonstrates the key parts:

.element {
  /* hard mode!! */
  display: none;

  transition: height 0.2s ease-in-out;
  transition-behavior: allow-discrete;

  height: 0; 
  @starting-style {
    height: 0;
  }

  &.open {
    height: calc(auto);
  }
}
  • The element starts with both display: none and height: 0.
  • There’s an .open class that sets the element’s height to calc-size(auto).

Those are the two dots we need to connect and we do it by first setting transition-behavior: allow-discrete on the element. This is new to me, but the spec says that transition-behavior “specifies whether transitions will be started or not for discrete properties.” And when we declare allow-discrete, “transitions will be started for discrete properties as well as interpolable properties.”

Well, DevTools showed us right there that height: auto is a discrete property! Notice the @starting-style declaration, though. If you’re unfamiliar with it, you’re not alone. The idea is that it lets us set a style for a transition to “start” with. And since our element’s discrete height is auto, we need to tell the transition to start at height: 0 instead:

.element {
  /* etc. */

  @starting-style {
    height: 0;
  }
}

Now, we can move from zero to auto since we’re sorta overriding the discrete height with @starting-style. Pretty cool we can do that!

To Shared LinkPermalink on CSS-Tricks


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

]]>
https://css-tricks.com/transitioning-to-auto-height/feed/ 8 378862
Poppin’ In https://css-tricks.com/poppin-in/ https://css-tricks.com/poppin-in/#comments Wed, 26 Jun 2024 16:37:05 +0000 https://css-tricks.com/?p=378637 Oh, hey there! It’s been a hot minute, hasn’t it? Thought I’d pop in and say hello while we get to know the Popover API a bit.


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

]]>
Oh, hey there! It’s been a hot minute, hasn’t it? Thought I’d pop in and say hello. 👋

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:

<div>👋</div>

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.

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

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.

DevTools inspector showing the computed values for an element with the popover attribute.
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">👋</div>

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

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 👎 */
#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.

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.

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.

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.

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?

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.

]]>
https://css-tricks.com/poppin-in/feed/ 2 378637
https://css-tricks.com/378960-2/ https://css-tricks.com/378960-2/#respond Mon, 24 Jun 2024 18:28:26 +0000 https://css-tricks.com/?p=378960 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.

]]>
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.

]]>
https://css-tricks.com/378960-2/feed/ 0 378960