# Quick Start

Install Mirror, generate the tokens and the stylesheet, then put a labelled, validated and styled field on screen.

::component-preview{name="FirstComponentPreview"}
```vue
<template>
  <form class="flex w-[17rem] flex-col gap-3" @submit.prevent="submit">
    <Field.Root
      class="group flex flex-col gap-1.5"
      :options="{ name: 'email', validate: validateEmail }"
    >
      <div :class="box">
        <div :class="stack">
          <span :class="line">
            <Input
              v-model="email"
              :class="control"
              type="email"
              placeholder=" "
              autocomplete="off"
              required
            />
          </span>
          <Label :class="label">Email</Label>
        </div>
      </div>

      <Field.Error v-slot="{ messages }" :class="error">
        {{ messages.join(' ') }}
      </Field.Error>
    </Field.Root>

    <Button :class="button" type="submit" :loading="saving">
      {{ saving ? 'Saving…' : 'Save' }}
    </Button>
  </form>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Button, Field, Input, Label } from '@maas/mirror/vue'

const email = ref('')
const saving = ref(false)

const box = [
  'relative isolate flex h-12 w-full cursor-text items-center justify-between gap-1.5',
  'rounded-component-lg border-surface border-2 px-[0.875rem]',
  'transition-all duration-100 ease-linear [&_*]:transition-all [&_*]:duration-100 [&_*]:ease-linear',
  'outline-4 outline-transparent focus-within:focus-ring',
  'group-[[data-invalid=true]:not([data-focused=true])]:border-danger-subtle',
  'group-[[data-invalid=true]:not([data-focused=true])]:bg-danger-subtle',
  'group-data-[disabled=true]:border-disabled-subtle group-data-[disabled=true]:cursor-not-allowed',
].join(' ')

const stack = [
  'group/stack flex h-full max-h-full w-full flex-col-reverse',
  'items-center justify-center gap-0 px-1',
  'focus-within:gap-1 has-[input:not(:placeholder-shown)]:gap-1',
].join(' ')

const line = [
  'relative flex h-0 w-full items-end',
  'group-focus-within/stack:h-[0.9375rem]',
  'has-[input:not(:placeholder-shown)]:h-[0.9375rem]',
].join(' ')

const control = [
  'type-component-lg leading-[normal]! text-surface',
  'block box-content h-[1lh] w-full py-1.25 -my-1.25 appearance-none bg-transparent outline-none',
  'placeholder:text-transparent',
  'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-on-muted',
  'data-[disabled=true]:text-disabled-solid',
].join(' ')

const label = [
  'type-component-lg leading-[normal]! text-surface-muted',
  'flex h-full w-full items-center [--mirror-label-cursor:text]',
  "before:absolute before:-inset-0.5 before:-z-10 before:content-['']",
  'group-focus-within/stack:h-auto group-focus-within/stack:text-[0.6875rem] group-focus-within/stack:[--mirror-label-cursor:auto]',
  'group-has-[input:not(:placeholder-shown)]/stack:h-auto group-has-[input:not(:placeholder-shown)]/stack:text-[0.6875rem] group-has-[input:not(:placeholder-shown)]/stack:[--mirror-label-cursor:auto]',
  'group-[[data-invalid=true]:not([data-focused=true])]:text-danger-muted',
  'group-data-[disabled=true]:text-disabled-muted',
].join(' ')

const error = 'type-component-2xs text-danger-muted'

const button = [
  'inline-flex h-12 items-center justify-center gap-1.5 px-[1.125rem] whitespace-nowrap',
  'rounded-component-lg border-2 border-[transparent] type-component-lg',
  'bg-primary-solid text-primary-on-solid',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:bg-primary-solid-hover active:bg-primary-solid-active',
  'data-[loading=true]:bg-disabled-solid data-[loading=true]:text-disabled-on-solid',
].join(' ')

function validateEmail(value: unknown) {
  const entry = String(value ?? '')

  return entry.length === 0 || entry.includes('@')
    ? null
    : 'That does not look like an email address.'
}

async function submit() {
  saving.value = true
  await new Promise((resolve) => setTimeout(resolve, 1200))
  saving.value = false
}
</script>
```
::

The field above is what this page builds. Its label is properly associated,
its error is wired to `aria-describedby` and validation runs on blur, and none
of the appearance was decided by Mirror.

## Install the package

::package-install
::

A few peer dependencies are needed at runtime as well, and without them
`Select` and `Combobox` fail to resolve.

::package-install{name="@vueuse/core focus-trap @floating-ui/vue"}
::

## Generate the tokens and the stylesheet

Mirror ships no compiled token file, so before the first build you run three
commands: the first writes the config, the other two produce everything your
app reads.

```bash
pnpm mirror init
pnpm mirror tokens
pnpm mirror css
```

`init` writes a `mirror.config.ts` pointing at our token repository, which both
of the other commands read. `tokens` compiles that source into `.maas/tokens/`,
and `css` reads the result and writes `.maas/tailwind.preset.css`.

