# Pseudo-Elements

_Box Kite 2.2.5 · a markdown copy of https://www.box-kite.dev/pseudo-elements/_

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
<Box
  before={{ width: 2, bgColor: 'indigo-500' }}   // ::before  — content comes with it
  after={{ content: 'attr(data-suffix)' }}       // ::after
  placeholder={{ color: 'slate-400' }}           // ::placeholder
  selection={{ bgColor: 'indigo-200' }}          // ::selection
  marker={{ color: 'indigo-500' }}               // ::marker
  firstLine={{ fontWeight: 600 }}                // ::first-line
  firstLetter={{ fontSize: 48 }}                 // ::first-letter
  backdrop={{ bgColor: 'slate-900' }}            // ::backdrop
  fileButton={{ 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
<Box before={{ content: 'empty' }} />                      // content: ''   (the default, spelled out)
<Box before={{ content: 'New' }} />                        // content: "New"  — text is quoted for you
<Box after={{ content: '"Step " counter(step)' }} />       // written as CSS, so a sequence works
<Box after={{ content: 'attr(data-suffix)' }} />           // attr(), counter(), url(), var()
<Box before={{ 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.

**after + attr()**

```jsx
<Button
  props={{ 'data-count': 3 }}
  position="relative"
  after={{
    content: 'attr(data-count)',
    position: 'absolute',
    top: -2,
    right: -2,
    width: 5,
    height: 5,
    borderRadius: 5,
    bgColor: 'rose-500',
    color: 'white',
    fontSize: 11,
    display: 'grid',
    placeContent: 'center',
  }}
>
  Inbox
</Button>
```

## An underline that grows, and only exists on hover

`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**

```jsx
<Box
  tag="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.

**dataAttr + after**

```jsx
<Button
  props={{ '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.

**placeholder, selection, marker**

```jsx
<Textbox
  props={{ placeholder: 'Search projects…' }}
  placeholder={{ color: 'indigo-400', fontStyle: 'italic' }}
/>

<P selection={{ bgColor: 'indigo-500', color: 'white' }}>Select this sentence.</P>

<Ul marker={{ 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.

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

