Skip to content

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

Open

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
  • Selected/checked item indicators
  • Change indicators
  • 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, the UA, etc) need to compete for or coordinate around. Often, it's easier to give up on them altogether.

Worse, because these are used for such vastly different effects, a common source of breakage is 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. 🫠

The platform suffers from this as well: we introduce new pseudo-elements that behave exactly like ::before/::after (e.g. ::checkmark) simply because these are off-limits for the UA because it would undesirably clash with author styles. At first, this seems harmless: there is no real difference between ::checkmark and ::before(ua-checkmark) and the former is shorter/nicer!
Except, from an author's pov, until they have learned a new pseudo-element, they don't know:

  • Where it sits in the tree
  • What properties it accepts
  • What pseudo-classes can come after it
  • etc

In OO concepts, a::before(checkmark) is an instance of a known class, a ::checkmark is a singleton of a new class. Separate pseudos are absolutely appropriate when the answers to these questions are different than for ::before()/::after(), but we should not need to introduce new vocabulary when they aren't.

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.

It would also open up these pseudos for use by components, CSS frameworks, and the UA itself, without the risk of conflicts.

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.

::before() and ::after() with no arguments would be aliases to ::before and ::after.

Once this is deployed widely, un-qualified ::before and ::after will be seen as pseudos that only final consumers should use, and the best practice would be for all tooling and libraries to use qualified ones to prevent clashes (a bit like how layers are going).

UA-provided ::before/::after

An explicit goal is to make ::before/::after machinery available to UAs, to limit the number of new pseudo-elements we need to introduce.

Whereas from an implementation pov there is no difference between a ::foo that behaves like a ::before and a ::before(foo), there is from a human factors perspective: seeing ::before(foo) lets the author leverage their existing mental model about how this pseudo-element works. It's a new word in an existing vocabulary. Whereas ::foo needs to be learned separately, because you can't assume upfront that it behaves like a ::before or that it doesn't have any limitations that ::before doesn't. Also, it means any machinery we introduce for before/after works automatically (e.g. currently you can't really place an author ::before before a ::checkmark in the DOM).

So to facilitate that, UAs need to have access to a separate namespace. How do we do that?

  1. Authors use <dashed-ident>, UAs use <ident>. The way we've gone in other parts of the language.
  2. Reserved prefix for UAs, e.g. ua-.

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.

Balancing

@Loirooriol brought up an interesting case below: often ::before and ::after are used to add decorations that should be balanced (e.g. brackets etc). Should whatever we come up with for ordering allow that?

My instinct is to say that if one of the options we consider for ordering naturally allows for that, that would be a +1, but otherwise this is out of scope for now. Yes, it's useful, but it's not in the 80% MVP. Would be good to hear what others think, though.

Out of scope (for now)

  • 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) to add content to an existing generated content pseudo — 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

Design decisions (added on Aug 4th, 2026)

Summarizing the discussion below, it seems these are the core design decisions:

  1. Can authors define defaults by just doing e.g. ::before(foo) { background: yellow } or should we have separate syntax for that so that engines can optimize?
  2. Should we use <dashed-ident> and reserve <ident> for UA things, or should we use a reserved prefix for UA things (e.g. ua-) or do we not want to use these for UA things, even if they function identically?
  3. How do we specify order?
  4. What participates in this order? Just ::before()/::after() or also things like ::checkmark, ::marker etc?
  5. What is the default order?
  6. Do we do anything special for balancing pairs?

Activity

  1. Loirooriol commented on Apr 29, 2026

    @Loirooriol
    Contributor

    I think that authors may want opposite order for ::before() and ::after(). Maybe it can be reversed automatically for one of them, like !important in layers

  2. LeaVerou commented on May 8, 2026

    @LeaVerou
    MemberAuthor

    I think that authors may want opposite order for ::before() and ::after(). Maybe it can be reversed automatically for one of them, like !important in layers

    What are the use cases?

  3. chriscoyier commented on May 8, 2026

    @chriscoyier

    Just wanted to note a thought I had about the placement of the pseudo-elements. It would be neat if they matched the insertAdjacentHTML JavaScript API.

    • beforebegin
    • afterbegin
    • beforeend
    • afterend

    Right now I think it's basically afterbegin for ::before and beforeend for ::after. But the other two would place the pseudo element outside the element. Maybe that would be useful for things like escaping hidden overflow, positioning based on the parent, not being subject to a grid/flex layout, etc.

    Just a pie-in-the-sky idea, not a huge deal.

  4. emilio commented on May 8, 2026

    @emilio
    Collaborator

    My gut feeling is that this would tank performance linearly with the amount of identifiers you define.

    If you define 3 of these "parametric pseudos" in order, you're basically forcing the engine to resolve 6 more styles per element, for all elements.

    Of course, in most cases you can just throw those styles away since they're not going to be generated, but you still need to look at whether those would match and that gets fairly annoying. Plus you might want to keep them around for invalidation?

    There are optimizations possible, maybe (you match all the "::before", "::before(foo)" etc rules together and then filter during the cascade or something), but still very weird and not free.

    The other potential issue is that we probably wouldn't want ::before(foo) rules to match generic ::before rules, but given things like *, *::before, *::after { box-sizing: border-box } are so common, that probably means we want ::before(*) or so as well...

  5. Loirooriol commented on May 8, 2026

    @Loirooriol
    Contributor

    What are the use cases?

    Nested brackets, nested decorations, etc.

  6. LeaVerou commented on May 9, 2026

    @LeaVerou
    MemberAuthor

    @chriscoyier

    Just wanted to note a thought I had about the placement of the pseudo-elements. It would be neat if they matched the insertAdjacentHTML JavaScript API.

    I think pseudos outside the element would be much harder. Also, internal consistency (within CSS) trumps external consistency (with JS) so being consistent with ::before/::after beats being consistent with a JS API (which wasn't even designed properly in a WG — it was shipped hastily by MSFT and then standardized post-hoc).

    @emilio

    My gut feeling is that this would tank performance linearly with the amount of identifiers you define.

    If you define 3 of these "parametric pseudos" in order, you're basically forcing the engine to resolve 6 more styles per element, for all elements.

    Wait, what? Why not 3? Defining a ::before(foo) doesn't also create an ::after(foo). They are separate (and I suspect in most cases the intersection won't be too big).

    Also, the alternative is not having nothing, it's doing it with HTML elements, but yeah, those are not per element for all elements... But do they really need to be there for all elements? Creating them lazily when they first get a non-none content value seems like a good optimization anyway.

    The other potential issue is that we probably wouldn't want ::before(foo) rules to match generic ::before rules, but given things like *, *::before, *::after { box-sizing: border-box } are so common, that probably means we want ::before(*) or so as well...

    That's an excellent point. Yes, we'd need a separate ::before(*)/::after(*) for these. Though I don't think it's MVP, it can ship later.

    @Loirooriol

    What are the use cases?

    Nested brackets, nested decorations, etc.

    Oooh that's a good point. Not sure how the API should accommodate that though. The actual intent isn't really "reverse the order of all pseudo-elements", it's keeping the pairs balanced for cases where you have the same id in both ::before() and ::after(). 🤔

  7. tabatkins commented on Jul 27, 2026

    @tabatkins
    Member

    (tldr note: there's an actual proposal at the end, after my commentary)

    An explicit goal is to make ::before/::after machinery available to UAs, to limit the number of new pseudo-elements we need to introduce.

    I don't think this should be a goal here. There is no meaningful difference between ::before(ua-foo) and ::ua-foo, except the former is longer to type (and, depending on how we define the idents, might need a reserved prefix). It would also require us to make a decision on all future pseudo-elements about whether to spell them ::ua-foo or ::before(...), and if we go with the latter, that's a break from existing precedent.

    I think the only benefit of this might be that it's slightly more natural to use the order-defining mechanic if they're all ::before()? But we'd want to apply it to the existing pseudos in that case anyway, so we'd need still need to include them anyway, and we can just continue to use that for all the UA-defined ones without needing them to be a ::before().

    But do they really need to be there for all elements? Creating them lazily when they first get a non-none content value seems like a good optimization anyway.

    Emilio's talking about the cost of creating the underlying object in the style system, not the box itself. In order to receive styles and know that you're going to generate a box, you need to already have something set up to do the receiving, and that probably happens on every element. We'd probably want to do something like what Emilio suggested and bucket them all together, with their names attached to the styles in some way, and then handle cascade a little more explicitly, so we'd only have to allocate one extra "thing" per element, just with some extra effort when they do exist.

    That said, we might still want to do some contortions to avoid encouraging authors to set these styles on every element, so we don't actually have to instantiate these objects by default.

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

    The main thing is that it lets us naturally namespace as ::before(foo.*) and provide consistent styles to all of the names in a particular group, plus for the ordering we can naturally group everything under a foo.* prefix together unless you explicitly interweave things.

    Determining the order via a property like pseudo-order (#6169) doesn't seem viable for a number of reasons (being cascade-dependent etc).

    I think the idea as expressed there (an integer-indexed propertly a la order, on each pseudo-element) isn't great: it requires global index management, unlike order; it encourages setting a property on every element's pseudo even if you only use the pseudo for real on a few elements, meaning we have to create style objects on every element; etc.

    But controlling this on a per-element basis, with an inheriting property, seems more reasonable. Each element can negotiate what the necessary ordering is for themselves, when it's relevant, and only needs to care about conflicts when it's actually generating multiple such pseudos. An unspecified pseudo can just use some trivial default ordering, since they mostly don't matter; ASCII-betical would work to avoid having to track and remember an appearance ordering across the stylesheet when this isn't a stylesheet-level decision.

    ::before() and ::after() with no arguments would be aliases to ::before and ::after.

    I don't think this is needed. We already have a way to spell plain ::before/after; we don't need a second way to do so.


    I think figuring out the ordering, with some semi-realistic examples of how it would work "correctly" in a mixed components system, and combined with UA-defined pseudos, is the most important thing here. Deciding on how "reversing" works between before/after is also fairly important. If we don't get these right in the beginning, I think we'll have a very hard time fixing them in the future.

    So here's my proposal:

    UAs keep minting new pseudo names as normal. Authors get access to a ::before(...)/::after(...) set, using dashed-ident names.

    We define a specific "default" order for all of the tree-abiding before/after pseudos (including ones like ::checkbox, etc); the author-defined ones all show up in one spot in ASCII-betical order by name. The "after" list is always in reverse order of the "before" list.

    Authors can override the order of these lists with a pseudo-order property on the originating element, which takes a list of names. UA-defined pseudos can be listed by name and intermixed, like pseudo-order: before, --foo, checkbox;. (Issue: decide whether the "after" list is still always the reverse of the "before" list, or if it can be overridden separately.) By default, all unspecified pseudos are put after the named ones, but you can control that by providing a * entry instead to indicate "all the rest". Dashed-ident names in this list can be followed by a * (like --foo*) to indicate a prefix match for multiple names at once, again only matching names not explicitly specified elsewhere. (So --foo-1, --foo*, --foo-2 would explicitly place --foo-1 and --foo-2, and put any other --foo names between them.)

    To avoid authors/libraries setting shared default styles for their pseudos on every element in the page and requiring us to instantiate things up on all elements, we create a new at-rule that provides the initial styles for author-defined pseudos, cascaded at UA-stylesheet level so they're trivial to override on individual elements:

    @pseudo-style ::before(--foo), ::after(--bar) {
    	color: red;
    }
    

    You can similarly use --foo* names in the list to style anything with that prefix, or just explicitly list them comma-separated. The 'content' property is considered invalid and ignored in this rule; if authors want to make a pseudo exist on all elements they have to say so on purpose. Multiple instances of @pseudo-style affecting the same pseudo cascade together. (Issue: does @layer apply? Probably.) A specific name has higher specificity than a wildcarded name, so --foo styles beat --foo* styles; wildcarded names use the length of their prefix as their specificity, so --foo-bar* beats --foo*.

    (In other words, these are *::before(--foo) {...} style rules, but we don't have to actually process them until/unless we see an individual element receive styles for them explicitly.)

    In actual style rules, only explicit names can be used (::before(--foo), but not ::before(--foo*)), because we need to know what pseudos are actaully being referred to when you tell them to exist with content.

  8. LeaVerou commented on Jul 29, 2026

    @LeaVerou
    MemberAuthor

    I don't think this should be a goal here. There is no meaningful difference between ::before(ua-foo) and ::ua-foo, except the former is longer to type (and, depending on how we define the idents, might need a reserved prefix). It would also require us to make a decision on all future pseudo-elements about whether to spell them ::ua-foo or ::before(...), and if we go with the latter, that's a break from existing precedent.

    There is absolutely a meaningful difference when it comes to human factors. ::before(ua-foo) tells you what it does, how it works, where it is in the DOM, what pseudo-classes are supported, what properties are accepted. ::ua-foo could be anything and needs to be learned separately. ::ua-foo is entirely new vocabulary, for ::before(ua-foo) all you need to know is its order and when it is generated. Obviously, ad hoc pseudo-elements would still be appropriate for cases where there are additional restrictions.

    Not to mention that any machinery we introduce for ::before() will automatically apply (e.g. pseudo-order, @pseudo-style etc)

    Emilio's talking about the cost of creating the underlying object in the style system, not the box itself. In order to receive styles and know that you're going to generate a box, you need to already have something set up to do the receiving, and that probably happens on every element. We'd probably want to do something like what Emilio suggested and bucket them all together, with their names attached to the styles in some way, and then handle cascade a little more explicitly, so we'd only have to allocate one extra "thing" per element, just with some extra effort when they do exist.

    That said, we might still want to do some contortions to avoid encouraging authors to set these styles on every element, so we don't actually have to instantiate these objects by default.

    It seems to me that what is needed is a model where these are instantiated lazily when their content is set and ignored otherwise. From an author pov this makes total sense and should work:

    ::after(ribbon) {
    	/* backgrounds, sizing, fonts, etc, no content property */
    }
    
    /* ... */
    
    article.featured {
    	&::after(ribbon) { content: "Featured" }
    }

    Authors should not have to jump through hoops and use @pseudo-style rules for what the language semantics can already express perfectly well.

    Given that nonexistent pseudo-elements are not really observable, UAs can do any number of internal optimizations here. What Emilio suggested, OR transforming .foo::before(n) { content: "" } as a hidden .foo { -before-content-n: "" } and resolve it lazily only when the property matches or whatever.

    FWIW not allocating gencontent pseudos lazily per element seems wasteful even without this, so optimizing it seems like a win overall, even for regular ::before/::after.

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

    The main thing is that it lets us naturally namespace as ::before(foo.*) and provide consistent styles to all of the names in a particular group, plus for the ordering we can naturally group everything under a foo.* prefix together unless you explicitly interweave things.

    That makes sense. I think the defaults should still be done via a regular rule rather than @pseudo-style. We can just ignore content in such a rule.

    But also, this seems non-MVP. As long as dotted idents are not allowed in the MVP, the feature can ship without this.

    Determining the order via a property like pseudo-order (#6169) doesn't seem viable for a number of reasons (being cascade-dependent etc).

    I think the idea as expressed there (an integer-indexed propertly a la order, on each pseudo-element) isn't great: it requires global index management, unlike order; it encourages setting a property on every element's pseudo even if you only use the pseudo for real on a few elements, meaning we have to create style objects on every element; etc.

    But controlling this on a per-element basis, with an inheriting property, seems more reasonable. Each element can negotiate what the necessary ordering is for themselves, when it's relevant, and only needs to care about conflicts when it's actually generating multiple such pseudos. An unspecified pseudo can just use some trivial default ordering, since they mostly don't matter; ASCII-betical would work to avoid having to track and remember an appearance ordering across the stylesheet when this isn't a stylesheet-level decision.

    I think that makes sense.

    ::before() and ::after() with no arguments would be aliases to ::before and ::after.

    I don't think this is needed. We already have a way to spell plain ::before/after; we don't need a second way to do so.

    +1

    I think figuring out the ordering, with some semi-realistic examples of how it would work "correctly" in a mixed components system, and combined with UA-defined pseudos, is the most important thing here. Deciding on how "reversing" works between before/after is also fairly important. If we don't get these right in the beginning, I think we'll have a very hard time fixing them in the future.

    I agree we should try to get this right but also I think you may be overestimating its importance. It's relatively rare for inline use cases to co-exist. Most use cases are about positioned elements, where order doesn't matter as much, and for flex/grid there's always order.

    So here's my proposal:

    UAs keep minting new pseudo names as normal. Authors get access to a ::before(...)/::after(...) set, using dashed-ident names.

    Wait, why both? Either UAs use <ident>s or they're fair game for authors. Unless you think we may want to reserve idents for UAs later.

    We define a specific "default" order for all of the tree-abiding before/after pseudos (including ones like ::checkbox, etc); the author-defined ones all show up in one spot in ASCII-betical order by name. The "after" list is always in reverse order of the "before" list.

    Ideally, we want to come up with a good enough heuristic that authors don't have to override it all the time.
    ASCIIbetical is unlikely to ever match author intent, except by coincidence.

    Authors can override the order of these lists with a pseudo-order property on the originating element, which takes a list of names. UA-defined pseudos can be listed by name and intermixed, like pseudo-order: before, --foo, checkbox;. (Issue: decide whether the "after" list is still always the reverse of the "before" list, or if it can be overridden separately.) By default, all unspecified pseudos are put after the named ones, but you can control that by providing a * entry instead to indicate "all the rest". Dashed-ident names in this list can be followed by a * (like --foo*) to indicate a prefix match for multiple names at once, again only matching names not explicitly specified elsewhere. (So --foo-1, --foo*, --foo-2 would explicitly place --foo-1 and --foo-2, and put any other --foo names between them.)

    This seems exceedingly complex and I'm worried it's solving problems that not enough authors have.

    To avoid authors/libraries setting shared default styles for their pseudos on every element in the page and requiring us to instantiate things up on all elements, we create a new at-rule that provides the initial styles for author-defined pseudos, cascaded at UA-stylesheet level so they're trivial to override on individual elements:

    @pseudo-style ::before(--foo), ::after(--bar) {
    	color: red;
    }
    

    See arguments above about this. This violates PoC and introduces new syntax for the sole purpose of implementor convenience. There doesn't seem any author-facing benefit to this over just:

    ::before(--foo), ::after(--bar) {
     	color: red;
    }

    If the problem is that the latter needs to go through the cascade in case getComputedStyle() observes it, I'd be fine with just defining that a ::before(<ident>) effectively does not exist until content is set on it.
    Though it would be even better for the implementation to mark relevant rules and getComputedStyle() to pull-resolve.

    You can similarly use --foo* names in the list to style anything with that prefix, or just explicitly list them comma-separated. The 'content' property is considered invalid and ignored in this rule; if authors want to make a pseudo exist on all elements they have to say so on purpose. Multiple instances of @pseudo-style affecting the same pseudo cascade together. (Issue: does @layer apply? Probably.) A specific name has higher specificity than a wildcarded name, so --foo styles beat --foo* styles; wildcarded names use the length of their prefix as their specificity, so --foo-bar* beats --foo*.

    (In other words, these are *::before(--foo) {...} style rules, but we don't have to actually process them until/unless we see an individual element receive styles for them explicitly.)

    In actual style rules, only explicit names can be used (::before(--foo), but not ::before(--foo*)), because we need to know what pseudos are actaully being referred to when you tell them to exist with content.

    I think the entire wildcarding part should be post-MVP. I'm not convinced there are enough use cases to warrant its complexity, and it can be added after with no compat risk.

  9. moved this from Agenda+ FTF to Tuesday afternoon in CSSWG summer 2026 meeting agendaon Aug 1, 2026
  10. tabatkins commented on Aug 4, 2026

    @tabatkins
    Member

    ::before(ua-foo) tells you what it does, how it works, where it is in the DOM, what pseudo-classes are supported, what properties are accepted.

    It just tells you that it's tree-abiding and is placed somewhere before the element's content. You still need to know what it is and what it does and what all its other styling/layout concerns are. I don't think knowing it's "like ::before" is sufficiently useful to justify spelling them differently from how we usually spell CSS-defined things.

    It seems to me that what is needed is a model where these are instantiated lazily when their content is set and ignored otherwise.

    What I and Emilio are talking about are the internal style-holding data structures that allow you to have 'content' set in the first place. You're thinking of creating boxes, which happens well after the point we're concerned about.

    I think the entire wildcarding part should be post-MVP.

    I'm fine with this.

  11. fantasai commented on Aug 4, 2026

    @fantasai
    Collaborator

    I think the entire wildcarding part should be post-MVP.

    I disagree. Authors need to be able to easily delete all of the pseudo-elements from an element, and they can't because the don't know all their names. We might not do sophisticated wildcarding, but it should be possible to set display: none on all the ::before()s on an element and reset it to its original state.

  12. css-meeting-bot commented on Aug 4, 2026

    @css-meeting-bot
    Member

    The CSS Working Group just discussed [css-pseudo] `::before(<ident>)` / `::after(<ident>)`: A possible path forwards for multiple gencontent pseudos?.

    The full IRC log of that discussion <ydaniv> lea: we have ::before and ::after we are the only ways of entering content
    <ydaniv> ... used for a lot of use-cases
    <ydaniv> ... ui icons, conectors, quotes, etc.....
    <ydaniv> ... sometimes overlays, list markers without restricitons, tab indicators...
    <ydaniv> ... partial borders, indicators for selected items, etc.
    <ydaniv> ... unfortunate when they collide
    <ydaniv> ... but also because of all of these use-cases, multiple parties competting on same space
    <ydaniv> ... and then we create a new specialized custom element for that use-case, etc.
    <ydaniv> ... most proposals were index-based, which was also problematic
    <ydaniv> ... also 3rd parties can't use these safely
    <ydaniv> ... from impl. POV might not be a real difference, but from authrs' it's a new concept
    <ydaniv> ... all these things are well defined, to have UA tap into these
    <TabAtkins> q+
    <ydaniv> ... discussed it with TabAtkins, this is not about adding nesting which is out of scope, only about before/after
    <ydaniv> ... this is about adding idents so that we can define them separately
    <lea> scribe+
    <lea> lea: Can authors define defaults by just doing e.g. ::before(foo) { background: yellow } or should we have separate syntax for that so that engines can optimize?
    <ydaniv> lea: there is no box until you set content, creates certain difficulties
    <ydaniv> ... my opinion there's no benefit for the syntax, should be [missed]
    <lea> lea: Should we use `<dashed-ident>` and reserve `<ident>` for UA things, or should we use a reserved prefix for UA things (e.g. ua-) or do we not want to use these for UA things, even if they function identically?
    <lea> lea: 3. How do we specify order?
    <ydaniv> lea: how do we specify order?
    <ydaniv> ... is it done differently for before/after?
    <keithamus> q/
    <keithamus> q+
    <lea> lea: 4. Do we do anything special for balancing pairs?
    <SebastianZ> q+
    <ydaniv> q+
    <lea> lea: 5. What participates in this order? Just ::before()/::after() or also things like ::checkmark, ::marker etc?
    <lea> lea: 6. What is the default order?
    <ydaniv> lea: if we do add that pseudo order thing...
    <fantasai> I agree with TabAtkins that built-in stuff should continue to be built in, and reserve this for author stuff. https://github.com//issues/13860#issuecomment-5096571287
    <ydaniv> ... we can do alphbetic, or something that makes sense
    <romain> alphabetic doesn't make sense, but it is stable
    <lea> scribe-
    <astearns> q+
    <ydaniv> TabAtkins: the concern I had is about performance, well before we create boxes, we need to know content has been set on these
    <lea> q+
    <ydaniv> ... the styling has to be constructed ahead of time, we can't make those optimistically
    <ydaniv> ... my concern that if you need to setup defaults, you would do that by setting these styles
    <ydaniv> ... [missec]
    <kbabbitt> +1 to this perf concern
    <ydaniv> ... have a proposal for the defaults use-case that you declare them as at-rule, so that you can style pseudos when they are created
    <florian> q+
    <emilio> q+
    <astearns> ack TabAtkins
    <ydaniv> ... rather than forcing us to create them everywhere
    <ydaniv> ... other big thing is ordering, we need unordered for the order defined psuedos
    <ydaniv> ... when they don't specify order clearly, we need some order for those, and I think for those we don't care
    <ydaniv> ... I suspect we should use an arbitrary order for those, and order what was specified
    <ydaniv> ... a lot of the time we use them as abs-pos
    <ydaniv> ... so mostly doesn't matter
    <romain> +1
    <ydaniv> ... you list befores/afters, don't have an opinion on how we specify the outset
    <ydaniv> ... also fantasai added to IRC, we should move the before pseudos, don't think we should do that
    <ydaniv> ... and then have a break on how we do future things, when we do these things we use arbitrary things for them
    <ydaniv> ... we should just contiue to have language defined stuff, and leave the customized before for author stuff
    <florian> q?
    <ntim> worth noting ::checkmark order is relative to ::before/::after is dependent on what is the host element
    <astearns> ack keithamus
    <fantasai> s/move the before pseudos/use before pseudos for UA-defined pseudos/
    <ydaniv> keithamus: don't love the syntax, but don't see why a developer need to make a distinction of before/after
    <lea> qq+ to reply to this
    <ydaniv> ... unless I supply and orderring which can specify it
    <ydaniv> lea: the order for the relative ordering
    <astearns> ack lea
    <Zakim> lea, you wanted to react to keithamus to reply to this
    <lea> astearns: that also took me off the queue for my main comment
    <ydaniv> keithamus: you just describe it in one rule, and then use them as pseudos, not clear why you need to specify them as before/after
    <ydaniv> TabAtkins: you need to namespace it, so can't be just ::foo
    <ydaniv> florian: could be ::--
    <ydaniv> lea: but that we may want to do for other stuff
    <ydaniv> fantasai: like expose parts
    <ydaniv> TabAtkins: they can live together anyway
    <astearns> ack SebastianZ
    <ydaniv> SebastianZ: wanted to address the 2nd item, I think we're making a mistake by allowing ident to be in the syntax, instead of using -- for custom idents
    <TabAtkins> (my proposal is to use dashed-ident, yes)
    <ydaniv> ... stongly believe we should use dash-dents in this case
    <fantasai> scribe+
    <astearns> ack ydaniv
    <fantasai> ydaniv: Big + regarding third-party use case. It's really impossible to even use ::before/::after because everyone is tripping on top of each other
    <fantasai> ydaniv: Many times you have custom graphical things you want to add
    <fantasai> ydaniv: Some of them we make a different feature, like overlays, but this issue keeps popping up. So we should solve it.
    <astearns> ack astearns
    <ydaniv> astearns: actually agree with use-cases and desire of authors, had previous attempts at this, but we say authors have to use custom elements, maybe that's the solution?
    <ydaniv> ... this gives you all the tools trying to solve it
    <astearns> ack lea
    <ydaniv> TabAtkins: but then you can add crappy markup to your page to solve that
    <ydaniv> lea: so replying to astearns, using custom elements is like using a sledge hammer for (something small)
    <ydaniv> ... comes with a lot of usability issues
    <ydaniv> ... custom elements are fine solution when you need more, but not instead
    <ydaniv> ... you can not throw a stylesheet on a page and now get advantage of this
    <ydaniv> ... there's a TAG design principle that <missed> should not drive syntax
    <astearns> s/but we say authors/but you say authors/
    <ydaniv> ... alomost everytime we resolve on not adding syntax for simple case
    <ydaniv> ... but maybe just adding a custom pseudo is the answer
    <TabAtkins> converting `.foo, ::before(--foo) {...}` into a style rule + pseudo rule is really a no-no
    <ydaniv> ... how do we explain to authors that CSS can't express someting it can already do
    <astearns> s/<missed>/implementation complexity/
    <ydaniv> ... the bit that is new here is that it comes from CSS
    <ydaniv> emilio: the big issue is for a given element, which of these pseudos you need to match? current proposal is all, which doesn't scale
    <ydaniv> ... we need a way to constrain these
    <kbabbitt> +1 emilio
    <ydaniv> ... what is the concrete set of elements we need to match?
    <ydaniv> ... all custom rules at the same time?
    <ydaniv> ... becomes way more expensive
    <ydaniv> ... for example, compared to custom highlight
    <ydaniv> ... where you only match the highlights that are relevant to you
    <florian> q?
    <keithamus> q+
    <ydaniv> ... what's missing here is some way to be able to scale sensibly with amount of elements you need to style
    <sgill> q+
    <ydaniv> ... not as trivial as it sounds
    <ydaniv> emilio: @pseudo-defaults reduces the amount of work...
    <ydaniv> TabAtkins: how does that let us do this?
    <ydaniv> emilio: styling of the element but not the pseudo
    <ydaniv> ... you're looking at element level
    <keithamus> q-
    <astearns> ack aaaaafter
    <ydaniv> ... gives you a concrete set of pseudos you have to look at
    <ydaniv> lea: for the order don't have a strong opinion, alphabetic is ok
    <ydaniv> ... we may want to sort inserting elements in the middle
    <ydaniv> ... if you have 2 pairs is very rare
    <keithamus> my 2 cents: foo { pseudos: --bar marker --baz before element after --baz --bar } foo::--bar { ... }
    <astearns> (doubts 2 pairs will be rare if we allow frameworks to use this)
    <TabAtkins> Doing just *new* CSS-defined Pseudos with ::before() is *much worse* than giving the existing ones two spellings
    <fantasai> +1
    <ydaniv> ... I disagree that as principle not allowing certain mechanisms on the platform
    <fantasai> idk about much worse, but it's weird to have a date-based cutoff
    <fantasai> and not a good idea imho
    <ydaniv> ... very wierd to have order for certain things here, like why ::part is not part of this
    <ydaniv> TabAtkins: the reason why part doesn't take part is [missed]
    <ydaniv> TabAtkins: we define it as tree abiding pseudo
    <SebastianZ> +1 to keithamus' proposal.
    <TabAtkins> s/[missed]/it's pointing to some element inside the shadow tree/
    <astearns> ack florian
    <ydaniv> florian: on the ordering thing I'm with TabAtkins, with alphabetic makes sense to me,
    <ydaniv> ... lets tree abinding pseudos be those wierd thing
    <lea> scribe-
    <ydaniv> ... proposed syntax is ::-- something and all pseudos in before/after will share that space, so prefer ::before(--ident)
    <lea> keithamus: centralizing this is EXACTLY the problem we are trying to avoid
    <ydaniv> florian: as for earlier discussion not sure I completely understand, having some kind of thing that says on what pseudos can live on
    <ydaniv> ... or if the element exist, will save a lot of work later
    <keithamus> lea: `{ pseudos: var(--other-pseudos) --my-pseudo ... }` no?
    <ydaniv> ... the thing we currently have is the content property, but why do we have to?
    <lea> lea: Parties now have to coordinate on a `--other-pseudos` property
    <lea> Which means this is still off limits for third parties
    <lea> (e.g. CSS frameworks, libraries, etc)
    <ydaniv> TabAtkins: a slight different thing, the content property doesn't exist on the elment, only on the pseudo
    <lea> q?
    <kbabbitt> nodding strongly to what TabAtkins is saying
    <ydaniv> ... we reserve the style for that pseudo
    <ydaniv> TabAtkins: the at-rule gives you style to resolve later
    <ydaniv> ... having some info on the element let's us resovle on the element and go look for those named pseudo styles
    <lea> I wonder if we could have a family of properties like `before-foo` on the main element **instead** of `content`. That seems quite ergonomic and solves these issues. We could even have a `before`/`after` property for current `::before`/`::after`.
    <kbabbitt> The issue is that you need to selector-match the entire rule ending in a pseudo to see if it matches. If we know which pseudos can apply, we can throw those rules away as soon as we see the pseudo
    <ydaniv> ... anything that relies on the pseudo styles requires us to watch all of pseudos
    <lea> q+ to discuss this
    <astearns> ack emilio
    <astearns> zakim, close queue
    <Zakim> ok, astearns, the speaker queue is closed
    <ydaniv> astearns: going to close q and not finish today
    <ntim> q-
    <astearns> ack sgill
    <ydaniv> sgill: my question is, what is the benefit we get on defining these which can apply to any element?
    <ydaniv> ... when you come up with these you have a specific content which is special for them, so why not put that on these specific elements?
    <ydaniv> TabAtkins: so add it to HTML?
    <lea> sgill: are you asking what the use cases for `::before` and `::after` are? Because these are exactly the same
    <ydaniv> sgill: no, so specify it on a property that gives you info on where to apply these during styling
    <astearns> lea's idea of a *property* that defines what pseudos are relevant for an element is intriguing
    <ydaniv> TabAtkins: we probably want something like that
    <ydaniv> sgill: I'm imagining reusablilty?
    <ydaniv> TabAtkins: you don't care about order, or what element it exists on, the info is already there, where just concerned with efficency
    <ydaniv> fantasai: the cascade replaces the declaration,
    <ydaniv> ... if they coordinate a single property...
    <ydaniv> emilio: you care about the order
    <ydaniv> fantasai: then you have to coordinate that
    <astearns> ack fantasai
    <Zakim> fantasai, you wanted to talk about style data storage
    <ydaniv> fantasai: I agree with TabAtkins tha UA defined pseudos should not be changed to ::--
    <ydaniv> ... aggree with lea that we don't need to use the names, and not require the dashed idents
    <ydaniv> ... the alphabetic order makes sense
    <ydaniv> ... defaulting to that makes sense, so then we don't need this coordination
    <ydaniv> ... ordering by default makes sense, should be before/after
    <ydaniv> ... you have to decide where to define, if begining or end
    <ydaniv> ... a little worried on adding UA pseudos into these
    <astearns> agreed that we should not allow interleaving of UA and custom pseudos
    <TabAtkins> I'm not sure why that affects author's ability to reorder them...
    <TabAtkins> We're only specifying a particular order based on certain assumptions about ::before usage
    <ydaniv> ... in terms of style sharing stuff, I wonder if would be possible while cascading, we do generic before/after, store that aside
    <lea> This is the bucketing idea that emilio had
    <ydaniv> ... and then decide if it needs extras
    <ydaniv> ... probably don't to defcide on all the pseudos
    <ydaniv> emilio: having to match all the rules, is something we want to avoid
    <TabAtkins> point of order - is this air unit thing spraying warm air directly at me? i'm dying
    <ydaniv> ... the least amount of work you need to do is match all of them in cascading order, and then apply cascade skipping, becomes messy really fast
    <fantasai> s/changed to ::--/changed to use this mechanism/
    <fantasai> s/ordering by default makes/flipping before-after ordering by default makes/
    <fantasai> s/should be before/would need to split into longhands pseudo-order-before and pseudo-order-after, after defaulting to flip of before/
    <fantasai> s/adding UA pseudos into these/adding UA pseudos into these, not sure we want to allow them to be reordered by the author/
    <astearns> ack lea
    <Zakim> lea, you wanted to discuss this
    <ydaniv> lea: wanted to say, if it's such a big problem to resolve based on content, one of the issues with having it on the main element
    <ydaniv> ... [missed]
    <lea> scribe+
    <lea> lea: Property might be ok the problem is the double opt-in which creates error conditions. But what if the property WAS the opt-in?
    <lea> before-content-foo: "bar"; → creates a ::before(foo) with content: "bar"
    <lea> lea: this is actually MORE ergonomic for authors, who often use custom properties for this pattern
    <JohnJansen> s/before-content/lea:before-content
    <lea> lea: We absolutely need DIFFERENT properties. Composability is a the whole point, a single `pseudos` property creates the same centralization/coupling and coordination problem
    <ydaniv> ... I think having separate properties for each could work
    <ydaniv> ... might be a path forwards
    <ydaniv> ... if using the regular syntax doesn't work
    <lea> scribe-
    <ydaniv> astearns: enough on this issue for today
    <astearns> zakim, open queue
    <Zakim> ok, astearns, the speaker queue is open
  13. LeaVerou commented on Aug 7, 2026

    @LeaVerou
    MemberAuthor

    I think the entire wildcarding part should be post-MVP.

    I disagree. Authors need to be able to easily delete all of the pseudo-elements from an element, and they can't because the don't know all their names. We might not do sophisticated wildcarding, but it should be possible to set display: none on all the ::before()s on an element and reset it to its original state.

    Agreed, but all we need for that is ::before(*) and ::after(*).

  14. bradkemper commented on Aug 7, 2026

    @bradkemper
    Contributor

    Authors can override the order of these lists with a pseudo-order property on the originating element, which takes a list of names. UA-defined pseudos can be listed by name and intermixed, like pseudo-order: before, --foo, checkbox;

    I don't think that part would be good. It would be too easy for the cascade to wipe it out. So, if you had this in some library:

    .icon {
        pseudo-order: before, --foo, checkbox;
    }
    

    And then some other library had this with the same or higher specificity:

    .icon {
        pseudo-order: before, --bar, marker;
    }
    

    Then the --bar would totally mess up the order for --foo.

    What would make more sense to me would be to have a property within the pseudo determining it's order among the other pseudos:

    .icon::after(--foo) {
       after-order: 2;
    }
    

    This would be similar to how the order property works, but after-order would only apply to the order of ::after, ::after() and after-like pseudos, while before-order would only apply to the order of ::before, ::before() and before-like pseudos.

  15. bradkemper commented on Aug 7, 2026

    @bradkemper
    Contributor

    lea: Property might be ok the problem is the double opt-in which creates error conditions. But what if the property WAS the opt-in?
    lea: before-content-foo: "bar"; → creates a ::before(foo) with content: "bar"

    That feels like a custom property. What about if it was something like this:

    @property --foo-content {
      syntax: "<before>";
      initial-value: none;
      inherits: false;
    }
    

    Then you could do this:

    .bar-after {
        --foo-content: "bar";
    
        &::after(--foo-content) {
            color: red;
        }
    }
    
  16. tabatkins commented on Aug 10, 2026

    @tabatkins
    Member

    I don't think that part would be good. It would be too easy for the cascade to wipe it out. So, if you had this in some library:

    Right, but you're describing a meaningful conflict between two non-coordinating libraries, which should be resolved by the author. If you're only using one of them on a given element, the library's default ordering works; if you're using multiple, you should decide what the ordering is.

    Numbering the pseudos doesn't resolve the problem, as there's still an implicit ordering being defined by the page which the libraries, if they want to apply their own order, are trying to guess at. It's just as fragile, but in a different way. This was discussed pretty extensively in the prior arts that Lea links in the OP, and generally rejected there.

  17. bradkemper commented on Aug 11, 2026

    @bradkemper
    Contributor

    Numbering the pseudos doesn't resolve the problem, as there's still an implicit ordering being defined by the page which the libraries, if they want to apply their own order, are trying to guess at. It's just as fragile, but in a different way. This was discussed pretty extensively in the prior arts that Lea links in the OP, and generally rejected there

    I disagree. I'm not talking about indexing each pseudo, I'm talking about a convenient way for conflicts within each group to be resolved by the author. We have precedent with order and reading-order, which work well in moderation, and that's what I'm looking for here. Even with order, the way I usually use it is as a way to make tweaks to push something to the front or end, and occasionally have a third thing (or group of things) in a spot in relation to the others. If one thing (or several things) goes to the front with -1 and another to the end with 1, that's useful, understandable, and not fragile. It isn't about numbering each item. If the library is trying to also use ordering (which most of the time shouldn't IMO), I can see what that is and adjust my numbers accordingly. I don't see the problem, and it's better than trying to set the order of everything in one exhaustive property.

    it's not a precise order that's desired, the intent is usually first, last, or around the relative order of related pseudos.

    Exactly what a order-like property could achieve.

  18. LeaVerou commented on Aug 12, 2026

    @LeaVerou
    MemberAuthor

    Btw the idea I mentioned at the end of the meeting was, if resolving pseudo existence via content is too expensive, instead of having a double-opt in via a property (which is error-prone), we make the property the opt-in. E.g. via a family of properties like before-content-foo or whatever. Then content on the actual pseudos is either ignored, or treated as an override only when they already exist through the property.

    I don't love it, but I don't hate it either, and I think it's better than the double opt-in.

  19. tabatkins commented on Aug 12, 2026

    @tabatkins
    Member

    @bradkemper

    I'm not talking about indexing each pseudo, I'm talking about a convenient way for conflicts within each group to be resolved by the author.

    Yes, I'm also talking about this. My point is that allowing individual pseudos to number themselves doesn't resolve that problem; pseudos can still conflict with each other and get an arbitrary ordering, or just use arbitrary numbering scales that make using them together be effectively randomly ordered, requiring the author to renumber them to a desired order. Having the author specify the order immediately does solve both of these problems; you know exactly what order the pseudos are, and putting them in any desired order is trivial.

    The cost of an order property on the element is you need to specify it even when the order doesn't matter (because they're absposed or something, and layering isn't meaningful), while letting the pseudos self-number means you can just not care and let them do whatever, which is convenient.

    But leaning on the ordering property to also define what pseudos will exist on the element means we solve two problems at once, which is also convenient.

    We have precedent with order and reading-order, which work well in moderation, and that's what I'm looking for here.

    order is rarely, if ever, used simultaneously by multiple independent libraries in a single container, all fighting over the order of their bits of the children they control; it's virtually always wielded solely by the author to achieve a singular purpose, or if they're doing multiple things, they can resolve the conflict personally.

    The more relevant comparison is z-index, which does often have multiple independent libraries fighting over the value space, and we've seen clearly that "everyone guesses at what the proper numeric ranges should be, and hopes they don't collide" is a pretty bad solution there.

    The other big place we've run into the same problem is @layer, and there we solved it with "the order is arbitrary, unless the author declares their desired global order". (We also used namespacing to allow libraries to set a local order among the layers they control, while still letting the author move the entire set. Maybe that's useful here? Would require some more thinking on how the value space looks. So long as we keep the pseudo names as a ::before()/::after() arg, we could use the same . namespacing as layers, which could be cool. Actually, dang, I really like that.)

    @LeaVerou

    double-opt in via a property (which is error-prone), we make the property the opt-in. E.g. via a family of properties like before-content-foo or whatever.

    I don't think it's worth changing the fundamental model for what makes a pseudo-element exist or not (the 'content' property). If your pseudo is already using meaningful content, whatever the element specifies isn't relevant or needed; if your pseudo doesn't have meaningful content, it's already well-known that you use content: ""; to make it exist (and can avoid setting it if you're going to make it conditional), and it's very easy to do so, not error-prone at all.

    This approach would also leave the ordering issue to be solved in a different way, which seems like a lost opportunity for convenience. Even if you don't care about the order, specifying multiple 'content' properties is significantly longer than specifying the names in an arbitrary order. Compare:

    el {
      before-content-foo: "";
      before-content-bar: "";
      before-content-baz: "";
    }
    
    /* vs */
    
    el {
      pseudo-order: foo, bar, baz;
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions