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.
<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.
<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.
<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.
<Buttonprops={{'data-saved':saved?'yes':'no'}}onClick={()=>setSaved(!saved)}dataAttr={{'saved=yes':{after:{content:' ✓',color:'emerald-500'}},'saved=no':{after:{content:' •',color:'slate-400'}},}}>
Save draft
</Button>
placeholder, selection, marker
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.
<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.