Repository navigation
[css-pseudo] ::before(<ident>) / ::after(<ident>): A possible path forwards for multiple gencontent pseudos? #13860
Description
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.
- We even have a wiki entry dating from …nobody knows when, discussing
::before[n]and stackable::before(e.g.::before::before). - An old (c. 2003) version of GCPM had a 4.2.2. Inserting multiple '::before' and '::after' pseudo-elements section discussing
::before(n)syntax.
A few proposals I was able to dig up are:
- De Bastiani, 2013-03 — ::before(x) proposal —
::before(n) - Adobe's
::nth-pseudo()proposal by @astearns and @therealglazou which goes beyond just multiple::beforeand::afterbut also acceptedbeforeandafterbut alsoletter,line, andcolumntoo. - Feature to define an arbitrary number of pseudo-elements #6169 Proposes
::trailing(n)but also floats the idea of<ident>! In that, order is determined by apseudo-orderproperty that requires cascading though. - Custom Pseudo Elements via New pseudo-before/pseudo-after Attribute #11670 (declares multiple named pseudos via an HTML attribute)
- WICG Discourse #964 — Multiple ::before and ::after pseudo elements? (2015)
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:
- 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.
- Implementation cost: Each element gets a fixed style-data slot per pseudo today. Allowing
::before(n)for arbitrarynmakes 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?
- Authors use
<dashed-ident>, UAs use<ident>. The way we've gone in other parts of the language. - 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:
- Parse order (not accounting for conditionals etc). Would need to follow mixins.
- 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::beforeand::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:
- 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? - 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? - How do we specify order?
- What participates in this order? Just
::before()/::after()or also things like::checkmark,::markeretc? - What is the default order?
- Do we do anything special for balancing pairs?
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTuesday afternoon
Activity
Loirooriol commentedon Apr 29, 2026
I think that authors may want opposite order for
::before()and::after(). Maybe it can be reversed automatically for one of them, like!importantin layersLeaVerou commentedon May 8, 2026
What are the use cases?
chriscoyier commentedon May 8, 2026
Just wanted to note a thought I had about the placement of the pseudo-elements. It would be neat if they matched the
insertAdjacentHTMLJavaScript API.Right now I think it's basically
afterbeginfor::beforeandbeforeendfor::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.
emilio commentedon May 8, 2026
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::beforerules, but given things like*, *::before, *::after { box-sizing: border-box }are so common, that probably means we want::before(*)or so as well...Loirooriol commentedon May 8, 2026
Nested brackets, nested decorations, etc.
LeaVerou commentedon May 9, 2026
@chriscoyier
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/::afterbeats 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
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-
nonecontentvalue seems like a good optimization anyway.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
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(). 🤔tabatkins commentedon Jul 27, 2026
(tldr note: there's an actual proposal at the end, after my commentary)
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-fooor::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().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.
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 afoo.*prefix together unless you explicitly interweave things.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, unlikeorder; 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 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-orderproperty on the originating element, which takes a list of names. UA-defined pseudos can be listed by name and intermixed, likepseudo-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-2would explicitly place--foo-1and--foo-2, and put any other--foonames 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:
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-styleaffecting the same pseudo cascade together. (Issue: does@layerapply? Probably.) A specific name has higher specificity than a wildcarded name, so--foostyles 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 withcontent.LeaVerou commentedon Jul 29, 2026
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-foocould be anything and needs to be learned separately.::ua-foois 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-styleetc)It seems to me that what is needed is a model where these are instantiated lazily when their
contentis set and ignored otherwise. From an author pov this makes total sense and should work:Authors should not have to jump through hoops and use
@pseudo-stylerules 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.That makes sense. I think the defaults should still be done via a regular rule rather than
@pseudo-style. We can just ignorecontentin 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.
I think that makes sense.
+1
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.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.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.
This seems exceedingly complex and I'm worried it's solving problems that not enough authors have.
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:
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 untilcontentis set on it.Though it would be even better for the implementation to mark relevant rules and
getComputedStyle()to pull-resolve.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.
tabatkins commentedon Aug 4, 2026
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.
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'm fine with this.
fantasai commentedon Aug 4, 2026
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: noneon all the ::before()s on an element and reset it to its original state.css-meeting-bot commentedon Aug 4, 2026
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
LeaVerou commentedon Aug 7, 2026
Agreed, but all we need for that is
::before(*)and::after(*).bradkemper commentedon Aug 7, 2026
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:
And then some other library had this with the same or higher specificity:
Then the
--barwould 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:
This would be similar to how the
orderproperty works, butafter-orderwould only apply to the order of::after,::after()and after-like pseudos, whilebefore-orderwould only apply to the order of::before,::before()and before-like pseudos.bradkemper commentedon Aug 7, 2026
That feels like a custom property. What about if it was something like this:
Then you could do this:
tabatkins commentedon Aug 10, 2026
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.
bradkemper commentedon Aug 11, 2026
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
orderandreading-order, which work well in moderation, and that's what I'm looking for here. Even withorder, 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.Exactly what a
order-like property could achieve.LeaVerou commentedon Aug 12, 2026
Btw the idea I mentioned at the end of the meeting was, if resolving pseudo existence via
contentis 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 likebefore-content-fooor whatever. Thencontenton 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.
tabatkins commentedon Aug 12, 2026
@bradkemper
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.
orderis 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
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: