GeistHaus
log in · sign up

OddBird CSS Notebook

Part of OddBird

Notes towards the future of CSS, from Miriam Suzanne & OddBird

stories
Changes to: CSS Overflow - Broad Research

Remove and re-link Chrome’s carousel proposal as tangential to the goals of this document

CSS Overflow - Broad Research »

Show full content

Remove and re-link Chrome’s carousel proposal as tangential to the goals of this document

CSS Overflow - Broad Research »


This is not a carousel explainer:

Chrome asked me to pursue a proposal for CSS carousels, but I only agreed to research a broader set of issues around overflow. I created this explainer to explore paged overflow generally in screen media.

Once that research was complete, Chrome began to contribute carousel-specific proposals here – and link this document as The Carousel Explainer. I’ve removed that content, since it’s not a proposal I’m involved with, and not the goal of this document.

For their proposal, see:

Participate

Anyone can comment on, file issues against, or contribute to this explainer on GitHub:

We also rely heavily on some existing proposals discussed in the CSS Working Group issue tracker:

Introduction

Managing layout and design on the web requires careful control of ‘overflow’ – allowing content to be hidden when it’s not needed, but accessibly discovered and revealed when necessary.

There are many overlapping user-interface design patterns that can be used – from standard continuous scrolling, to scroll-snapping, slideshows, carousels, accordions, tabs, and various hybrid approaches. The lines between these patterns are often somewhat blurry:

  • CSS currently supports several overflow values, but the only built-in overflow-access interface is the continuous scrolling container. That can be extended with features such as scroll-animation, scroll-behavior, scroll-snapping, and so on.
  • At the other end of a spectrum, most tab and accordion patterns are highly structured & sectioned overflow. Each section has an interactive label that can be used to show or hide the section contents (often one-at-a-time).
  • In-between those two extremes, there are a wide range of patterns that get referred to as ‘carousels’. These are not limited to the (problematic) infinite-auto-advancing home-page widgets, but range from video-streaming media selection, to slide shows, and e-commerce product-image viewers.

There have been many attempts over the years to define new tab or carousel components – but they are often held up by:

  • The complexity of use-case mixing and matching patterns from scrollers, carousels, tabs, and accordions
  • The need to work around many underlying features that are missing from the web platform
  • The fact that ‘overflow’ is a presentational concern tightly coupled with device/media/size conditions (and therefor part of CSS), but also requires interactive controls generally left to HTML components or JavaScript.
Goals

The goal of this explainer is not to propose yet another component, or provide a one-size-fits-all solution for all overflowing UI – but to consider some baseline improvements to CSS overflow that could help authors flesh out the details of each pattern more elegantly, consistently, and accessibly. This could also help form the scaffolding for future components, as needed.

In some cases, we’re able to propose a path forward – and in other cases, we simply document the problem and some of the tradeoffs with different approaches.

Overlapping patterns

The patterns we’re looking at fall along a continuum of scrolled and paged overflow:

  • On one end, media-scroller carousels (like the Netflix video selection interface) are similar to horizontal overflow with scroll-snapping, and the occasional use of ‘next/previous’ navigation that is sometimes linked to items and sometimes to ‘pages’ of overflow.
  • Slide-show style carousels often add small ‘scroll-markers’ (like dots) that represent either the individual items listed, or the viewable pages (if multiple items appear at once).
  • Product-image carousels will often replace the dots with thumbnail image scroll-markers, and only show one item per ‘page’. In this case general mouse scrolling (and visible scroll-bars) are often removed in favor of tab controls, while sometimes leaving gesture-based scrolling intact.
  • Tabs are similar, but may often removing scroll-controls entirely.

Rather than thinking of these as distinct patterns, we want to understand the underlying set of controls that are combined to create any given variation. Primarily: paged overflow and interactive scroll markers in a variety of forms.

Overflow changes based on context

While HTML, CSS, and JS often have distinct and separate ‘concerns’ – semantics, presentation, and interaction – overflow falls into a middle ground that can be hard to break apart. Content that overflows on one device, or at a particular screen size, may not need to overflow in other situations. That’s a presentational concern, rather than a semantic one, and often needs to be handled using media queries.

However, semantics do play a clear role in what patterns make sense (the difference between a carousel and tabs often comes down to how content is sectioned), and different types of ‘overflow’ require different interactions. Any solutions provided here have to account for changes in overflow based on device/media/container context – and help authors access the proper interactions and accessible roles for each type of overflow.

Non-goals Application-Level Tabs

The ‘tabs’ interface pattern has two common and somewhat distinct use-cases:

  1. Managing display of sectioned content within a single document
  2. Managing display of multiple documents within an application

The former can be seen as a form of ‘content overflow’ covered by by this proposal. When searching within a document, users would expect content in hidden sections to be ‘discoverable’. On the other hand, application level tabs (such as in a browser or text-editor) represent distinct documents rather than ‘overflow’. Users would not expect a search in one document to reveal content in another tab.

This proposal is specific to handling content overflow presentation in HTML and CSS, rather than providing a generic solution to the ‘tabs’ interface design pattern.

Auto-advancing & Cyclic Home Page Carousels

The term ‘carousel’ has a strong negative connotation in web design circles, thanks to the poor user-experience of home page carousels used to display an endless cycle of ‘featured’ content.

Readers rarely interact with this pattern, and generally don’t see content beyond the first page of the carousel, unless forced by auto-advancement, which causes a whole new list of accessibility and usability issues.

While that makes us hesitant to use the term ‘carousel’ here, there are a range of much more essential use-cases that fall under the term – and it’s helpful to understand the somewhat fluid and overlapping nature of scroll/carousel/tab/accordion patterns.

Virtual lists

Virtual lists are tightly related to overflow, but outside the scope of this document.

Possible solutions

Rather than proposing a single complete solution to these fluid ux patterns, we think it would be useful to address the core HTML/CSS layout and overflow issues that make such an element difficult to implement. This should provide a stepping-stone for both authors and browser vendors to explore and develop more narrowly defined web-components or HTML elements where they make sense.

There has already been some significant progress along these lines. The scroll-snap properties, scroll-linked animations, and size container queries all help authors better address overflow. Below, we’ll explore some of the features that are still missing or difficult for authors to get right.

Paged overflow, in the browser

Simple carousels (or ‘media-scrollers’) are often built with scrollable overflow and scroll-snap targets. What makes it a ‘carousel’ is the addition of ‘paged’ navigation options:

  • next/previous page arrows
  • page markers (often dots) to show the number of pages and current active position

Paged overflow isn’t a new idea in CSS. While implementations have not always been complete or consistent, paged overflow is already well-defined for print, and has a number of other use-cases if it were available in the browser.

Establishing paged overflow on an element would generate ::page pseudo-element fragmentainers for descendant elements & content to flow through.

  • pages are created as-needed based on the amount of content, and the size available to each page.
  • by default, a page could be the size of the containing box (not including overflow), so that scrolling the view 100% would bring a new ‘page’ fully into view.
  • Pages could be resized, which would impact how much content fits in a given page, how many pages are visible without scrolling, and how many pages are needed for the flow of content.
  • Individual pages could be targeted using a syntax similar to nth-child

It’s not immediately clear what syntax would be best for invoking this sort of paged overflow. While this behavior is related to both overflow and (in some ways) display, neither property seems like a particularly good fit.

Paged use-cases (e.g. carousels) might involve scrolling between pages, while others (e.g. multicol wrapping) may not. So pagination is not necessarily alternative to scrolled overflow. Even if paged overflow had an auto-like scroll behavior, to allow scrolling and non-scrolling pages, single-axis paged overflow(-x/-y) doesn’t make much sense.

Authors will also need a mechanism for handling the layout of elements within pages (and the layout of the pages themselves) – both of which require display values. It’s possible the pagination and layout controls could be combined in a single property (e.g. display: paged grid) if it makes sense for them to cascade together. Since display is a shorthand for inside and outside values, pagination would either need to be added to one of those properties or a third new display sub-property.

Styling paged overflow

As mentioned above, authors will need a way to provide display values for both

  1. the layout of ::page elements themselves
  2. the layout of child contents flowing through those pages

I would expect the overflowing/paginated parent element to handle the layout of pages, in which case a new (::page-contents?) could be used for the layout of content flowing through pages (or vice versa). An additional wrapping pseudo-element like this might only support a limited subset of CSS properties or pseudo-classes.

In some cases, an author would style all ::page elements, but it would often be useful to target specific pages with e.g. ::page:nth-child(even) (or ::page(even)). However, there might be recursion issues with other combinations, such as ::page:nth-last-child(even) or ::page:focus-within, if the styles applied could change the number of pages or placement of content in those pages.

Note:

Could these pseudo-classes be used for paged (e.g. print) media as well?

Page navigation / pagination

One of the design patterns often associated with a ‘carousel’ component is the use of ‘dots’ for page-based navigation. Rather than (or in addition to) a continuous scrollbar, readers can select the page they want to move to. These ‘scroll markers’ or ‘pagination markers’ are much like list markers – often taking the form of dots/bullets, but occasionally styled as counters, or even thumbnails and other content.

Sometimes those page-access markers represent individual items in a list. This per-item marking has a large overlap with ‘tabs’ – a line that blurs in ‘product-image’ carousels for example. Where’s the line between a carousel-marker with an image thumbnail, and a section tab with a text label?

In general, item-based markers can be provided by authors in the markup, along with the items they mark. In that case, the main concerns are:

  • Keeping the markers inline with the items they mark, vs creating a distinct table-of-contents before/after the flow of items/pages
  • Managing the link between markers and the content they mark
  • Maintaining the current state of active content and markers

We’ll explore some of those issues more below.

However, many scroll markers represent the abstract ‘pages’ generated by a flow of content – where the number of markers may change based on the size of a container – or arbitrary scroll-snap points that combine items and pages. In those situations, a marker would need to be generated either using JavaScript, or (ideally) by the browser.

Una Kravets and Adam Argyle have explored this in an early draft ::scroll-marker explainer. In their proposal, markers can be generated by setting a scroll-display property to one of bar (the default), auto, or marker on the scrolling parent. This leaves a number of interesting questions open for consideration:

  • Would we need a way to request markers generated per-item, per-page, or per-snap-target? Can those options be combined?
  • Can the scroll-display can be set differently for inline/block axis?
  • Can values be combined to show both a continuous-scroll bar and also scroll markers?
  • Do previous/next arrows fall into a similar category? While they are simpler for authors to provide in-markup, they require a fair amount of state-management to get right.
Interactive, state-managing, generated content?

One of the most difficult aspects in building web carousels, slideshows, and tabs is proper ‘wiring’ for the interactive navigation (dots, tabs, prev/next) – with proper accessibility and keyboard interaction, along with scroll-position and active-state management. This is especially true when the ‘targets’ of that navigation are based on fluid overflow rather than specific DOM elements.

This is the primary reason that authors would prefer a built-in web platform solution, but it also raises big questions about using ‘generated content’ for interactive controls.

The generated markers would also need to track and expose the current active state - providing a way to style the active marker.

Per-item markers & tabs

In more tab-like cases, where a predictable number of markers are needed – e.g. one marker per list-item – they could be provided in markup.

This has several advantages, since it gives authors more control, and doesn’t rely on interactive pseudo-elements. However, it still comes with it’s own set of issues:

  • Do we need a new element or attribute for assigning scroll-marker behavior? Or do authors have to do all the interaction/state-management manually?
  • Is there a way for authors to keep the marker markup inline, and still ‘hoist’ the element into a parent grid context?
<tabs>
<section>
<h2 marker>tab label</h2>
<div class='panel'>tab panel</div>
</section>

</tabs>
Layout of scroll-markers / tabs Key scenarios

[TBD]

Detailed design discussion & alternatives

[TBD]

Prior Art & Context Stakeholder Feedback / Opposition

[TBD]

References & Acknowledgements

Much of this work is based on the research and proposals compiled by others:

  • Robert Flack
  • Nicole Sullivan
  • Rachel Andrew
  • Una Kravets
  • Adam Argyle
  • Brian Kardell, Dave Rupert, Jon Neal, Sarah Higley, Scott O’Hara, and others in the OpenUI Community Group.
https://css.oddbird.net/changelog/overflow/explainer/2025-09-22t170656000z/
New page: Defining the `&` selector in a `@scope` rule

New page added

Defining the `&` selector in a `@scope` rule »

Show full content

New page added

Defining the `&` selector in a `@scope` rule »


TL;DR we resolved during the scope breakout session that & nested directly inside @scope should refer to the implicit :where(:scope) selector that is prepended on declarations and scoped selectors by default. This post explains the thought process behind our decision – which is a change from the current specification.

How the & selector works, when nesting

CSS nesting gave us the & selector. When used in a nested context, it ‘represents’ the parent selector wrapped with :is():

.parent {
& { color: green; }
& > .child { color: red; }
}

This can be ‘de-sugared’ to conventional un-nested syntax:

:is(.parent) { color: green; }
:is(.parent) > .child { color: red; }

The & has been swapped out, and replaced by the parent selector. It matches the same elements as the parent selector, with the same specificity. In the examples above (where & is the start of the selector) we’re allowed to leave it out and imply the relationship. This has exactly the same behavior and specificity as our examples above:

.parent {
color: green;
> .child { color: red; }
}

If we add the & to the selector, the implicit & is not applied. These two rules are the same:

.parent {
.context > & { color: red; }
}

.context > :is(.parent) { color: red; }

Note that &

  • Represents the parent selector, matching the same elements
  • Is implied at the start unless explicitly placed in a nested selector
  • Implicit and explicit starting & match the same elements
  • Implicit and explicit starting & have the same specificity
How the :scope selector works

Despite @scope being new in CSS, ‘scoped’ styles have existed for some time, and the :scope pseudo-class has a well-established behavior. The main examples I’m familiar with are the JavaScript querySelector() methods. Since the query is run on an element, the :scope pseudo-class represents that ‘scope root’ element. When left out, it is implied at the start of the selector – so these two queries select the same elements:

parentElement.querySelector('a');
parentElement.querySelector(':scope a');

Again, if we move :scope elsewhere in the selector, it’s no longer implied at the start. The following will match links in scope when the scope is also inside an element with the .context class:

parentElement.querySelector('.context :scope a');

While it has been less useful, :scope is also allowed in plain CSS, where it behaves the same as :root. Since there is no other scope root defined, the implied scope is the document root. And since :scope is a pseudo-class, it has the specificity of a class selector.

Note that :scope

  • Represents the single scope-root element
  • Is implied at the start unless explicitly placed in a scoped selector
  • Implicit and explicit starting :scope match the same element
  • Implicit and explicit starting :scope have different specificity

Since only the specificity is different, we can think of the implicit scope as being something like :where(:scope) – with zero specificity.

How the @scope rule works

The new @scope rule allows authors to define scoped styles in CSS. This has a lot of overlap with nesting, because (until now) the default ‘descendant combinator’ has been our best approximation of the scope use-case.

These two selectors are similar, and should match the same elements – a .child class descendant of the #parent ID.

#parent {
.child { color: green; }
}

@scope (#parent) {
.child { color: green; }
}

But the @scope rule is intended for filtering only, and does not add any implicit specificity. We can see how that is the result of the behavior we described above, by making the implicit selectors explicit. The nested selector here has a specificity of the parent ID and child class together, while the scoped selector only has the child class specificity:

#parent {
/* specificity: 1,1,0 */
& .child { color: green; }
}

@scope (#parent) {
/* specificity: 0,1,0 */
:where(:scope) .child { color: green; }
}

If we add bare declarations inside an @scope rule, the behavior is similar. These two color declarations have the same meaning and specificity:

@scope (#parent) {
color: green;

:where(:scope) { color: green; }
}
What does & represent, when scoping?

The question is what it means to use & in an @scope context.

The original spec

A scoped & could refer to the <scope-start> selector (#parent in the previous example). But then adding the & at the start of a selector changes both the specificity and also what it can match:

@scope (.parent) {
/* - only selects the :scope itself */
/* - specificity of 0,0,0 */
border: thin dotted;

/* - selects any .parent that is in-scope */
/* - specificity of 0,1,0 */
& { border: thin dotted; }
}

This was the initial shape of the specification, but was raised as an issue by Roman Komarov. While & here can be seen as ‘referencing the parent selector’, the resulting behavior is actually quite different from the way & works in a nested context. The decision to add it or leave it off has a big impact on the meaning and specificity of a style.

An author is given multiple options, all with subtly different behavior that might be hard to learn:

  • Bare declarations match only the scope-root at zero specificity
  • :where(:scope) is the same as the implicit behavior
  • :scope matches only the scope root, with 1-class specificity
  • & matches based on a selector, with that selector’s specificity
The proposed change

But if we think of & instead as ‘referencing the implicit context’ of a selector, we can fix that. From that perspective, the ‘parent selector’ of an @scope rule is actually :where(:scope) – since that’s the selector added implicitly at the start of scoped selectors. Now three of these selectors behave the same:

@scope (.parent) {
/* - only selects the :scope itself */
/* - specificity of 0,0,0 */
border: thin dotted;
:where(:scope) { border: thin dotted; }
& { border: thin dotted; }

/* same behavior, different specificity */
:scope { border: thin dotted; }
}

The & behaves the same way as it does when nesting, representing our implicit parent context directly. Adding or removing the implicit & gives us the same results. That allows authors to use & consistently in both nested and scoped contexts, as a reference to the implicit selector which will otherwise be prepended.

This is the solution we resolved on during the scope breakout session – but we plan to re-visit with the full group next week.

https://css.oddbird.net/scope/parent-selector/
Changes to: Physical/logical properties, keywords, and functions

Clarify SVG properties, and change title

Physical/logical properties, keywords, and functions »

Show full content

Clarify SVG properties, and change title

Physical/logical properties, keywords, and functions »


This list is based on the MDN properties reference, and the index of CSS properties.

I have attempted to capture all the places where axis or side is implied by the order of values – which includes (but is not limited to) shorthand properties. For example, background-position is a shorthand for background-position-x and background-position-y – but object-position uses the same syntax without having individual longhand sub-properties.

This is not meant to be a prescriptive list of which properties should provide logical alternatives, but a descriptive list of what properties could be toggled from one syntax to another.

Apart from properties, all <basic-shape> functions, transform functions, and gradient functions currently use physical syntax – and there have been various requests for logical alternatives. It’s not clear to me on first-glance if a logical/physical toggle could (or should) change how those functions are parsed.

As a side-note, I also captured where we have physical keywords without logical alternatives, although that’s clearly tangential to the syntax-toggle issue, since they are explicit to an axis or side. Still, they seem relevant to a larger question of providing logical alternatives wherever it is useful.

I think the next step is to sort these into groups by shared syntax, and describe how a toggle (either local or global) would apply to each.

You can support this effort or read more about it.

Multi-value properties

These accept multiple dimensions in a single syntax, but don’t have associated sub-properties for the dimensions involved. Ideally, a global toggle would apply here.

  • aspect-ratio physical (x/y)
  • box-shadow physical (x/y)
  • text-shadow physical (x/y)
  • background… (*-position is listed with shorthands)
    • background-size physical (x/y)
    • background-repeat physical (x/y) and keywords
  • border-image
    • border-image-outset physical (trbl)
    • border-image-repeat physical (x/y)
    • border-image-slice physical (trbl)
    • border-image-width physical (trbl)
  • mask
    • mask-position physical (x/y) with offset keywords
    • mask-repeat physical (x/y) and keywords
    • mask-position physical (x/y)
  • mask-border
    • mask-border-outset physical (trbl)
    • mask-border-repeat physical (x/y)
    • mask-border-slice physical (trbl)
    • mask-border-width physical (trbl)
  • object-position physical (x/y) with offset keywords
  • offset-position physical (x/y) with offset keywords
  • perspective-origin physical (x/y) with offset keywords
  • transform… (see related functions as well)
    • transform-origin physical (x/y) with offset keywords
    • rotate physical (x/y/z)
    • scale physical (x/y/z)
    • translate physical (x/y/z)

No change needed…

  • border-spacing logical (columns & rows)
  • column-width & proposed column-height are flow-relative
  • view-timeline-inset is controlled by view-timeline-axis
Shorthand properties

These shorthands accept dimensions, which can be set individually using sub-properties. Some of them have logical as well as physical long-hand properties available, while some are missing either the physical or logical alternative.

  • background-position (*-x & *-y) with offset keywords
    • missing: *-inline & *-block (ED level 4, unpublished)
  • size (width & height)
    • available: inline-size & block-size
  • border
    • border-width (border-<trbl>-width)
      • available: border-<axis>-<side>-width
    • border-style (border-<trbl>-style)
      • available: border-<axis>-<side>-style
    • border-color (border-<trbl>-color)
      • available: border-<axis>-<side>-color
    • border-radius (border-<trbl>-radius)
      • available: border-<block>-<inline>-color
    • border-image (see sub-properties above)
      • missing: logical -outset, -repeat, -slice, & -width
  • inset (top, right, bottom, & left)
    • available: inset-<axis>-<side>
  • contain-intrinsic-size (contain-intrinsic-width & *-height)
    • available: contain-intrinsic-<axis>-size
  • margin (margin-<trbl>)
    • available: margin-<axis>-<side>
  • scroll-margin (scroll-margin-<trbl>)
    • available: scroll-margin-<axis>-<side>
  • padding (padding-<trbl>)
    • available: padding-<axis>-<side>
  • scroll-padding (scroll-padding-<trbl>)
    • available: scroll-padding-<axis>-<side>
  • mask-border (see sub-properties above)
    • missing: logical -outset, -repeat, -slice, & -width
  • overflow (overflow-x & overflow-y)
    • available: overflow-<axis>
  • overflow-behavior (*-x & *-y)
    • available: overflow-behavior-<axis>

No change needed…

  • border defines all sides equally
  • outline/outline-width defines all sides equally
  • overflow-clip-margin defines all sides equally
  • columns already flow-relative
  • gap already flow-relative
  • animation-range controlled by *-timeline-axis
Keywords

Since keywords clearly establish physical/logical directions, they would not be impacted by a global ‘switch’ – but it’s still useful to know where logical functionality might be missing.

  • background-repeat has repeat-x & repeat-y keywords
    • missing repeat-inline & repeat-block
  • mask-repeat has repeat-x & repeat-y keywords
    • missing repeat-inline & repeat-block

Properties including background-position, mask-position, object-position, offset-position, fill-position, and stroke-position all use the <position> syntax with optional offset keywords. That allows for positioning relative to either a physical or logical edge without any toggle on the property parsing itself.

No change needed…

  • caption-side (top & bottom) are flow-relative, along with inline-start and inline-end alternatives to left and right
  • clear has both logical & physical keywords
  • float has both logical & physical keywords (including page-floats)
  • flex-* & grid-* already flow-relative
  • margin-trim already flow-relative
  • scroll-snap-align already flow-relative
  • scroll-snap-type has both logical & physical keywords
  • scroll-timeline-axis has both logical & physical keywords
  • place-* and sub-properties, already flow-relative
  • position-area already flow-relative
  • resize has both logical & physical keywords
  • text-align has both logical & physical keywords
  • text-align-last has both logical & physical keywords
  • azimuth logical dimensions have no meaning here
Functions
  • All <basic-shape> functions use physical dimensions
    • Impacts clip-path, shape-outside, offset-path
    • Deprecated clip (for rect() only)
    • Never implemented shape-inside
  • rotate(), translate(), and scale()
    • Impacts transform
  • various gradient() functions
    • Impacts all image properties
SVG

Most SVG-related properties such as cx and cy only provide physical syntax. However, the text-anchor property only has start and end values, with no physical variant. We could consider adding physical keywords for text-anchor, but I don’t see any need for a syntax toggle.

The fill & stroke properties have *-image, *-position, *-size, and *-repeat sub-properties specified – but not currently implemented. Any changes to similar background/border productions, should apply here. I think this is the only place a syntax toggle would impact SVGs.

https://css.oddbird.net/changelog/logical/properties/2025-06-16t173054000z/
Changes to: Physical/logical properties, keywords, and functions

The position type does support logical offset keywords

Physical/logical properties, keywords, and functions »

Show full content

The position type does support logical offset keywords

Physical/logical properties, keywords, and functions »


This page has more recent changes available. For more details, see:

https://css.oddbird.net/changelog/logical/properties/2025-06-12t210337000z/
Changes to: Physical/logical properties, keywords, and functions

Provide more context in a summary

Physical/logical properties, keywords, and functions »

Show full content

Provide more context in a summary

Physical/logical properties, keywords, and functions »


This page has more recent changes available. For more details, see:

https://css.oddbird.net/changelog/logical/properties/2025-06-12t202531000z/
New page: Logical shorthand notes & side quests

New page added

Logical shorthand notes & side quests »

Show full content

New page added

Logical shorthand notes & side quests »


Why inline-size but padding-inline?

This is a question from @johanwestling on Mastodon:

But why the opposite property naming order? I mean padding, padding-block, padding-inline for example.

I wasn’t in the CSS Working Group when this decision was made, but here’s what I’ve found so far.

Github issues only go back to late 2015, after the names seem to be settled, so I can’t find any discussion there.

Many of the people participating in those conversations are still involved with the CSSWG today, so I can ask around for more information – but it seems to me that the likely answer is:

  • The logical margin, padding, and border properties were added as longhand versions of existing properties, with the well established <base-property>-<sub-property> syntax.
  • At the time, width and height were not longhand properties with a shared shorthand base-property, and so the logical alternatives were not designed as longhand sub-properties.
  • inline-size and block-size already existed as conceptual terms, and they read better than the flipped size-inline and size-block. Without a shorthand property to base the names off, there’s little reason to change them.
  • Now that we are adding a size shorthand, the pattern seems inconsistent – but it wasn’t even a consideration previously.
https://css.oddbird.net/logical/research/
Changes to: CSS Scope & Encapsulation

Scope is part of Interop 2025

CSS Scope & Encapsulation »

Show full content

Scope is part of Interop 2025

CSS Scope & Encapsulation »


progress:

Scope is shipping as part of Interop 2025.

Authors often complain that CSS is “globally scoped” – so that every selector is compared against every DOM element.

There are several overlapping concerns here, based on a wide range of use-cases – and they can quickly become confused. That has lead to a wide array of proposals that are sometimes working towards different goals.

Both shadow-DOM and the abandoned “scope” specification were focused around strong isolation. Shadow-DOM in particular creates persistent DOM-defined boundaries, that impact all style rules.

Meanwhile, most of the user-land “scope” tools for CSS have a much lighter touch. I’ve been mainly interested in those low-isolation, namespacing problems.

https://css.oddbird.net/changelog/scope/2025-03-25t185952000z/
New page: CSS Logical Shorthands

New page added

CSS Logical Shorthands »

Show full content

New page added

CSS Logical Shorthands »


The CSS Working Group recently resolved to add a size shorthand for setting both the width and height of an element. Many people asked about using it to set the ‘logical’ inline-size and block-size properties instead. But ‘logical shorthands’ have been stalled in the working group for years. Can we get them unstuck?

You can support our efforts

Next steps
  1. Research what properties would be impacted
  2. Choose syntax for individual properties
  3. Develop lexical toggle to allow default-logical styles
CSS Working Group discussion

Fantasai opened the original issue in 2017, when publishing the First Public Working Draft of CSS Logical Properties: [css-logical] Flow-relative syntax for margin-like shorthands. Here are some key comments:

Other resources
https://css.oddbird.net/logical/
New page: Cascade Layering of HTML Linked Style Sheets

New page added

Cascade Layering of HTML Linked Style Sheets »

Show full content

New page added

Cascade Layering of HTML Linked Style Sheets »


Authors

Miriam Suzanne

Participate

There are discussion threads on both the WHATWG and CSSWG github repos:

Introduction

Cascade layers allow CSS authors to create explicit cascade priority tiers, and place style sheets into those tiers using either a block at-rule (@layer) or a condition on the @import rule (layer or layer(layer-name)). The details are defined in CSS Cascading and Inheritance Level 5.

One of the primary use-cases for cascade layering is to manage the priority of third-party CSS (libraries and design systems) in relation to site-specific styles. However, there are many situations where authors do not want to use @import for performance reasons, or cannot use @import because of build tooling. Providing this functionality on the HTML <link> tag would bring it in better alignment with the CSS import functionality.

Goals

The motivating use-case is quite specific: a syntax for assigning linked style sheets to a cascade layer.

This should:

  • Allow assigning styles to either named or anonymous layers
  • Invalidate links if a layer name is invalid, or cascade layers are unsupported
Non-goals

This raises issues of compatibility. For some period of time, there will be browsers that do not support the new layering syntax. If those browsers apply the linked styles without applying the appropriate layer rules, the results will be unexpected and unreliable.

Ideally, browsers without support for a layering syntax should not load layered style sheets. With the appropriate tools for support-conditioned links, authors could then choose to load alternative style sheets.

However, that would currently require changes to the media or type attributes. Looking farther out, the WHATWG discussion seems to prefer a separate/new attribute for support conditions, since they are not technically treated as media queries in CSS. The details of that proposal seem to be blocking progress on the less controversial layer attribute proposed below. For that reason, I’ve set aside support conditions as a non-goal for this document.

Authors can polyfill conditional loading as desired, with a small amount of javascript to detect feature support, and alter the existing media attribute as needed.

Proposed solution: the layer attribute

The layer attribute applies to both link and style elements, with the following behaviors:

  • missing: The style sheet is not assigned to any cascade layer
  • empty string: The style sheet is assigned to an anonymous layer
  • <layer-name>: The style sheet is assigned to the named cascade layer. As with @import, the layer is added to the layer order even if the link fails to load the style sheet, but is subject to any other link conditions (such as media). This is just as if the layer was declared by an @layer rule wrapped in the appropriate conditional group rules.
  • none of the above: The link is invalidated, and the style sheet should not load.

This is designed to match the behavior of the layer keyword and layer() function in the CSS import syntax.

Note that the empty attribute syntax sets the value implicitly to the empty string – so the layer attribute can be applied as though it is a boolean attribute, resulting in an anonymous layer.

Key scenarios

Existing style sheet links without the layer attribute should continue to work without any changes:

<!-- no layering is applied -->
<link rel="stylesheet" href="example.css" />
<link rel="stylesheet" href="screen.css" media="screen" />

When working with resets, third-party libraries, and design systems, authors may want to apply layers on-import. This is especially common since site authors may not have access to edit the style sheet, but still want to manage cascade priority of the resulting styles:

<!-- site.css can define layer order, and internal layering -->
<link rel="stylesheet" href="site.css" />

<!-- external styles can be assigned existing layers, or create new ones -->
<link rel="stylesheet" href="library/styles.css" layer="framework.library" />
<link rel="stylesheet" href="reset.css" layer="reset" />
<link rel="stylesheet" href="design/system.css" layer="framework.system" />

In some cases, authors might want to wrap styles in an anonymous layer that can’t be accessed later. This can be used to enforce code order, or simply move a reset to the lowest cascade position:

<!-- anonymously push a reset into a lower layer -->
<link rel="stylesheet" href="site.css" />
<link rel="stylesheet" href="reset.css" layer />

<!-- enforce layer rules are grouped in relevant files -->
<link rel="stylesheet" href="defaults.css" layer />
<link rel="stylesheet" href="patterns.css" layer />
<link rel="stylesheet" href="components.css" layer />

When given an invalid layer name the style sheets will not apply. That ensures a typo doesn’t apply un-layered style contents which would have the highest cascade-priority.

Detailed design discussion

While it would be nice to ship this along with a broader import-condition syntax, the details of that syntax have been contentious. However, the need for a new layer attribute, and the general shape of that attribute have not been controversial.

It might be worth noting that other CSS features are likely to need similar dedicated attributes. For example, the @scope block rule may warrant a scope() import rule and scope attribute. However, this is the exception for CSS – and only needed for larger architectural features. It is not likely to become a common request.

Stakeholder Feedback / Opposition

Authors have been asking when it will happen. Implementors have been positive in the linked discussions. Now that there is an explainer to reference directly, I will ask for more explicit standards positions:

  • Blink: TBD
  • WebKit: TBD
  • Gecko: TBD
References & acknowledgements

Many thanks for valuable feedback and advice from:

  • Discussions in the CSSWG & WHATWG
  • Simon Pieters
  • Florian Rivoal
  • Adreu Botella
https://css.oddbird.net/layers/link-layer/
Changes to: CSS Mixins & Functions Explainer

Changes to variable scope and function result syntax

CSS Mixins & Functions Explainer »

Show full content

Changes to variable scope and function result syntax

CSS Mixins & Functions Explainer »


Author

Miriam Suzanne

(Based heavily on a custom-function proposal by Tab Atkins)

Intro

In order to reduce code repetition, ensure consistency across a project, and encourage best practice, authors have often turned to third-party CSS pre-processors (Sass, Less, PostCSS, Stylus, etc) to define custom reusable ‘macros’. These generally fall into two categories:

  • Functions return CSS values – like a string, color, or length. They can only be used inside the value space of a CSS property.
  • Mixins return CSS declarations or entire rule blocks. They can only be used outside the value space of a CSS property.

CSS already provides a wide range of built-in functions, such as calc(), minmax(), and many more. Ideally, custom functions would work in a similar way, but prefixed with a dashed-ident to avoid future compatibility issues. For a simple example:

@function --negative (--value) {
result: calc(-1 * var(--value));
}

html { padding: --negative(var(--gap)); }

CSS does not yet have built-in mixins, though several have been proposed in discussions of this feature. A simple mixin might look something like this:

@mixin --button (--face, --text, --radius) {
--background: var(--face, teal);
--color: color-mix(in lch, var(--text, white) 85%, var(--background));
--border-color: color-mix(in lch, var(--text, white) 80%, var(--background));

@result {
background: var(--background);
border: medium double var(--border-color);
border-radius: var(--radius, 3px);
color: var(--color);
padding: 0.25lh 2ch;
}
}

button[type='submit'] { @apply --button(rebeccaPurple); }
button.danger { @apply --button(maroon); }
Discussion

There are several other relevant discussions in the CSS Working Group, that predate this proposal:

(If there are more I haven’t found, please let me know.)

Summary & Goals

Features often change as they move from (generally imperative) pre-processors into CSS – taking on different affordances and constraints appropriate for a declarative, client-side language:

  • How would CSS-native mixins and functions differ from pre-processors?
  • What extra functionality or limitations come from providing these features in the browser?

From a language/implementation perspective mixins and functions are entirely distinct features – they live at different levels of the syntax, and come with different complications. If we pursue both, we likely want to define them at different levels of a specification, or even in different specifications.

Removing the reliance on pre-processors would further simplify maintenance for CSS authors, while providing new client-side functionality:

  • Passing cascaded custom-properties as arguments.
  • Adding media/support and other client-side conditions.

My goal here is to explore what would be possible with each feature, where we could re-use syntax between them, and how we might move forward with implementing them.

I am not expecting this to be the final shape for either feature, but I want to capture the state of the conversation, and help move it forward. If these features are officially adopted by the working group, further development can be broken into individual specs and issues.

Author Interest

There is some (incomplete) data from the HTTP Archive project that can help us understand how authors are using Sass currently:

I also ran a small survey on Mastodon:

“What are the most common custom functions or mixins that you define/use in a css pre-processor?”

The answers included:

  • (Functions) Conversion from pixel to rem units
  • (Functions) random number generators
  • (Functions) Color contrast
  • (Mixins) Named shorthands for common media queries
  • (Mixins) Generating output from object for-each loops (like Sass Maps)
  • (Mixins) Reusable component styles
  • (Mixins) Complex solutions, like scroll-shadows or gradient text
  • (Both) Fluid typography settings
  • (Both) Complex calc() shorthands for various situations

Some of these would be possible to achieve in CSS with a declarative syntax, without additional new functionality. Others (like loops) would require imperative control structures.

While some of these (like random()) are already being discussed for built-in functions, others (like color-contrast()) may be simpler to solve in user-space. It has been very difficult for the CSSWG to settle on a long-term solution for the entire platform, while an individual team would be more able to change their approach gradually over time. By capturing that logic in a single place (like a custom function), many changes could be made without any invasive re-write of the code base.

The ability to declare this logic in CSS rather than a pre-processor would provide several benefits:

  • Reduce the external dependencies and build steps required in order to generate the code
  • Reduce the file size delivered from the server (though this may be negligible after compression & increased client-side processing)
  • use custom properties as arguments so that the mixins or functions could respond to changes in the cascade
  • use media/container/support conditions as part of the internal logic
Defining parameters

Both functions and mixins rely on a <parameter-list> syntax. Each <parameter> in the <parameter-list> consists of three parts:

  • <name> (required) which is a dashed-ident
  • <syntax> (default: *) similar to the syntax descriptor in @property
  • <default-value> (default: guaranteed invalid) which is any value that matches the syntax

Defining all three aspects in the function prelude (name, type, and default) can make the syntax over-complicated. My initial proposal included special @property-like descriptor blocks to make that possible.

Since then, the discussion has moved towards a more concise approach using a comma-separated list.

Authors can provide names only:

@function --my-function(--param-a, --another-param) { … }

Optionally, they can also provide a default value:

@function --my-function(
--param-a: 1em,
--another-param: 'this is a string'
) { … }
Note:

Since the list is comma-separated, this would require better handling of arguments with commas in CSS.

Finally, authors could define a syntax for any parameter, using the type() function along side the name. This would work with or without default values:

@function --my-function(
--param-a type(string),
--another-param type(length): 1em
) { … }
Defining a function: the @function rule

In order to define a custom function, we need several bits of information:

  • function-name (required)
  • parameter-list (optional - see above)
  • Some amount of internal logic using function-rules
  • A returned result value

The proposed syntax (with a few adjustments) could look something like:

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

  result: <result>;
}

