Skip to content

[css-pseudo] ::before(<ident>) / ::after(<ident>): A possible path forwards for multiple gencontent pseudos? #13860

@LeaVerou

Description

@LeaVerou

The problem

::before and ::after are used in a variety of use cases:

  • Decorations (e.g. ribbons, speech bubble pointers, turned pages, etc)
  • UI icons
  • Counters
  • Connectors
  • Quotes (both just textual ones, e.g. for <q>, as well as huge decorative ones for <blockquote>)
  • Overlays
  • Scrims
  • Fancy shadows
  • Separators
  • List markers without the restrictions of ::marker
  • Active tab indicator that animates when the tab changes
  • Partial borders
  • And many other things.

It is very common to need more than two on the same element, as @chriscoyier explains in https://css-tricks.com/use-cases-for-multiple-pseudo-elements/ . We keep introducing specific pseudo-elements to address specific use cases (e.g. ::marker, ::backdrop, view transitions), yet the pile of use cases is only increasing.

But the main problem is that this is a composability failure. They are effectively a shared resource that different sources of styling (components, design systems, themes, etc) need to compete for or coordinate around. Often, it's easier to give up on them altogether (as UAs do, with very few exceptions).

Worse, because these are used for such vastly different effects, the cascade can produce terrible results when two different sources are styling the same pseudo-element. E.g imagine the combination of code using ::before to add a UI icon and code using ::before to add a scrim or a shadow. 🫠

Being able to create multiple <ident>-keyed ::before/::after would not only solve both problems, it would also open up the way for implementing many things that currently need custom elements and/or shadow DOM via plain CSS, dramatically reducing complexity.

Prior art

Multiple ::before/::after have been proposed many, MANY times before, dating back to the mailing list era.

A few proposals I was able to dig up are:

Nearly all proposals revolved around numerical indices. The only one I could find that touched on idents in passing was #6169.
Thus, the common arguments against them have generally been quite specific to that approach:

  1. Cascade fragility & collisions (the z-index problem): Indices don't eliminate the shared resource problem, they multiply it. There is no way for a certain styling source to claim a certain index for itself, since integers are by definition global.
  2. Implementation cost: Each element gets a fixed style-data slot per pseudo today. Allowing ::before(n) for arbitrary n makes that unbounded.

The hope is that this proposal completely eliminates 1 and hopefully mitigates 2.

Strawman

Taking inspiration from @layer, multiple ::before/::after pseudos are declared as parametrized pseudos with <ident>, and are all siblings.

.pros > ::before(marker) {
	content: "✅";
	/* ... */
}

button::before(icon) {
	content: "";
	display: inline-block;
	height: 1cap;
	aspect-ratio: 1;
	background: currentColor;
	mask: var(--icon) no-repeat;
}

.curved shadows {
	position: relative;
	
	&::before(curved-shadows-1) {
		/* ... */
	}
	&::before(curved-shadows-2) {
		/* ... */
	}
}

The same pseudo-element using the same <ident> on the same target references the same pseudo-element and cascades normally.

Ordering

Most use cases are separate enough that order doesn't matter much, so we have some flexibility in the algorithm in order to make it easier to implement. Even for the use cases where order matters, it's not a precise order that's desired, the intent is usually first, last, or around the relative order of related pseudos.

A non-negotiable seems to be immutability: Once determined for a given element, even if the pseudo-elements get removed (by setting their content to none), the order cannot be changed.

Determining the order via a property like pseudo-order (#6169) doesn't seem viable for a number of reasons (being cascade-dependent etc). Even a second argument on the ::before() seems fraught when combined with immutability.

Instead, it seems we want some reasonable deterministic default + a way to override.
What could that reasonable default be? Some examples:

  1. Parse order (not accounting for conditionals etc). Would need to follow mixins.
  2. Order the pseudo-elements are actually created (which would account for conditionals etc).

Open questions TBB:

  • Is this internal state or can it be changed by moving rules around via the CSSOM?
  • Is the order global, per tree, or per element? Ideally, it should be possible to have different orders for different elements, but it's probably not a dealbreaker if we can't.

Authors can customize the order via a similar mechanism as the empty @layer rule, e.g. something like @before-order foo, bar, baz; or @pseudo-order ::before foo, bar, baz; (and if there is no ::before or ::after it's specifying the order for both). default could be reserved, so that it can represent the plain version.

Like layers, any pseudos not in the sequence would be appended at the end. For example, this:

@pseudo-order foo, default, bar;

div::before(foo) {}
div::before(baz) {}
div::before(bar) {}
div::before {}

Would produce an order of foo, (default), bar, baz`.

@tabatkins suggested also borrowing dotted idents from layers, though I can't think of any use cases that aren't just solved by namespacing.

Out of scope

  • Enabling multiples of other pseudo-elements (e.g. ::marker) — this is strictly about ::before and ::after
  • Nested pseudo-elements (e.g. ::before(foo)::before(bar) — useful but not MVP

Would love to hear from implementors on whether this general shape might be feasible, and if not, what would make it feasible. cc @emilio @andruud @nt1m

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions