# Styling

Read this to style a Mirror component from its data attributes, slot props, public CSS variables and generated utility classes.

Mirror’s components don’t come with a look. They render the markup, keep track
of their state and tell you about it, and leave everything visual to you. In
practice there are four places where that happens: data attributes, slot props,
CSS variables and the generated utility classes.

## The functional CSS

Every component carries the geometry its parts cannot express in markup:
slider thumb positions, progress width, the hidden inputs, popup sizing
against the anchor, the tabs indicator and the textarea auto-size box. That
CSS ships inside the component files, so your bundler extracts it for exactly
the components you use and there is nothing to import.

Those rules hang off a class every part renders: its own name, kebab cased and
prefixed with `mirror-`. `Select.Trigger` is `.mirror-select-trigger`, and a
`Root` takes the component name on its own, so `Avatar.Root` is
`.mirror-avatar`. It is always the first class on the element, ahead of anything
you pass through `class`, so a part is easy to recognize in the inspector.

## Style from data attributes

Whenever a part changes state, it writes that state to a `data-*` attribute on
its element. This is what we style against, both inside Mirror and in our own
apps, which is also why none of the components take a `variant` or `color`
prop.

Boolean states are written as the string `true` and removed entirely when
false, so selectors should check the value rather than the mere presence of the
attribute.

```css
.checkbox[data-disabled='true'] {
  opacity: 0.5;
}
```

In a template, the Tailwind variant is the same selector.

```vue
<template>
  <Checkbox.Root
    class="border-surface data-[disabled=true]:opacity-50
      data-[state=checked]:bg-primary-solid"
  />
</template>
```

Enumerated state is written as a word. `data-state` holds `checked`,
`unchecked`, `indeterminate`, `open`, `closed`, `on` or `off` depending on the
part, and `data-status`, `data-orientation`, `data-side` and `data-align` hold
the rest. Each component page lists what its parts write.

## The field state set

A control’s relationship to its `Field` is described by a set of state
attributes. Every control writes all of them and so does every `Field` part, so
you can style the label from the control’s validity without a wrapper class.

::docs-table
---
columns:
  - label: Attribute
  - label: Description
rows:
  - items:
      - label: data-disabled
      - label: 'The control or its field is disabled.'
        plaintext: true
  - items:
      - label: data-readonly
      - label: 'The control is read-only.'
        plaintext: true
  - items:
      - label: data-required
      - label: 'The control is required.'
        plaintext: true
  - items:
      - label: data-valid
      - label: 'Validation has run and passed.'
        plaintext: true
  - items:
      - label: data-invalid
      - label: 'Validation has run and failed, or `invalid` was set explicitly.'
        plaintext: true
  - items:
      - label: data-dirty
      - label: 'The value has changed from its initial value.'
        plaintext: true
  - items:
      - label: data-touched
      - label: 'The control has been focused and blurred at least once.'
        plaintext: true
  - items:
      - label: data-filled
      - label: 'The value is non-empty.'
        plaintext: true
  - items:
      - label: data-focused
      - label: 'The control currently holds focus.'
        plaintext: true
  - items:
      - label: data-scope
      - label: 'The part’s role in the field. One of `label`, `control`, `description` or `error`.'
        plaintext: true
---
::

Where a component page says “the field state set”, it means these, and only the
additions are listed per part.

Outside a `Field`, a control writes the ones it can work out on its own, which
are `data-disabled`, `data-readonly`, `data-required`, `data-filled` and
`data-focused`. The validity attributes are left off rather than guessed.

`data-scope` is only written by the parts whose role is fixed, so a control
that could sit in any scope is left without one.

```css
[data-scope='label'][data-invalid='true'] {
  color: var(--app-color-danger-fg-muted);
}
```

## Read the same state as slot props

Data attributes are enough when the difference is a declaration. When you need
different *elements*, read the same state from the slot, where the names are
the same minus the `data-` prefix and in camelCase.

```vue
<template>
  <Checkbox.Root v-slot="{ checked, indeterminate, disabled }">
    <icon :name="indeterminate ? 'dash' : checked ? 'check' : 'blank'" />
  </Checkbox.Root>
</template>
```