The function-name is a dashed-ident. If multiple functions have the same name, then functions in a higher cascade layer take priority, and functions defined later have priority within a given cascade layer. This matches the behavior of other name-defining at-rules.

It may also be useful to define an intended ‘return type’ (e.g. color or length) for the function, so that it can be validated at parse time. Like custom properties, there is still a chance that a function’s output will be invalid at computed value time, but we can at least ensure that the function is intended to return an appropriate syntax for the context where it is being called.

Extending the above syntax, I would imagine re-using the type() function in the prelude:

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

  result: <result>;
}

I would expect <syntax> to allows the same subset of CSS Types provided by the syntax descriptor of the @property rule. Maybe it would be possible to remove the requirement for quotes around a syntax in this context?

Returning values

There have been several syntax options discusses for returning a <result> value, but it seems to me like the simplest and most familiar would be a descriptor called something like result or output. This would help re-enforce the declarative nature of functions, since it can be treated similar to other declarations: the last result is used if multiple are present.

Like custom properties:

  • The <result> can contain any valid CSS value-space syntax
  • This value has invalid at computed value time behavior

Since functions exist in the value space, <function-rules> will not contain any other (non-custom) CSS properties, so the single result descriptor should stand out. If multiple results are encountered, the last result takes precedence (consistent with other descriptors and properties). This is discussed in more detail below.