To edit the tokens rather than read ours, run `mirror init --tokens` instead of
`mirror init`. It copies the token tree into `./tokens` and writes a
`mirror.config.ts` whose `source` reads that directory, so every later
`mirror tokens` compiles files that are checked in with the project. Pass
`--from` to copy a repository of your own instead of ours. See the
[CLI reference](/overview/cli#starting-from-your-own-tokens).

Nothing prompts and nothing is remembered between runs, so we run both build
commands as part of every build rather than committing their output.

```json
{
  "scripts": {
    "build": "mirror tokens && mirror css && nuxt build"
  }
}
```

::note
Everything under `.maas/` is generated, so add the folder to `.gitignore`.
`mirror.config.ts` sits at the root of your project and is the one file you
write rather than the build.
::

## Import the stylesheets

Import the generated preset into your Tailwind stylesheet, after Tailwind
itself, since that is where all the Mirror class names come from.

```css
@import 'tailwindcss';
@import '../../.maas/tailwind.preset.css';
```

Then load the token files. If you are using Nuxt, register the module and list
the token files in `nuxt.config.ts`, and the module loads the component CSS for
you.

```ts
export default defineNuxtConfig({
  modules: ['@maas/mirror/nuxt'],

  css: [
    '~~/.maas/tokens/css/application.css',
    '~~/.maas/tokens/css/theme/dark/application.css',
    '~~/assets/css/tailwind.css',
  ],
})
```

The module registers every Mirror export globally with an `M` prefix, so
`Select.Root` becomes `<m-select-root>` and there is nothing to import. If your
app already ships its own `M*` components, set `mirror: { prefix: 'Mirror' }`.

If you are using Vue on its own, import the token files and the component CSS
once at your entry point. There is no plugin to install.

```ts
import '@maas/mirror/vue/index.css'
import '../.maas/tokens/css/application.css'
import '../.maas/tokens/css/theme/dark/application.css'
import './assets/css/tailwind.css'
```

::warning
`@maas/mirror/vue/index.css` is not optional. The Nuxt module loads it; without
Nuxt, import it yourself. It holds the geometry the parts cannot express in markup: slider thumb
positions, progress width, the hidden inputs, popup sizing, the tabs indicator
and the textarea auto-size box. Without it, `Slider` renders as overlapping
boxes.
::

## Compose the parts

`Field.Root` owns the validity state, `Field.Label` points at the control,
`Input` joins the field on its own, and `Field.Error` only renders while the
field is invalid.

```vue
<script setup lang="ts">
import { Field, Input, Button } from '@maas/mirror/vue'

const email = ref('')

function validateEmail(value: unknown) {
  const entry = String(value ?? '')

  if (entry.length === 0) {
    return 'Enter an email address.'
  }

  return entry.includes('@') ? null : 'That does not look like an email address.'
}
</script>

<template>
  <form @submit.prevent="save">
    <Field.Root :options="{ name: 'email', validate: validateEmail }">
      <Field.Label>Email</Field.Label>
      <Input v-model="email" type="email" placeholder="you@example.com" />
      <Field.Error v-slot="{ messages }">{{ messages.join(' ') }}</Field.Error>
    </Field.Root>

    <Button type="submit">Save</Button>
  </form>
</template>
```

That already works, and it is completely unstyled.

## Paint it with the generated classes

Whenever a part changes state it writes that state to a `data-*` attribute,
which is what the classes below select on, so the stylesheet needs no
JavaScript and no extra props. Use the generated utilities so every colour
stays a token.

```vue
<template>
  <Field.Label
    class="type-component-sm text-surface-muted
      data-[invalid=true]:text-danger-muted"
  >
    Email
  </Field.Label>

  <Input
    v-model="email"
    type="email"
    class="rounded-component-md border border-surface bg-surface-high px-3 py-2
      type-component-sm text-surface
      focus-visible:focus-ring focus-visible:outline-none
      data-[invalid=true]:border-danger-subtle"
  />

  <Field.Error class="type-component-xs text-danger-muted" />
</template>
```

Pair a fill with its matching `on-*` colour: `bg-primary-solid` goes with
`text-primary-on-solid`, and because that pairing is resolved in the token
layer it survives every theme and both colour modes.

```vue
<template>
  <Button
    type="submit"
    class="rounded-component-md px-3.5 py-2 type-component-sm transition-colors
      bg-primary-solid text-primary-on-solid
      hover:bg-primary-solid-hover active:bg-primary-solid-active
      focus-visible:focus-ring focus-visible:outline-none
      data-[loading=true]:bg-primary-light
      data-[loading=true]:text-primary-muted"
  >
    Save
  </Button>
</template>
```

That is the field in the preview above, running in your app.

## Further reading

- [Styling](/components/styling): data attributes, slot props, CSS variables and
  the generated utilities, and how the four combine.
- [Composition](/components/composition): wrap the class list into one component
  per control, and give it the appearance API your project talks about.
- [Theming](/tokens/theming): turn on dark mode and scope a theme to a
  subtree.