`data-readonly` arrives as `readOnly`.

## Override the public CSS variables

Where a shipped value is a taste decision rather than a mechanism, the
component reads it from a public variable with a fallback that doubles as the
default.

```css
.mirror-slider-thumb {
  block-size: var(--mirror-slider-thumb-size, 1rem);
}
```

Set it on `:root` to move every instance in the app, or on any ancestor
element to move only the parts inside that subtree.

```css
:root {
  --mirror-slider-thumb-size: 1.25rem;
  --mirror-slider-track-size: 0.375rem;
}
```

Public variables are named `--mirror-<component>[-<part-or-state>]-<property>`
and are documented one for one on each component page.

A few of them work the other way round: the component writes them and you read
them to position your own decoration. The `Select`, `Combobox` and
`Autocomplete` floating boxes write `--rack-floating-anchor-width` and
`--rack-floating-available-height`, and setting either has no effect.

```css
.select-popup {
  min-inline-size: var(--rack-floating-anchor-width);
}
```

Variables under a short prefix (`--mpr-`, `--msl-`, `--mta-`, `--mtb-`) are
internal to the component. Read one where a page tells you it is useful, as
`Progress` does with `--mpr-percentage`, and never set one.

## Paint with the generated utilities

Color, type, radius and shadow come from the classes `mirror css` generates. A
class exists because a token exists, so every value you apply stays themeable.

```vue
<template>
  <Field.Label class="type-component-sm text-surface-muted">Email</Field.Label>
</template>
```

Pair a fill with its matching `on-*` color: `bg-primary-solid` goes with
`text-primary-on-solid`. See [Utilities](/utilities/overview) for the families.

## Bring your own styling layer

Everything above is plain CSS surface: a stable class on every part, state in
data attributes, defaults in custom properties. None of it assumes Tailwind,
so any tool that can target a selector can style a Mirror component.

Scoped SFC styles address the part class directly, and state stays an attribute
selector, since the components toggle no state classes. In a CSS Module, wrap
the part class in `:global()`: a module hashes every class name it declares, so
a bare `.mirror-select-trigger` compiles to a name nothing on the page carries.

```vue
<style scoped>
.mirror-select-trigger {
  border: 2px solid var(--mirror-border-surface);
}

.mirror-select-trigger[data-content-open='true'] {
  border-color: var(--mirror-border-primary-subtle);
}
</style>
```

A runtime CSS-in-JS library works the same way: generate a class, pass it
through `class`, and write the state rules against the `data-*` attributes
rather than swapping classes from script.

Whichever layer you pick, take color, type, radius and shadow from the
`--mirror-*` token variables instead of literal values. A literal is correct
in one theme and one color mode; the variable is correct in all of them,
which is the same guarantee the generated utilities give you.

## How the four combine

Most controls end up using all four at once. The utilities supply the palette,
the `data-*` variants switch between values in it, a public variable sizes what
markup cannot, and the slot decides what gets rendered.

```vue
<template>
  <Checkbox.Root
    class="size-5 rounded-component-2xs border-2 border-surface bg-surface-high
      transition-colors
      data-[state=checked]:bg-primary-solid
      data-[state=indeterminate]:bg-primary-muted
      data-[disabled=true]:opacity-50
      outline-4 outline-transparent focus-visible:focus-ring"
    v-slot="{ checked, indeterminate }"
  >
    <Checkbox.Indicator class="text-primary-on-solid">
      <icon :name="indeterminate ? 'dash' : 'check'" v-if="checked || indeterminate" />
    </Checkbox.Indicator>
  </Checkbox.Root>
</template>
```

Reserve the ring before focus arrives. `focus-ring` sets a border width and an
outline width as well as their colors, so a control that has neither at rest
grows the moment it takes focus, and animates its outline out of whatever
`currentColor` happens to be, which reads as a white flash in dark mode. Give
every element that receives `focus-ring` a `border-2` and an
`outline-4 outline-transparent` at rest, so focus changes color and nothing
else.

## Further reading

- [Composition](/components/composition): wrap that class list into one component
  per control.
- [Animation](/components/animation): the transition names, and where timing
  lives.
