# 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.

## Import the component CSS

The Nuxt module loads `@maas/mirror/vue/index.css` for you; without Nuxt,
import it once at your entry point. It is around 6 KB of the geometry the 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.

```ts
import '@maas/mirror/vue/index.css'
```

Without it most components still look fine, but the slider thumbs pile up in
one corner and popups size to their content rather than their trigger, so
import it before anything else.

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 recognise 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`. A few parts shorten a name and say so in
their own table, as `Select.Trigger` does when it writes `data-popup-open` and
hands the slot `open`.

## 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

Colour, 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-*` colour: `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-popup-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 colour, type, radius and shadow from the
`--mirror-*` token variables instead of literal values. A literal is correct
in one theme and one colour 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 colours, 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 colour 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.