Note:

Tab covers declarative execution in the CSSWG issue with a bit more detail.

Function rules

The <function-rules> can include custom property declarations (which are scoped to the function), as well as conditional at-rules (which may contain further nested custom properties and results). Element-specific conditions (such as container queries) would be resolved for each element that calls the function.

My assumption would be that custom properties defined inside the function are not available on elements where the function is called. However, it’s clear that authors will expect to reference external custom properties from inside functions – using some variation of dynamic scope, and ‘shadowing’ behavior.

As far as I can tell, only custom properties, args/variables, and conditional rules are useful inside a function definition. Functions have no output besides their returned value, so nested selectors, built-in properties, and name-defining rules are not necessary or meaningful. I don’t think there’s any need for these things to invalidate the entire function, but they should be ignored and discarded.

An example function using conditional rules to return one of multiple values:

@function --sizes(
--s type(length),
--m type(length),
--l type(length),
) returns type(length) {
--min: 16px;

@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));
}
}

Some functions will also want access to contextual variables on the calling elements. To avoid fully dynamic scoping of custom properties, Tab has proposed a second list of properties that should be available in the function:

@function --my-function (--arg1, --arg2) using (--var1, --var2) {
/* --arg1 and --arg2 can be provided as arguments */
/* --var1 and --var2 will shadow identically-named variables
in the calling context */
}
Note:

Tab covers variable scoping in the CSSWG issue with a bit more detail.

Calling functions

Custom functions can be called from the value space of any property, with the name of the functions, followed by parenthesis and a comma-separated list of arguments:

button {
background: --contrast(pink, 0.7);
}

If we do (eventually) want to support named arguments, it would ideally use a familiar declaration syntax:

button {
background: --contrast(--color: pink; --ratio: 0.7);
}

If positional and named arguments are allowed in the same function call, the common convention is to require all positional values come before any named values to avoid confusion:

button {
background: --contrast(pink; --ratio: 0.7;);
}

We need to allow a broad syntax for argument values – including values that contain commas. There’s an active discussion about the best way to handle this more generally in issue #9539. Custom functions should use whatever solution is agreed on there.

Putting it all together

Adapting the fluid ratio function above to the proposed syntax:

@function --fluid-ratio(
--min-width type(length),
--max-width type(length),
) returns type(percentage) {
--min: var(--min-width, 300px);
--max: var(--max-width, 2000px)l
--scale: calc(var(--max) - var(--min));
--position: calc(100vw - var(--min));
--fraction: calc(var(--position) / var(--scale));

@return clamp(
0%,
100% * var(--fraction),
100%
);
}

p {
font-size: calc-mix(--fluid-ratio(375px; 1920px), 1rem, 1.25rem);
padding: calc-mix(--fluid-ratio(375px; 700px), 1rem, 2rem);
}

We could also consider moving the mix() logic into the function:

@function --fluid-mix(
--min-value type(length),
--max-value type(length),
--from-width type(length),
--to-width type(length)
) returns type(length) {
--from: var(--from-width, var(--fluid-min, 375px));
--to: var(--to-width, var(--fluid-max, 1920px));
--scale: calc(var(--to) - var(--from));
--position: calc(100vw - var(--from));
--fraction: calc(var(--position) / var(--scale));
--progress: clamp(0%, 100% * var(--fraction), 100%);

@return calc-mix(var(--progress), var(--min-value), var(--max-value));
}

p {
font-size: --fluid-mix(1rem, 1.25rem);
padding: --fluid-mix(1rem, 2rem, 375px, 700px);
}
Defining a mixin: the @mixin rule

Rather than returning a single value, mixins return entire declarations and potentially entire nested rule blocks. While much of the function syntax could be re-purposed, we would need an additional way to manage property scoping – clearly marking what rule blocks are internal, and which should be part of the output.

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

Again, when there are multiple mixins that use the same name, the last mixin with that name takes precedence.

Mixin rules and output

The simplest approach to nested rules and output would be to treat the inside of a mixin definition the same as any rule-block nested context. Anything we can put inside a rule block can be put inside a mixin, and will be output where the mixin is called (with any parameters being replaced first). This will work for many simpler cases:

@mixin --center-content {
display: grid;
place-content: center;
}

.page {
@apply --center-content;
/*
display: grid;
place-content: center;
*/
}
@mixin --clearfix {
&::after {
display: block;
content: "";
clear: both;
}

@supports (display: flow-root) {
display: flow-root;

&::after { display: none; }
}
}

.float-container {
@apply --clearfix;
/*
&::after {
display: block;
content: "";
clear: both;
}

@supports (display: flow-root) {
display: flow-root;

&::after { display: none; }
}
*/
}

This approach doesn’t allow the mixin to contain any internal logic scoped to the mixin itself. Mixins should be able to use internally scoped custom-properties, and also optionally output custom properties as part of the returned rule block. As things stand, this doesn’t seem relevant to anything other than custom properties. Built-in properties, selectors, and at-rules are only useful for their output.

Given that this issue is specific to custom properties, we could consider a flag such as !private. That flag could be interesting for custom properties in other contexts, but I won’t follow that path unless there’s interest. Alternatively, we could explicitly mark blocks of content with either @output or @private at-rules.

Applying mixins: the (new) @apply rule

In order to apply a mixin, we use an @apply rule:

@apply <mixin-name> [(<argument-list>)]?

The <argument-list> syntax should ideally match the function argument notation.

When the mixin is resolved, the output of the mixin is inserted where the apply rule was called:

/* declaration */
.float-container {
@apply --clearfix;
}

/* result */
.float-container {
&::after {
display: block;
content: "";
clear: both;
}

@supports (display: flow-root) {
display: flow-root;

&::after { display: none; }
}
}

There is an additional question about how to handle mixin output at the top level of the document (not nested inside a selector):

@apply --center-content;

As long as there is a selector wrapping the output, this should not be an issue. Even if that selector is simply the parent reference &, that has a well-defined behavior at the top level of documents – referring to the current :scope. However, if the result is bare declarations without any selector, they should be discarded and ignored.

Another example, from a Sass mixin I’ve used on occasion:

@mixin --gradient-text(
--from-color type(color),
--to-color type(color),
--at-angle type(angle),
) {
--to: var(--to-color, teal);
--from: var(--from-color, mediumvioletred);
--angle: var(--at-angle, to bottom right);
color: var(--from, var(--to));

@supports (background-clip: text) or (-webkit-background-clip: text) {
--gradient: linear-gradient(var(--angle), var(--from), var(--to));
background: var(--gradient, var(--from));
color: transparent;
-webkit-background-clip: text;
background-clip: text;
}
}

h1 {
@apply --gradient-text(pink, powderblue);
}
Layers of complexity

The popular Sass functions and mixins demonstrate a range of different input needs, from relatively static shorthands, to fully imperative control structures.

Simple shorthands

A clearfix mixin often has no exposed ‘parameters’, and no internal logic. When the mixin is invoked, it will output the same code every time. This is useful for maintaining DRY code (Don’t Repeat Yourself),

Static mixins like this end up very similar to ‘utility classes’ such as .clearfix. However, mixins still have the advantage that they can be applied in CSS, rather than HTML. The need for CSS control comes into focus when combined with @media/@container and other conditional logic. There is currently no way in CSS to write this code without defining all the custom properties twice:

.dark-mode {
--background: black;
--text: white;
/* more custom props as needed… */
}

@media (prefers-color-scheme: dark) {
html:not(.light-mode) {
--background: black;
--text: white;
/* more custom props as needed… */
}
}

Most of the existing proposals around this use-case would combine conditional logic with selector logic, so that both can be defined at once. In Sass, we might fix this instead by providing a dark-mode mixin that can be used multiple times to output the same declarations with only minimal repetition:

@mixin dark-mode {
--background: black;
--text: white;
/* more custom props as needed… */
}

.dark-mode {
@include dark-mode;
}

@media (prefers-color-scheme: dark) {
html:not(.light-mode) {
@include dark-mode;
}
}

Using Container Style Queries might also be an option here. They can be somewhat mixin-like, but come with all the limitations of container queries. If we set a custom property --mode on the root html element, we have to assign properties on a different element than we query:

.dark-mode {
--mode: dark;
}

@media (prefers-color-scheme: dark) {
html:not(.light-mode) {
--mode: dark;
}
}

@container style(--mode: dark) {
/* The html element cannot query itself */
body {
--background: black;
--text: white;
/* more custom props as needed… */
}
}

That can cause several problems:

  • There are optimizations and features specific to the root, that can’t be replicated on other elements.
  • In other component contexts, it’s likely to require extra markup.

While no-parameter mixins like these are somewhat common, it’s much less common to have a function without parameters, since a simple value can be captured in a variable or custom property instead.

Built-in conditions

It can also be useful to provide mixins that have no author-facing parameters, but still contain internal logic and conditional statements – using @supports, @media, or @container:

@mixin gradient-text {
color: teal;

@supports (background-clip: text) or (-webkit-background-clip: text;) {
background: linear-gradient(to bottom right, teal, mediumvioletred);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
}
}

A mixin like this might even reference external values by relying on custom properties without accepting explicit override parameters:

@mixin gradient-text {
--gradient-text-start: var(--color-primary, teal);
--gradient-text-end: var(--color-complement, mediumvioletred);
color: var(--gradient-text-start);

@supports (background-clip: text) or (-webkit-background-clip: text;) {
background: linear-gradient(
to bottom right,
var(--gradient-text-start),
var(--gradient-text-end)
);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
}
}
Accepting parameters

The most common reason to use a function or mixin is the ability to define parameters that alter the output based on different input. For example, a darken() function would accept two parameters: a color, and an amount to darken that color.

In many cases (like darken()) the internal function logic can be represented by an inline calculation using existing CSS features. In those situations, a custom function could still provide more concise and easy-to-use shorthand around a more complex calc() or relative color adjustment.

Parameter conditions

