A badge, an underline, a custom bullet, the colour of selected text — nine pseudo-elements as nested props, and content that quotes itself.
Decoration without another element
A ::before is a box the browser gives you for free, and the reason to want one is that it costs no markup: a badge, a focus ring that overshoots, a gradient underline that grows on hover. Nine of them are nesting keys now — before, after, placeholder, selection, marker, firstLine, firstLetter, backdrop and fileButton — and every prop, state, breakpoint and theme works inside them.
The nine keys
JSX
<Boxbefore={{width:2,bgColor:'indigo-500'}}// ::before — content comes with itafter={{content:'attr(data-suffix)'}}// ::afterplaceholder={{color:'slate-400'}}// ::placeholderselection={{bgColor:'indigo-200'}}// ::selectionmarker={{color:'indigo-500'}}// ::markerfirstLine={{fontWeight:600}}// ::first-linefirstLetter={{fontSize:48}}// ::first-letterbackdrop={{bgColor:'slate-900'}}// ::backdropfileButton={{bgColor:'indigo-500'}}// ::file-selector-button/>
content, which you no longer have to remember
A generated element with no content renders nothing at all — the single commonest way a ::before is silently missing. So declaring before or after supplies content: '' unless you say otherwise, and the pseudo-element exists in exactly the states you styled it in: put it under hover and it appears on hover.
What content takes
JSX
<Boxbefore={{content:'empty'}}/>// content: '' (the default, spelled out)<Boxbefore={{content:'New'}}/>// content: "New" — text is quoted for you<Boxafter={{content:'"Step " counter(step)'}}/>// written as CSS, so a sequence works<Boxafter={{content:'attr(data-suffix)'}}/>// attr(), counter(), url(), var()<Boxbefore={{content:'none'}}/>// and off again
A badge is one prop
The tag below is a single <Button>: the count rides in a data-* attribute and after draws it, so there is no second element to position, no wrapper, and nothing to keep in sync.
before nested inside hover is a pseudo-element that appears in that state — including the content it needs. Nested the other way round, before: { hover: … }, it is the same one compound selector: .x:hover::before either way, and one class.
before + hover
Hover this label
JSX
<Boxtag="span"position="relative"before={{position:'absolute',bottom:-1,left:0,height:0.5,width:0,bgImage:'gradient-primary',transitionDuration:250,}}hover={{before:{width:'fit'}}}>
Hover this label
</Box>
And it follows a state your own code sets
Everything that nests around a prop nests around a pseudo-element: a breakpoint, a theme, a group, and the dataAttr/ariaAttr/has/not variants. The element is appended to whatever they build, because CSS allows exactly one and it has to come last.
The other six style something the browser already renders, so they take the properties CSS lets them and ignore the rest — a ::selection is colours, not layout. On a <Textbox> the name placeholder means both things: a string is the attribute, an object is the styles. Want both, the text goes in props, where every attribute goes.
placeholder, selection, marker
Select this sentence to see ::selection restyled.
One
Two
JSX
<Textboxprops={{placeholder:'Search projects…'}}placeholder={{color:'indigo-400',fontStyle:'italic'}}/><Pselection={{bgColor:'indigo-500',color:'white'}}>Select this sentence.</P><Ulmarker={{color:'indigo-500',fontSize:18}}><Li>One</Li><Li>Two</Li></Ul>
One per selector, and the element goes last
CSS allows a single pseudo-element in a compound selector, at the end. That is a slot rather than a list here, so nesting one inside another is a type error, and a merged component style that manages it anyway is dropped instead of emitting ::before::after, which matches nothing. It is also why the element lands on the target of a group selector: .card:hover .x::before, never .card:hover::before .x.
Where it lands
CSS
/* hover={{ before: { opacity: 1 } }} */.x:hover::before{opacity: 1 }/* md={{ dataAttr: { 'state=open': { after: { opacity: 1 } } } }} */@media(min-width: 768px){.y[data-state="open"]::after{opacity: 1 }}/* hoverGroup={{ card: { before: { opacity: 1 } } }} — the state is the card's, the element is ours */.card:hover .z::before{opacity: 1 }
Text is quoted, and a value that cannot be is refused
content is the one prop whose value is text a caller wrote, and text becomes rule text. So a plain string is quoted and escaped (a quote, a backslash, a newline), a value written as CSS is scanned instead — every quote closed, every parenthesis balanced, no ;, } or @ outside a string — and one that fails produces no rule and no class name, exactly like an unmatched prop value.