What You've Missed in CSS Land
Many new and improved features have landed in CSS over the past few years, and a cross-browser effort to improve compatibility has made the language more stable than ever! Let’s review these enhancements to layout, responsive design, element styling, properties, and selectors, and also peek at upcoming features.
The coordinated implementation of new features across browsers these days means we can start to use these features pretty much as soon as they appear, which is great for keeping our stylesheets as simple as possible. A few, single-line properties can now replace multi-line, hacky solutions. In some cases, newly available features may even mean we can remove JavaScript workarounds we once needed to get around old limitations!
The CSS Working Group (CSSWG)—the body responsible for updates to the language—has been taking feedback from developers and drawing inspiration from community solutions. The improvements described below provide an exciting glimpse into what’s now possible with CSS. There’s never been a better time to get (back) into learning CSS!
New and Enhanced Properties
In this first section, we’ll review some exciting new properties that have landed in CSS in recent times.
Custom Properties
With the end-of-life timeline started for Internet Explorer 11, it’s high time to use custom properties! Also referred to as “CSS variables”, custom properties allow us to define values that are reusable across our stylesheets. Custom properties can be used as an entire value for a property or as partial values, like the h
in an hsl()
definition. We can also modify custom properties in JavaScript. Check out my overview of how custom properties can be used.
Since custom properties aren’t static like Sass or other preprocessor variables, they open up a lot of opportunities for more flexible, dynamic styles. Custom properties offer far more than just being “CSS variables”, as Lea Verou explains in her incredible presentation at CSS Day 2022.
aspect-ratio
A new property that eliminates “the padding hack” is aspect-ratio
. This does what its name suggests, allowing us to define an aspect ratio for sizing an element. The hack I referred to was commonly used to maintain a 16:9 ratio for video embeds. Now that ratio can be achieved through this one property and the declaration aspect-ratio: 16/9
. It’s also a quick way to achieve a perfect square with aspect-ratio: 1
.
Here’s a CodePen demo of how to use aspect-ratio
in conjunction with the older property object-fit
to keep a consistent avatar size regardless of the original image ratio and without distorting the image.
Individual Transform Properties
Also newly implemented across browsers are individual transform properties. Previously, if we wanted to redefine the rotate()
value within the transform
property, we would have to duplicate the definition (or leverage custom properties). As of Chromium 104, we can now use the individual properties of translate
, rotate
, and scale
.
Logical Properties
If we’re managing content for an international audience, we can consider using logical properties. Available for most CSS 2.1 properties, logical variants consider the writing mode and flow of text. For standard English text, we would trade “left/right” for “inline”, and “top/bottom” for “block”:
.element { margin-block: 2rem;}
As in the example above, logical properties also offer shorthands for setting two sides at once, where margin-block
for horizontal writing modes is equivalent to setting margin-top
and margin-bottom
.
The inset Shorthand
Another useful logical shorthand is inset
, which sets top
, right
, bottom
and left
at once.
New and Extended Selectors
Three of the most influential changes to CSS of late have been the :is, :where, and :has pseudo-class selectors. Here’s a summary of them:
:is()
accepts a selector list and equals the specificity of the highest selector. For example,:is(#id, a, .class)
will have the specificity of anid
.:where()
accepts a selector list and always has zero-specificity, making it a popular choice for defining baseline styles in resets.:has()
is the long-awaited “parent selector”, which allows checking a parent for a particular child and then styling either the parent or extending into a compound selector to style the children. (There’s a whole tutorial on:has()
in this series.)
Forgiving Selectors
Uniquely, both :is()
and :where()
are also forgiving selectors. This means that, if an invalid selector or a prefixed selector that doesn’t apply is in the list, the rule will continue to work for the valid selectors. This is contrary to standard compound rule definitions like -webkit-property, property
, where browsers that don’t understand -webkit-property
will throw out the whole rule.
This CodePen demo harnesses :where()
, :is()
, and :has()
to create an author bio component that changes grid display properties depending on whether or not an avatar is available.
Partial Browser Support
Note that :has()
only has partial browser support at the time of writing, so the demo above will currently work in Safari 15.4+ and Chrome/Edge 105+, as well as Firefox 103 with the layout.css.has-selector.enabled
flag enabled.
Enhanced :not()
The :not()
selector has recently been enhanced to accept a selector list, making :not(nav a, footer a)
valid. However, unlike :is()
and :where()
, the update hasn’t made :not()
forgiving of invalid selectors due to backward compatibility support.
Focus Selectors
The next two new pseudo-classes both affect focus behavior. The :focus-within
selector can be used to style a parent when a child is in focus—such as a container around a form field. For element focus styles, we can now use :focus-visible
, which has recently replaced :focus
as the cross-browser default for styling elements that receive focus.
The :focus-visible
pseudo-class makes defined styles visible based on heuristics. Practically speaking, this involves setting focus styles when elements gain focus via keyboard tabbing but not via mouse clicks. For more on this, check out my method of standardizing focus styles using custom properties and focus-visible, which also covers the handy companion property outline-offset
.
::marker
Last but not least, the ::marker
pseudo-element allows us to directly select and style list item bullets and numbers on <ul>
and <ol>
elements, as well as the “caret” for the <summary>
element. This means we can use ::marker
to change just a list’s bullet color! There’s a reduced set of allowed properties for styling with ::marker
, and it may not be the best approach depending on the complexity of a design. For more details on this, and for more ways to apply custom list styling, check out my “ Totally Custom List Styles ” article, as well as this additional information about ::marker.
Improvements for Element Styling
In this section, we’ll review some useful new properties for styling elements.
accent-color
One of the most common things frameworks and design systems seek to change is native form field styles. Prior to the arrival of the accent-color property, even changing a form element’s color was impossible. With accent-color
, we can now influence the checked appearance of radio buttons and checkboxes, and the filled-in state for range inputs and the progress element.
color-scheme
If we’re looking to adapt our interface to a user’s light or dark mode preference using a custom toggle and/or the prefers-color-scheme
query, we should also add the color-scheme property. This provides an opt-in to adapting browser UI elements such as scrollbars, form controls, and CSS system colors. Whereas accent-color
lets us pick custom colors for a few elements, color-scheme
requests that the browser adapt even more—such as asking text inputs and textareas to display as light or dark themed.
It’s recommended that this be applied to the :root
element and that the values be listed in order of a site’s defaults. In other words, if we default to light but support dark, list light dark
. If we default to dark but support light, list dark light
. If we only support light
or dark
, just list that single value:
:root { color-scheme: light dark;}
Defining color-scheme on Individual Elements
We can define color-scheme
on individual elements as well, which is useful for selecting how to adapt a UI for a wider range of custom themes. For more on this, dive into how color-scheme works and learn about the related <meta>
element.
Forced-color Modes
To round out the topic of color, there’s one more preference query and property pair to discuss. Some users on Windows require a “high contrast” theme, in which the OS forces a reduced palette to take the place of our defined colors. The palette fills in values for system colors, which replace things like background, text, button, and link colors, while styles like box-shadows are removed.
If we have critical styles that use color—such as product color swatches—we may need to use the forced-colors
query alongside the force-color-adjust
property. Given the following pairing, our original colors for the .swatch
would be retained:
@media (forced-colors: active) { .swatch { forced-color-adjust: none; }}
Don’t Force It
Forced colors should be used sparingly and only when the user experience is negatively impacted by swapping for the high contrast theme colors. If you’re unfamiliar with high contrast themes, learn about styling with forced colors.
Text Decoration Improvements
On the text decoration front, we now have the text-underline-offset
property available, which allows us to adjust the position of the defined text-decoration
from its original position. The text-decoration-thickness
companion property allows us to control the stroke thickness of the text-decoration
. Combined, these properties eliminate the hack of using a border or even a pseudo-element in order to style a link underline.
Color Fonts
Since 2018, the COLR font-format has had cross-browser support, affecting fonts that include a color palette. Recently, two related CSS properties were released that allow us to define some features for color fonts: font-palette
and @font-palette-values
. The first, font-palette
, allows us to select which font-defined palette to use, and @font-palette-values
allows us to define a custom palette. Learn more about color fonts and the CSS features in this article by Ollie Williams.
New and Updated Properties for Layout and Scroll Behavior
In this section, we’ll review properties that enhance the options for controlling layouts and how users can scroll a page.
gap
The gap
property is now supported for both Grid and Flexbox. The gap
shorthand property allows setting a “gap” of space between flex and grid children. A gap
is only ever applied between children, not around them, which makes it different from applying margin. While gap
has been available for Grid since its inception, support for Flexbox gap was lagging behind until it landed in Safari in April 2021, resulting in cross-browser compatibility.
Scroll Control Properties
Several scroll-related features have become stable, with one of them being scroll-snap
. With scroll-snap
, we can control orientation and “snap points” within a scrolling area. Example use cases include a native swiping experience through an image gallery, or creating a CSS-powered slide deck.
Not the Be-all
Though we can now create a range of scrolling effects with CSS, using CSS alone isn’t always the best option. JavaScript may still be required for fully accessible advanced scrolling interactions.
To use scroll snapping, we must create a wrapper that contains overflow and sets scroll-snap-type
, which defines the scrolling behavior. Then, optionally, set scroll-snap-*
properties on the children to define alignment and whether or not scrolling should stop on each child. An example, along with some important cautions for using scroll-snap
, can be found in my scroll snap demo on SmolCSS.
Related to the scroll snap feature are the properties for scroll-margin
and scroll-padding
. These properties are unique, because they’re essentially tacked on to the related element only in the context of scroll position, while not affecting the layout position of the element.
My favorite use case is leveraging scroll-margin-top
to provide space above an element that’s the target of an anchor link (such as https://example.com/#my-target-element
). For example, the following code makes a nice addition to our reset styles and results in 2rem
of space between the top of the viewport and the top of the element that’s being navigated to:
:target { scroll-margin-top: 2rem;}
A property for controlling scroll behavior that immediately allowed me to remove some JavaScript from certain projects was scroll-behavior
, which offers the value smooth
. This results in smooth scrolling—such as when visiting in-page anchor links. The scroll-behavior
property gained cross-browser support as of Safari 15.4, released March 2022.
Note that scroll-behavior: smooth
can cause issues for folks with motion sensitivities. To enable it in a more accessible way, wrap it with the preference query prefers-reduced-motion: no-preference
:
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; }}
Yet another newly cross-browser scroll-related addition (as of Safari 16) is overscroll-behavior
, which defines the behavior when the scroll position reaches the boundary of a contained scroll area. A practical and common scenario where this is useful is documentation sites that have a sidebar for navigation. Without the use of overscroll-behavior
when users are scrolling within the navigation and reach the end of the list, the rest of the content on the page will begin to scroll. Now we can set overscroll-behavior: contain
to prevent the remaining page area from scrolling.
Here’s a CodePen demo of this newly supported feature.
Dynamic Viewport Units
New viewport units have been released to address a problem that can cause scroll-related frustration. This update was prompted by the 100vh
issue in Safari iOS, which ignores its dynamic UI elements and makes the value taller than the visible viewport. Now we can opt in to the preferred measurement by using the large, small, and dynamic viewport units. Bramus has an excellent overview of them, but in summary:
- “Large” (
lvh/lvw
) assumes any dynamic UI element is retracted (hidden), resulting in more viewport space. - “Small” (
svh/svw
) assumes any dynamic UI element is expanded (visible), resulting in less viewport space. - “Dynamic” (
dvh/dvw
) recalculates the value as a dynamic UI element retracts and expands.
Modern Responsive Design Features
The CSS math functions min()
, max()
and clamp()
became stable in April 2020. These functions accept multiple values, which makes them excellent companions for dynamically changing numeric values wherever numbers are accepted. This includes dimension properties as well as gradients, background-size
, box-shadow
, font-size
, and more!
Here’s what they do:
min()
asks the browser to use the smaller computed size.max()
asks the browser to use the larger computed size.clamp()
accepts three values, in order—a minimum, an ideal, and a maximum size—where the ideal value should include a dynamic measure such as a viewport unit or a percentage.
How do these relate to responsive design? We can use them to request that the browser move smoothly between acceptable values as the context changes, instead of creating multiple media queries to handle adjustments.
Here are three quick use cases:
.container { /* Grow no larger than 90vw */ /* When 100% - 2rem computes to less than 90vw, use that calculation, resulting in a 1rem "gutter" on smaller viewports */ width: min(90vw, 100%—2rem);}
.container + .container { /* On taller viewports, expand up to 8vh, and on shorter viewports don't shrink less than 4rem */ margin-top: max(8vh, 4rem);}
.element { /* Allows padding up to 3rem for larger viewports, and reduced padding down to 1rem for smaller viewports, with the "ideal" being 5%, which is relative to the element's computed width */ padding: clamp(1rem, 5%, 3rem);}
Container Queries
One of the most exciting developments in all of CSS—and one which greatly affects what we can do with responsive design—is container queries. At the time of writing (April 2023), they’re available in Chrome/Edge 105+, Safari 16+, and Firefox 110+. A container query polyfill enables the essential features for Firefox and a significant number of older browsers.
So, what are container queries? Like media queries, they take the form of an at-rule. But they differ from media queries in that, instead of orchestrating changes at the viewport level, container queries allow any component or element to respond to a defined container’s width. (Currently, height is still being considered.) By using a container query, we’re able to change styles for a container’s children.
The following CodePen demo —from my in-depth primer on container queries —alters the text and colors as the containers transition through a range of sizes. (Widen and narrow the viewport to see these changes take place.)
The example above is considered a size-related container query. Also in the works for the container query specification are style-related queries. These will allow us to query style features of the query container—and most excitingly, this includes custom property values. Chrome 111 shipped with support for custom property values for style queries, which we can test in spec author Miriam Suzanne’s style query CodePen demo. (We cover container queries in more depth in the Practical Uses of Container Queries tutorial in this series.)
Container Units
Container units were also released alongside the @container
feature. While there are several of these units available, cqi
will probably be one of the most frequently used, as 1cqi
corresponds to 1% of an element’s inline dimension (“width” for horizontal writing modes). This makes it an attractive upgrade for fluid typography techniques that previously used clamp()
with viewport units—which becomes insufficient for varying element widths. Now we can have context-independent fluid typography using a definition like font-size: clamp(1rem, 1rem + 4cqi, 1.75rem)
. The container query polyfill (referenced in the “Container Queries” section above) also covers support for these units.
Catering for Text Zoom
When using fluid typography techniques, make sure that text can be zoomed to 200% of its original on-screen size to meet WCAG Success Criterion 1.4.4—Resize Text.
Media Query Range Syntax
There’s been an update to range syntax that’s now enabled for both container queries and media queries from Chrome/Edge 104, Firefox 63, and Safari 16.4. Older versions are also supported via the container query polyfill.
Range syntax means we can use mathematical comparison operators within queries, as shown in the following code:
/* Legacy syntax */@media (min-width: 320px) and (max-width: 768px) {}
/* New range syntax */@media (320px <= width <= 768px) {}
A Paradigm-shift for CSS Organization
CSS Cascade Layers provides a new at-rule (@layer
) that assists authors in managing the cascade. @layer
is a powerful tool for controlling specificity, which it does by orchestrating the order of rule sets. In an impressively coordinated cross-browser rollout, CSS Cascade Layers was made available from Chromium 99, Safari 15.4, and Firefox 97.
What does @layer
do? It allows us, as authors, to be explicit about controlling two key elements of the “C” in CSS—the cascade. With @layer
, the power is in our hands to pre-emptively mitigate conflicts between specificity and order of appearance —the last two priorities the browser considers when applying an element’s style.
A large codebase, or any project with multiple authors, can easily fall back on !important
when specificity issues arise. With @layer
, we can define groups of rule sets with a pre-determined order to reduce the likelihood of conflicts.
Consider this small example below, in which we create an ordering of layers, and then assign rules to those layers. Although the @baseline
layer receives a p
rule first, it still wins over the @reset
layer, because the initial order of layers defines the order used. Additionally, specificity matters between layers, but a high-specificity rule in an earlier layer can be beaten out by a lower-specificity rule in a later layer.
Given the following layer order, <p class="red">
will render gray text:
@layer reset, baseline;
@layer baseline { p { color: gray; }}
@layer reset { p.red { color: red; }}
There are lots of deeper concepts to understand to be successful with layers. For more details, and for tips on how to incorporate them with Sass or third-party styles, check out my getting started guide for cascade layers.
Using New CSS Features
While most of the items we’ve reviewed have great evergreen browser support, we should do our own testing with devices and browsers that match our audience. Often, we can use these new additions as a progressive enhancement —which involves providing more stable fallbacks when features aren’t supported.
Sometimes, we can define a legacy property that accomplishes a close but less-perfect solution prior to the new property. The browser will use the latest defined property that it understands, so if it understands the modern property it will use it. For example, to begin using logical properties, we can do something like this:
.element { margin-left: 1rem; margin-inline-start: 1rem; /* Browsers that understand `margin-inline-start` will use this rule */}
@supports
The @supports
rule also enables feature detection within CSS. Here’s an example of using it for an aspect-ratio
fallback:
.aspect-ratio-item { height: max(25vh, 15rem);}
@supports (aspect-ratio: 1) { .aspect-ratio-item { aspect-ratio: 4/3; height: auto; }}
We can also check for lack of support with @supports not (property: value)
, or for selector support with @supports selector(:selector(a))
.
Supporting @supports
Support for @supports
and @supports selector()
varies, so use these options with caution! For the most robust solution, we should compare relative levels of support for the features we’re trying to use with support for @supports itself.
Further information about @supports
features, along with more details about expected behavior, are available on the MDN @supports page.
Build Tools for Modern CSS Support
A popular build tool is PostCSS. It’s used with various plugins—particularly PostCSS Preset Env, which helps to polyfill various CSS features until they’re stable. This includes nesting, media query range syntax, and cascade layers.
Depending on what features we need supported, it may be worth looking into Lightning CSS, a PostCSS alternative that provides some polyfills and also the bonus built-in benefits of minification and autoprefixing.
Looking Towards the (Near) Future
CSS Color Module Level 5 includes a lot of color features, including the following, which have presently no or very minimal, partial support:
- new color spaces
- color-mix()
- relative color syntax
Of these, I’m most excited about relative color syntax, which is a way to create a color from another one. For example, rgb(from red r g b / 50%)
creates a red with 50% alpha transparency. This feature will also allow changing between color spaces, or adjusting parts of color spaces like the l
in lch()
.
The Grid spec is still evolving, and an addition with partial-and-growing support is the long-awaited subgrid
feature. At the time of writing, subgrid has support in Safari 16+ and Firefox 71+, with Chromium work in development.
Subgrid will allow nested grid containers to use a parent grid container’s rows and/or columns and match the size of the parent’s tracks. This can enable a site-wide grid that’s set once and that all page containers can use. Or more granularly, it can be used to align content within a grid of cards even with variable character lengths for titles and such. Rachel Andrew demonstrates the features of subgrid in this Smashing Meets talk.
These three specs don’t currently have any stability but are showing positive development:
@scope
. This will allow us to apply styles within a scoped region without affecting children beyond that region, as explained by spec author Miriam Suzanne.@scroll-timeline
. This will allow animations that are triggered by scroll position, where scrolling advances or rewinds the animation timeline. Watch Bramus Van Damme demonstrate scroll-linked animations in his talk from CSS Café.- Nesting. This feature will allow native rule nesting—similar to what’s allowed in preprocessors like Sass—as explained in the editor’s draft.
Keeping Up with New CSS Features
To keep up with these and other changes to CSS, bookmark the Interop dashboard and have a look at the CSS Working Group’s GitHub projects and issues. If you’re a VS Code user, the webhint plugin also highlights CSS features when their support is less than what’s indicated in your package browserslist
.