Once we allow both parameters and conditional logic, the next step would be to allow parameters to be used in the conditions themselves. For example:

@mixin button(--style: outline, --shape: pill) {
@when arg(--style: outline) {
border: medium solid;
color: teal;
} @else {
background: teal;
color: white;
}

@when arg(--shape: pill) {
border-radius: 50%;
}
}
Imperative control flow

Some use-cases require more complex ‘flow control’ such as loops. For example, a combination of mixins might generate a full color-palette based on a single origin color. In Sass, it might looks something like this:

@use 'sass:color';
@use 'sass:math';

@mixin tint-shade($color, $name, $steps: 2) {
--#{$name}: #{$color};

$step: math.div(100%, ($steps + 1));

@for $i from 1 through $steps {
$amount: $step * $i;
--#{$name}-t#{$i}: #{color.mix(white, $color, $amount)};
--#{$name}-s#{$i}: #{color.mix(black, $color, $amount)};
}
}

@mixin theme($color, $type: 'complement') {
/* generate tints and shades for the main color */
@include tint-shade($color, 'primary');

@if $type == 'complement' {
$complement: color.adjust($color, $hue: 180deg);
@include tint-shade($complement, 'complement');
} @else if $type == 'triad' {
/* logic for triad themes… */
}
/* etc… */
}

html {
@include theme(blue);
}

The resulting output CSS would be:

html {
/* generate tints and shades for the main color */
--primary: blue;
--primary-t1: #5555ff;
--primary-s1: #0000aa;
--primary-t2: #aaaaff;
--primary-s2: #000055;
--complement: yellow;
--complement-t1: #ffff55;
--complement-s1: #aaaa00;
--complement-t2: #ffffaa;
--complement-s2: #555500;
/* etc… */
}

I think it would be reasonable to draw a boundary here, since CSS is a declarative language. Adding imperative flows would likely cause confusion around the execution model.

Detailed discussion and open questions Other result syntaxes for functions

Both Lea and I have noted that it would be useful if authors could rely on cascade ‘order of appearance’ to provide ‘fallback’ return values. Sadly, however, that sort of parse-time fallback is not possible with dynamic computed-value-time features like custom properties or functions.

I initially proposed an at-rule syntax (@return), arguing that:

  • It helps distinguish the final returned value from any internal logic like custom properties and nested rules
  • Result is not a property, but looks a lot like one

However, result does act like a property in many ways, and would help to re-enforce our familiarity with declarative execution. While many imperative languages allow an ‘eager’ first-takes-precedence function return, CSS and other declarative languages generally uses a last-takes-precedence approach. For the same reason, we should avoid active words like return that suggest the evaluation is linear and can be cut short.

François Remy has proposed setting a custom property with the same name as the function, and that property is treated as the resulting value. Lea Verou suggested making the property name customizable in the prelude.

I prefer a syntax that is more consistent and reliable. I don’t see any utility that comes from allowing this functionality to be renamed in each function, or requiring that name to be determined by authors, or putting it in the author’s custom-ident name space. Those all seem to me like ways of inviting typos and confusion, without any clear gain.

Matching the function name seems to me extra fragile – as you could never rename one without also updating the other. Still, either approach could work, and provide the same basic behavior. We can continue to bike-shed the details.

Passing nested content to mixins

Another common feature of Sass mixins is the ability to pass nested content blocks into a mixin, and have the mixin place that content in a specific context. This seems like a feature that could be supported in CSS as well, but would require another mixin-specific at-rule (or similar placeholder). I’ll call it @nested for now:

@mixin --media-medium {
@media screen and (env(--small) < inline-size < env(--large)) {
@nested;
}
}

.grid {
@apply --media-medium {
padding: var(--padding-medium, 1em);
}
}

The expected behavior would be the same as writing:

.grid {
@media screen and (env(--small) < inline-size < env(--large)) {
padding: var(--padding-medium, 1em);
}
}

This seems like something that could be added later, if necessary.

Invalid function fallbacks

Sadly, last-takes-precedence @return behavior doesn’t provide the same benefit here that it has in the cascade – where invalid declarations can be discarded at parse time, falling back on previously declared values. In order to achieve that, we would need to limit functions so that they are the only value in a property. I don’t think that tradeoff makes sense for the use-cases I’ve seen.

I’m also not sure it makes sense to provide function-defined fallback values to return when arguments provided have invalid syntax. Ideally, function fallbacks would be modeled after variable fallbacks – established where the function is called, rather than where it is defined. It’s hard to see where this would fit in the proposed syntax.

One option would be a var()-like wrapper function:

button {
background: fallback(--contrast(pink; 0.7), black);
}

We could even use the existing var(), but that would result in functions and custom properties sharing a single namespace, which might not be ideal. Maybe the proposed function for first-supported() would also be an option that has broader use? This likely needs more bike-shedding.

Using parameters in conditional rules

Above, I used an example with conditional output using media queries inside the function. Authors may reasonably wish to take this farther and use parameters to define the media queries themselves:

@function --media(
--breakpoint: length,
--below: length,
--above: length
) {
@media screen and (width < var(--breakpoint)) {
result: var(--below);
}
@media screen and (width >= var(--breakpoint)) {
result: var(--above);
}
}

This is a very common use of pre-processor mixins, and a common use-case for the proposed inline if() and media() functions as well.

As I understand it, that will not be possible as written above, for the same reasons var() is not currently allowed in media-query conditions. However, the issues are specific to cascaded values that need to be resolved at computed value time. Passing static arguments from a parameter should not pose the same problem.

If we had a new way of accessing values passed in – I’ll use arg() for the sake of argument – simple value substitution should be possible:

@function --media(
--breakpoint: length,
--below: length,
--above: length
) {
@media screen and (width < arg(--breakpoint)) {
result: var(--below);
}
@media screen and (width >= arg(--breakpoint)) {
result: var(--above);
}
}

html {
/* this works fine, since the argument is accessed with `var()` */
padding: --media(40em, 0, var(--padding, 1em));

/* this errors, since the argument is accessed with `arg()` */
margin: --media(var(--break, 40em), 0, 1em);
}

In the above example, the padding declaration would be valid since a static value can be passed along to the media query arg() – but the margin declaration would fail since it supplies a custom property to a media query condition.

It’s not clear to me if parameters used this way would need to be explicitly marked in advance for any reason? As proposed here, it would be up to function authors to document and communicate which parameters can accept cascading variables, and which can not.

Argument conditions and loops

With both mixins and functions it can be useful to have conditions based on the arguments passed in. For example, we might want to pass in one of several established keywords, and return a different value depending which keyword is used:

@function --link(
--theme: *;
) {
@when (arg(--theme): light) {
result: env(--link-light);
} @else {
result: env(--link-dark);
}
}

It’s not clear to me if the proposed @when/@else features can be adapted to this use-case, or if it would need to be a distinct set of similar flow controls.

Similarly, as we saw in the tint-shade example earlier, it can be useful to loop over a set number of repetitions (for loop) or a set list of items (each loop).

While these would be helpful features for authors, they are not required for (or dependent on) an initial implementation of mixins or functions. They feel like distinct features that would go well together.

Can we allow the <calc-sum> syntax?

This question was raised by Brandon McConnell in the ‘Declarative Custom Functions’ issue (see point 5, even though it’s not specific to recursion). The goal is to provide custom functions that take raw calc expressions, without being explicitly wrapped in a nested calc() function, similar to the way other math functions work:

.item {
width: min(100% - 1em, 30em);
}

On the one hand, custom property substitution makes it trivial to capture expressions, and later call them inside a calc() function. This already works:

html {
--l: 100% - 50%;
background: hsl(0deg 100% calc(var(--l)));
}

To take it farther, we would need to expose the <calc-sum> grammar as a valid syntax for authors to use.

It might also be worth considering what other syntax/types would be useful to expose – either for parameters specifically, or for property registration more generally. It seems ideal to me if those lists can be kept in alignment.

What about @extend?

In Sass, mixins without parameters also overlap with the @extend feature, which is used to combine related classes – one as an ‘extension’ of the other. In most cases, that has the same intended result as a no-parameter-mixin:

/* extends */
.error {
border: thin solid maroon;

&:hover {
background-color: #fee;
}
}

.error--serious {
@extend .error;
border-width: thick;
}

