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.
]]>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 anhref
). 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 Link — Permalink 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.
]]>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.
]]>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.
<webui-disclosure>
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.
<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.
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.
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.
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.
<webui-ajax-loader>
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.
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.
]]>@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.
]]>@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…
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 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:
@function
; mixins are defined with @mixin
but are both named with a dashed ident (e.g. --name
).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:
@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
.)@layer
? scope
? Something else?)Miriam has an excellent outline of the open questions and discussions happening around mixins.
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-mixins
CSS Functions and Mixins Module Notes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]>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 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.
]]>I first …
Smashing Hour With Dave Rupert originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]>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 Link — Permalink 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.
]]>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.
]]>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.
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).
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:
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.
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:
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.
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.
Now text that is center-aligned text is correctly aligned to the center:
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;
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.
]]>You …
Pop(over) the Balloons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]>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:
popover
attributeAny 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.
<details>
element as a starterOne 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:
<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.<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:
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:
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.
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.<details>
marker here, we need to set a special vendor-prefixed pseudo-element, summary::-webkit-details-marker
to display: none
.cursor: pointer
on the <summary>
elements as well.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.-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);
}
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;
}
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;
}
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.
]]>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.
]]>Alvaro was kind enough to share the trustiest of his trusty CSS:
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.
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.
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.
<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.
<body>
some breathing roomNot 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 Link — Permalink 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.
]]>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.
]]>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.
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;
}
}
if()
might look likeOn 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)
);
}
if()
improvementsAlthough 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:
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));
}
if()
, e.g. just as we can make calculations without calc()
if we are inside a clamp()
or round()
function.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.
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.
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.
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.
The basic idea is that we establish two elements:
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.
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.
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.
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.
]]>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.
]]>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 Link — Permalink on CSS-Tricks
Sara Joy: Everybody’s Free (To Write Websites) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]>CSS Selectors originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]>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.
/* 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;
}
/* 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. */
}
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 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 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 might be the most commonly used type of CSS selector you will see around the web. Classes are ideal because they are slightly more specific than element selectors but without the heavy-handedness of IDs. You can read a deep explanation of how the CSS Cascade determines specificity, but the following is an abbreviated illustration focusing specifically (get it?!) on the selector types we’ve looked at so far.
That’s what makes class selectors so popular — they’re only slightly more specific than elements, but keep specificity low enough to be manageable if we need to override the styles in one ruleset with styles in another.
The only difference when writing a class is that we prepend a period (.
) in front of the class name instead of the hashtag (#
).
/* Selects all <article> elements */
article {
background: #fff;
}
/* Selects any element with class="featured" */
.featured {
background: hsl(35 100% 90%);
border-color: hsl(35 100% 50%);
}
Here’s how our <article>
example shapes up when we swap out #featured
with .featured
.
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:
article
)#someID
).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.
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.
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 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 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.
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.
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.
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
120 | 117 | No | 120 | 17.2 |
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
127 | 127 | 127 | 17.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 */
}
}
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.
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:
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.
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.
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.
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.
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.
The vast majority of what you’re reading here is information pulled from articles we’ve published on CSS-Tricks and those are linked up throughout the guide. In addition to those articles, the following resources were super helpful for putting this guide together.
CSS Selectors originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]>if()
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.
]]>if()
conditional to the CSS Values Module Level 5 specification. It was Lea Verou’s X post that same day that caught my attention:
A historical day for CSS 😀🎉
— Lea Verou (@LeaVerou) June 13, 2024
If you write any components used and/or styled by others, you know how huge this is!
background: if(style(–variant: success), var(–green));
Even if you don’t, this will allow things like:
padding: if(var(–2xl), 1em, var(–xl) or var(–m),… pic.twitter.com/cXeqwBuXvK
Lea 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.
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
Lea Verou, “Inline conditionals in CSS?”media()
andsupports()
from Tab’s@when
proposal) whereas in the 2018 proposal how conditions would work was largely undefined.
I like how Lea points out that CSS goes on to describe how CSS has always been a conditional language:
Folks… CSS had conditionals from the very beginning. Every selector is essentially a conditional!
Lea Verou, “Inline conditionals in CSS?”
True! The Cascade is the vehicle for evaluating selectors and matching them to HTML elements on a page. What if()
brings to the table is a way to write inline conditions with selectors.
It boils down to this:
<if()> = if( <container-query>, [<declaration-value>]{1, 2} )
…where:
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:
--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.--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!
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()
andmedia()
, 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.
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:
0
or 1
. (Ana has a wonderful article on this.)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.
Method | Input values | Output values | Pros | Cons |
---|---|---|---|---|
Binary Linear Interpolation | Numbers | Quantitative | Can be used as part of a value | Limited output range |
Toggles | var(--alias) (actual values are too weird to expose raw) | Any | Can be used in part of a value | Weird values that need to be aliased |
Paused animations | Numbers | Any | Normal, decoupled declarations | Takes over animation propertyCascade weirdness |
Type Grinding | Keywords | Any value supported by the syntax descriptor | High flexibility for exposed APIGood encapsulation | Must insert CSS into light DOM Tedious code (though can be automated with build tools) No Firefox support (though that’s changing) |
Variable animation name | Keywords | Any | Normal, decoupled declarations | Impractical outside of Shadow DOM due to name clashes Takes over animation propertyCascade weirdness |
Belated by two weeks, but thanks for sharing the spoils of your big day with us! 🎂
To Shared Link — Permalink 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.
]]>Transitioning to Auto Height originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]>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.
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!
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
.
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.
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);
}
}
display: none
and height: 0
..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 Link — Permalink on CSS-Tricks
Transitioning to Auto Height originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]>Poppin’ In originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]>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.
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.
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.
]]> .work + .life { border: 10px solid #000; }
…
originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]> .work + .life { border: 10px solid #000; }
originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
]]>