/* mixin */
@mixin error {
border: thin solid maroon;

&:hover {
background-color: #fee;
}
}

.error--serious {
@include error;
border-width: thick;
}

The difference is that a class definition can be compiled from multiple rule blocks in different style sheets, while a mixin generally has one centralized definition. This is part of the reason extensions have become less common in Sass – it can be difficult to reason about their impact. For now, I think mixins would provide the similar functionality without the same complexity.

If we are interested in exploring @extend at some point, Tab has already written an unofficial draft specification that we can build from.

Can functions be chained, or call themselves?

I would expect that it should be possible to chain function/mixin calls together. A theme-generating mixin should be able to reference a single-color generating mixin or function internally.

It’s less clear to me if recursive function calls are possible or necessary. There are likely use-cases for recursion as a form of looping, but I’m not sure how central they are. This doesn’t seem like a feature requirement in level 1.

Keyframe-based mixins for interpolated values?

There has been a lot of recent discussion around interpolating values between breakpoints for e.g. responsive typography. Conceptually, animation keyframes work well for defining the steps involved – but in this case the result is not technically animated, and interpolated values should ideally not be removed to the animation origin.

To get around that, the most recent proposals involves a new property (tentatively interpolate) that would accept a keyframes name and timeline, then ‘expand in place’ to represent the declarations in the referenced @keyframes rule.

@keyframes typography {
from {
font-size: 1.2em;
line-height: 1.4;
}
to {
font-size: 3em;
line-height: 1.2;
}
}

h2 {
/* declaration, this is all pseudo-code */
interpolate: typography --container-size ease-in;

/* result, with interpolated values */
font-size: /* interpolated… */;
line-height: /* interpolated… */;
}

Alan Stearns has pointed out in conversations that this is a very mixin-like behavior, and suggested treating keyframes as an existing form of mixin, rather than a new property. Given the same keyframes above, we could consider a syntax like:

h2 {
/* mixin, this is all pseudo-code */
@apply typography(--container-size; ease-in);

/* result, with interpolated values */
font-size: /* interpolated… */;
line-height: /* interpolated… */;
}

If that clutters the mixin namespace, another approach might be requiring dashed-ident mixin names, and providing some built-in mixins such as:

h2 {
/* declaration, this is all pseudo-code */
@apply keyframes(typography; --container-size; ease-in);

/* result, with interpolated values */
font-size: /* interpolated… */;
line-height: /* interpolated… */;
}
Prior art The @apply Rule (abandoned) Links:

Why I abandoned @apply by Tab Atkins-Bittner

At one point, there was a plan for custom properties to act as a form of mixin, using the @apply rule. That proposal was abandoned as the wrong approach for several related reasons:

  • Custom properties are value-level syntax, while mixins are declaration-level
  • It doesn’t make sense for mixin definitions to be passed around in the cascade

These are not difficult issues to avoid. I’m working from the premise that:

  • Both function and mixins should be defined globally, and not rely on any element-aware aspects of the cascade.
  • Similar to e.g. @keyframes, function and mixin definitions would still resolve name conflicts using global cascade features like layers and order of appearance.
  • Functions are applied in the value space, while mixins are applied in the declaration space.
Container Style Queries (partially implemented) Links:

CSS Containment Module Level 3

The style() feature of @container can sometimes be used to approximate mixin behavior. There are several recent posts and articles written about that approach. However, style queries share the limitation of other container queries: we can’t style the container being queried.

Container queries are designed as a conditional selector mechanism, for responding to changes in context. The ancestor/descendant limitation is required for browsers to separate selector-matching from value-resolution on a given element.

However, mixins do not alter selection, they only ‘bundle’ existing CSS rules and declarations for re-use. Ideally, these two features should work well together, so that contextual conditions can change the arguments passed to a given mixin.

Custom Properties (implemented) Links:

CSS Custom Properties In The Cascade by Miriam Suzanne

We can also use custom properties to approximate some basic mixins and functions. While these tricks can be useful, they involve significant complexity, caveats, and limitations:

  • Each ‘function/mixin’ and ‘argument’ is a custom property, which can only have a single resolved value per element
  • Arguments are substituted in the function/mixin before the computed value inherits, so the logic has to be defined on every element that should re-calculate a result
Mixins and functions in pre-processors Links:

In addition to parameters, Sass mixins can accept content blocks. An example from the documentation:

@mixin hover {
&:not([disabled]):hover {
@content;
}
}

.button {
border: 1px solid black;
@include hover {
border-width: 2px;
}
}

That might be a useful feature for CSS mixins as well. It would be required for the use-case of creating named conditions. That use-case may also be solved by the proposed @when rule and ‘custom media queries’ feature.

Sass provides some built-in core functions, but (so far) does not provide core mixins. Likely for that reason, the HTTP Archive report lists several commonly-used built-in functions (if(), and darken()), but only the most commonly used custom mixin name (clearfix).

Existing Proposal for Custom Functions

In July of 2022, Johannes Odland proposed ‘Declarative custom functions’ in the CSS Working Group issue tracker. Since then, the proposal has gone through several revisions and updates.

The current (2023-08-08) proposal in that thread suggests that:

  • Functions would be resolved at the same time as variable substitution
  • Function parameters defined with a CSSOM ‘syntax’ can be validated at parse time (like @property-registered variables)
  • This would be a declarative version of the more full-featured Houdini API feature

There are also several example use-cases, such as this function for fluid typography:

@custom-function --fluid-ratio(
--min-width,
--max-width
) {
result: clamp(
0%,
100% * (100vw - var(--min-width)) / (var(--max-width) - var(--min-width)),
100%
);
}

p {
font-size: mix(--fluid-ratio(375px, 1920px), 1rem, 1.25rem);
padding: mix(--fluid-ratio(375px, 700px), 1rem, 2rem);
}
Unit division in math functions:

In addition to the new syntax proposed here, browsers would also need to implement unit-division in math functions for this use-case to work as shown.

Or a function for generating checkerboard background-images:

@custom-function --checkerboard(--size) {
result: linear-gradient(
45deg,
silver 25%,
transparent 25%,
transparent 75%,
silver 75%
)
0px 0px / var(--size) var(--size),
linear-gradient(
45deg,
silver 25%,
transparent 25%,
transparent 75%,
silver 75%
)
calc(var(--size) / 2) calc(var(--size) / 2) / var(--size) var(--size);
}

.used {
background: --checkerboard(32px);
}

For these use-case, custom functions could be a simple wrapper for inserting parameters into existing functions like calc(). Tab Atkins has suggested a math-only version of this would be simplest to implement. While that might be a useful first-step, it quickly falls short of the use-cases I’ve seen. I would prefer to start with a more fully-featured approach, and work backwards to an attainable level 1 implementation if needed.

In addition to some bike-shedding of the syntax, there are several more open questions in the thread:

  • Can authors provide a fallback output for invalid arguments?
  • Would it be helpful to include default parameter values in the function definition?
  • Can function authors define internally-scoped custom properties?
  • Can authors use conditional at-rules inside the function logic?
  • Can functions expose a parameter that accepts bare calculations (without calc() syntax) similar to clamp() etc?
  • Can functions perform recursive function calls?
  • Can functions be called with named (rather than positional) arguments?

I hope to expand on this proposal, and explore some of those questions along the way.

Acknowledgments

This proposal is based on an existing discussion with input from:

  • Johannes Odland
  • David Baron
  • Brian Kardell
  • Tab Atkins-Bittner
  • @jimmyfrasche
  • Brandon McConnell
  • Lea Verou

I’ve also incorporated feedback along the way from:

  • Nicole Sullivan
  • Anders Hartvoll Ruud
  • Rune Lillesveen
  • Alan Stearns
  • Yehonatan Daniv
  • Emilio Cobos Álvarez
  • François Remy
  • Steinar H Gunderson
  • Matt Giuca
Todo
https://css.oddbird.net/changelog/sasslike/mixins-functions/2024-02-11t202037000z/
Changes to: CSS Mixins & Functions Explainer

Updates to parameter syntax and variable scope

CSS Mixins & Functions Explainer »

Show full content

Updates to parameter syntax and variable scope

CSS Mixins & Functions Explainer »


This page has more recent changes available. For more details, see:

https://css.oddbird.net/changelog/sasslike/mixins-functions/2023-12-01t214011000z/