# Configuration

mirror.config.ts is the only file the token CLI reads, and this page documents every key in it. Read it before changing your token structure, because the utilities and themes are generated from whatever shape it describes.

`mirror.config.ts` sits at the root of your project and steers the whole token
build. It is the only file the CLI reads, and both commands need it, so a
project without one stops with an error rather than guessing.

```ts
import { extendMirrorConfig } from '@maas/mirror'

export default extendMirrorConfig({
  source: { type: 'git', repo: 'magicasaservice/mirror-tokens' },
})
```

That is a whole config. `source` is the only key you have to write, and
everything else comes from ours: the `config`, `application`, `radii`, theme and
breakpoint targets, and the `css`, `js`, `ts` and `json` platforms. Run
`mirror init` to have exactly this written for you.

## Extending ours or writing your own

There are two ways to export a config, and the import at the top of the file
says which one you are using.

`extendMirrorConfig` deep-merges what you write over ours, so a config that
names nothing but `source` builds everything Mirror builds. Reach for it when
your tokens follow our structure.

`defineMirrorConfig` takes a complete config and merges nothing. Nothing of
ours survives, so you own every target, and a target we add later does not
quietly appear in your build. Reach for it when your token tree is your own.

```ts
import { defineMirrorConfig } from '@maas/mirror'

export default defineMirrorConfig({
  source: { type: 'local', path: './tokens' },
  options: { platforms: ['css', 'js', 'ts', 'json'] },
  targets: {
    config: { selector: ':root', files: [{ src: 'config' }] },
    application: {
      include: [{ src: 'config' }],
      selector: ':root',
      files: [{ src: 'application' }],
    },
  },
})
```

The merge happens inside `extendMirrorConfig`, at the moment you write the
file, rather than somewhere in the CLI. Three rules govern it.

Objects merge key by key, one level at a time, so patching a single key of one
of our targets keeps the rest of it.

```ts
targets: { 'theme/dark/application': { selector: '[data-mode="dark"]' } }
```

Arrays replace rather than concatenate. Every array in this config is a set
rather than a list to append to, so naming `platforms`, `files`, `include`,
`atRule`, `enum` or `tokens.ignore` replaces ours outright.

```ts
options: { platforms: ['css'] }
```

And `null` drops one of our targets.

```ts
targets: { radii: null }
```

Our own pieces are exported as plain values, so a target of yours can reuse
one of ours or sit on the same selector ladder.

```ts
import { extendMirrorConfig, stockSelectors, stockTargets } from '@maas/mirror'

export default extendMirrorConfig({
  source: { type: 'local', path: './../design-tokens/tokens' },
  targets: {
    'theme/brand/application': {
      include: [{ src: 'config' }],
      selector: stockSelectors.theme('brand'),
      files: [{ src: 'theme/brand' }],
    },
    'theme/print/application': {
      ...stockTargets.application,
      atRule: ['@media print'],
    },
  },
})
```

`stockConfig`, the whole thing, and `stockOptions` are exported alongside them.

## Where the file sits

Two commands read it, at two different moments, and neither of them assumes
anything the file does not state.

```
tokens/*.json                    the source, one file per group
  ↓                              mirror tokens, reading `source` and `targets`
.maas/tokens/{css,js,ts,json}/   one compiled file per target
  ↓                              mirror css, reading `utilities`
.maas/tailwind.preset.css        the classes and the @theme block
```

`dist` decides where that middle line is written and defaults to `.maas`. Both
commands read it, so moving the compiled output is one key rather than a flag
on each run.

## Source

`source` says where the raw token JSON comes from, and it is required because
there is no sensible default for somebody else’s tokens. It is either a Git
repository or a local path.

Which type you pick depends on who owns the tokens. Point at a Git repository
when the tokens are a shared source of truth that several projects follow:
nothing is checked in, and a token change reaches every project on its next
build. Point at a local path inside the project when the project owns its
tokens: the files are checked in, and token changes go through review like any
other code.

```ts
source: {
  type: 'git',
  repo: 'magicasaservice/mirror-tokens',
}
```

```ts
source: {
  type: 'local',
  path: './../design-tokens/tokens',
}
```

A local path is what `mirror init --tokens` writes. It copies the token tree
into `./tokens` and sets `source` to `{ type: 'local', path: './tokens' }`, so
the tokens are a directory under revision control rather than a repository
fetched on every build. See the
[CLI reference](/overview/cli#starting-from-your-own-tokens).

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: type
        description: Whether the tokens are fetched or read from disk.
      - label: '''git'' | ''local'''
  - items:
      - label: repo
        description: '`owner/name` of the token repository. `git` only.'
      - label: string
  - items:
      - label: provider
        description: Where the repository is hosted. Defaults to `github`.
      - label: '''github'' | ''gitlab'' | ''bitbucket'' | ''sourcehut'''
  - items:
      - label: ref
        description: Branch, tag or commit to fetch. Defaults to `main`. `git` only.
      - label: string
  - items:
      - label: dir
        description: Directory inside the repository holding the token JSON. Defaults to `tokens`. `git` only.
      - label: string
  - items:
      - label: path
        description: Directory holding the token JSON, resolved from your project root. `local` only.
      - label: string
---
::

::note
A checkout can point somewhere else for one run without editing the file. Pass
`--source github:owner/repo#main`, and everything else stays as the config
wrote it. See the [CLI reference](/overview/cli).
::

## Options

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: platforms
        description: Which outputs build. `ts` writes its declarations next to the JS modules.
      - label: '(''css'' | ''js'' | ''ts'' | ''json'' | ''ios'' | ''android'')[]'
        escape: true
  - items:
      - label: debug
        description: Turns on Style Dictionary’s warnings and verbose logging, and makes a broken reference throw rather than print.
      - label: boolean
---
::

`ios` and `android` are opt-in through the same key. The `ts` platform writes
its declarations alongside the JS modules rather than into a directory of its
own. Leave `js` in the list whatever else you do, because the utility generator
reads the compiled JS modules rather than the CSS.

## Build targets

`targets` is an object whose keys are output paths. Each entry describes one
output file, made up of a set of token files and a selector to emit them under.

```ts
export default extendMirrorConfig({
  source: { type: 'local', path: './tokens' },
  targets: {
    'theme/brand/application': {
      include: [{ src: 'config' }, { src: 'application' }],
      selector: '[data-theme="brand"][data-theme][data-theme]',
      files: [{ src: 'theme/brand' }],
    },
  },
})
```

The key is the file name, written once. `theme/brand/application` writes
`css/theme/brand/application.css`,
`js/theme/brand/application.variables.js`, `json/theme/brand/application.json`
and `ios/theme_brand_application.swift`, and each segment is kebab-cased on the
way, so write the key the way you want the file spelled. Add a target and you
get a file, and add that file to your `css` array and you get a theme.

Build order is the object’s key order, so a config builds top to bottom the way
it reads.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: files
        description: The token files this target emits, each one a `src` path under your token source without the `.json`.
      - label: 'TokenFile[]'
  - items:
      - label: include
        description: Files this target resolves its references against. Nothing from them is emitted.
      - label: 'TokenInclude[]'
  - items:
      - label: selector
        description: 'CSS selector the block is emitted under. Defaults to `:root, [data-color-mode="light"]`. CSS only.'
      - label: string
  - items:
      - label: atRule
        description: 'At-rules wrapped around the selector, outermost first, such as `[''@media (min-width: 640px)'']`. CSS only.'
      - label: string[]
  - items:
      - label: enum
        description: Swift enums the target’s tokens nest inside. `ios` only.
      - label: string[]
  - items:
      - label: log
        description: Prints every token of this target to the console as it is transformed. For inspecting a build, not for shipping.
      - label: boolean
---
::

A `TokenInclude` is a `src` and nothing else. A `TokenFile` is a `src` plus an
optional `tokens.ignore`, which is a list of globs over the token path with its
segments joined by `/`. The list is read the way `.gitignore` is: `'**'` drops
everything and a leading `!` puts some of it back, so the pattern below emits
nothing but the palette.

```ts
files: [
  { src: 'config', tokens: { ignore: ['**', '!**/color/palette/**'] } },
]
```

::caution
The ignore lists of every file in one target are concatenated and then applied
to all of them. If one file in a target needs a filter and another does not,
split them into two targets.
::

Source files are read as JSON, and only the files a target lists can contribute
to it: a token is emitted when its own file path matches one of the target’s
`src` entries and the ignore list lets it through.

## Selectors

The generated files load in any order, so the selectors do the ranking.
Repeating an attribute selector adds one to its specificity without changing
what it matches, and that gives every target a rung to sit on.

::docs-table
---
columns:
  - label: Rung
  - label: Selector
  - label: Outranks
rows:
  - items:
      - label: Base
        plaintext: true
      - label: ':root, [data-color-mode="light"]'
        description: '`stockSelectors.base`'
      - label: Nothing, this is the default
        plaintext: true
  - items:
      - label: Colour mode
        plaintext: true
      - label: '[data-color-mode="dark"][data-color-mode]'
        description: '`stockSelectors.dark`'
      - label: The base
        plaintext: true
  - items:
      - label: Theme
        plaintext: true
      - label: '[data-theme="brand"][data-theme][data-theme]'
        description: '`stockSelectors.theme(''brand'')`'
      - label: The base and either colour mode
        plaintext: true
  - items:
      - label: Theme under a colour mode
        plaintext: true
      - label: '[data-color-mode="dark"] [data-theme="brand"][data-theme][data-theme]'
        description: '`stockSelectors.themeDark(''brand'')`'
      - label: The theme
        plaintext: true
  - items:
      - label: Breakpoint
        plaintext: true
      - label: ':root:root, [data-color-mode][data-color-mode]'
        description: 'Inside the `atRule` of the target. `stockSelectors.breakpoint`'
      - label: The base
        plaintext: true
---
::

The base selector also names `[data-color-mode="light"]`, so a light subtree
inside a dark document reads the base values again.

That ladder is a property of this config rather than of the build. Nothing in
the pipeline reads a selector or checks one against another, so the files end up
ranked by specificity or by load order, the way any CSS is. Repeating attributes
picks specificity, so [Theming](/tokens/theming) can say the files load in any
order. If your own config ranks by load order instead, every target can share
one selector, but the order of the `css` array becomes load-bearing and a theme
that arrives through a lazily imported stylesheet ends up wherever the bundler
puts it.

## Building a theme

Everything a theme leaves out keeps resolving from the base file, so declare
only what the theme changes.

```ts
export default extendMirrorConfig({
  source: { type: 'local', path: './tokens' },
  targets: {
    'theme/brand/application': {
      include: [{ src: 'config' }, { src: 'application' }],
      selector: stockSelectors.theme('brand'),
      files: [
        { src: 'theme/brand', tokens: { ignore: ['**', '!**/color/**'] } },
      ],
    },
  },
})
```

If the theme needs its own dark mode, add a second target one rung up and
restate only the tokens that move. `stockSelectors.themeDark` names all three
shapes for you, so the theme is found whether it sits on the same element as
the colour mode, inside it, or around it.

```ts
'theme/brand/dark/application': {
  include: [{ src: 'config' }, { src: 'application' }],
  selector: stockSelectors.themeDark('brand'),
  files: [{ src: 'theme/brand/dark' }],
}
```

Then list the generated files in your `css` array, in any order. See
[Theming](/tokens/theming) for what a consumer does with them.

## Component targets

Mirror ships no `tokens/component/*` of its own, since it has no components to
tokenise, but the mechanism is there for repositories that do. Generate targets
that follow a pattern with `Object.fromEntries`.

```ts
import { defineMirrorConfig, type TokenTarget } from '@maas/mirror'

const components = ['button', 'input', 'select']
const base = [{ src: 'config' }, { src: 'application' }]

export default defineMirrorConfig({
  source: { type: 'local', path: './tokens' },
  targets: {
    ...Object.fromEntries(
      components.map((component): [string, TokenTarget] => [
        `component/${component}`,
        {
          include: base,
          selector: ':root, [data-color-mode="light"]',
          files: [{ src: `component/${component}` }],
        },
      ])
    ),
  },
})
```

## Reshaping the token tree

Nothing in the build knows what a colour is. It reads whatever JSON your token
source holds, and the generator reads whatever the build produced, so the shape
of the tree is yours. Three names are the exception.

::docs-table
---
columns:
  - label: Name
  - label: Where it binds
rows:
  - items:
      - label: app
        description: Required. The generator finds the token tree by looking for an `app` key inside a compiled module, so a tree rooted at any other name is not found and `mirror css` stops.
      - label: The root of every semantic file
        plaintext: true
  - items:
      - label: component
        description: Reserved. Dropped from every generated name, so `app.color.component.primary.bg` becomes `--app-color-primary-bg` and two paths differing only by a `component` segment collide.
      - label: Any segment
        plaintext: true
  - items:
      - label: default
        description: Reserved. Dropped from generated names too, and read as the value of the group above it, so `bg.solid.default` is also readable as `bg.solid`.
      - label: Any segment
        plaintext: true
---
::

Everything else is free, the primitive layer included. `config` is what our
source happens to name both that file and the root key inside it, and neither
of the two is fixed.

What is not free is the set of paths the shipped utility roles read, because the
generator addresses them literally.

::docs-table
---
columns:
  - label: Role
  - label: Reads
rows:
  - items:
      - label: '`bg`, `fg`, `border`'
        description: Each role reads two patterns, one for the groups sitting directly under `color` and one for the intents nested under `component`, and both produce the same class names.
        plaintext: true
      - label: 'color.*.bg.*, color.component.*.bg.*.*'
        escape: true
  - items:
      - label: type-surface-*
        escape: true
        description: 'Eight roles, one per group: display, title, subtitle, callout, body, code, footnote, caption. Each reads a size axis plus its font family, weight and case.'
      - label: 'fontSize.surface.<group>.*, fontFamily.surface.<group>'
        escape: true
  - items:
      - label: type-component
        description: 'Reads `fontSize.component.text.*`, plus `fontFamily.component.text`, `textCase.component`, `lineHeight.default` and `fontFamily.component.number`.'
      - label: 'fontSize.component.text.*'
        escape: true
  - items:
      - label: focus
        description: 'Four literals: `color.component.focus.border`, `color.component.focus.outline`, `dimension.component.border` and `dimension.component.outline.focus`.'
      - label: 'color.component.focus.*, dimension.component.*'
        escape: true
  - items:
      - label: Passthroughs
        description: Fed straight into Tailwind’s own `--radius` and `--shadow` namespaces rather than through a role.
      - label: 'dimension.surface.radius.**, dimension.component.radius.**, boxShadow.**'
        escape: true
---
::

A discovery path that matches nothing is survivable: the role is skipped with a
warning and its classes are absent. A literal that resolves to nothing is
not, and the build stops with the path it could not find. That is the difference
between reshaping colour, where every role discovers what it needs, and
reshaping the focus ring, whose four values are literals.

There is one more rule the generator enforces while discovering. A family whose
value template mentions two sub-namespaces needs both of them to find the same
set of rungs, so `fontSize.surface.title.*` and `letterSpacing.surface.title.*`
have to agree; if they do not, the build stops and names the family.

When the tree moves, four things follow it.

- The `src` paths in `include` and `files`, which are file names under your
  token source.
- The `tokens.ignore` globs, which are written against the token path.
- `utilities.overrides.<role>.values` and `utilities.passthrough[].from`, which
  are the paths above. See
  [Extending utilities](/utilities/extending#pointing-a-role-at-different-paths).
- `utilities.targets`, if you renamed a target the stylesheet reads.

## A custom structure end to end

Say a brand keeps our typography but writes colour without a `component`
segment, and uses a density axis where we use a colour mode. The source is three
files.

```
tokens/config.json             config.color.palette.*
tokens/application.json        app.color.brand.bg.solid, app.color.ring.border, …
tokens/density/compact.json    app.dimension.*, app.fontSize.*
```

Nothing of ours applies to a tree like that, so the config declares all three
files itself and the selectors are whatever the brand wants to select on.

```ts
import { defineMirrorConfig } from '@maas/mirror'

export default defineMirrorConfig({
  source: { type: 'local', path: './tokens' },
  options: { platforms: ['css', 'js', 'ts', 'json'] },
  targets: {
    config: {
      selector: ':root',
      files: [{ src: 'config' }],
    },
    application: {
      include: [{ src: 'config' }],
      selector: ':root',
      files: [{ src: 'application' }],
    },
    'density/compact/application': {
      include: [{ src: 'config' }, { src: 'application' }],
      selector: '[data-density="compact"][data-density]',
      files: [{ src: 'density/compact' }],
    },
  },
})
```

That writes `.maas/tokens/css/application.css` and
`.maas/tokens/css/density/compact/application.css`, and the attribute is
`data-density` because that is what the selector says. Nothing else in Mirror
knows the name.

Then the utilities half, in the same file. The colour roles are pointed one
segment shallower, and the focus ring, whose values are literals, is dropped and
rewritten rather than narrowed.

```ts
utilities: {
  overrides: {
    bg: { values: { value: 'color.*.bg.**' } },
    fg: { values: { value: 'color.*.fg.**' } },
    border: { values: { value: 'color.*.border.*' } },
    focus: false,
  },
  emitters: [
    {
      role: 'ring',
      values: {},
      families: [
        {
          prefix: 'focus-ring',
          static: true,
          properties: {
            'border-color': '@color.ring.border',
            'border-width': '@dimension.border',
            'outline-color': '@color.ring.outline',
            'outline-width': '@dimension.outline.focus',
            'outline-style': 'solid',
          },
        },
      ],
    },
  ],
}
```

The class names come out unchanged. A rung is built from the segments the
wildcards captured rather than from the path they sit at, so
`app.color.brand.bg.solid.hover` is `bg-brand-solid-hover` here exactly as
`app.color.component.primary.bg.solid.hover` is `bg-primary-solid-hover` in
ours. The typography roles keep working because the brand kept
`fontSize.surface.*`; a tree that renames those groups loses `type-surface-*`
with a warning on the console, and the answer is the same as for the focus ring,
meaning drop the roles and append your own.

One thing is left over. The density target defines sizes the base one does not,
so name it under `utilities.targets` and the generator folds both trees into
one. The next section covers that.

```ts
utilities: {
  targets: ['application', 'density/compact/application'],
}
```

## Reading a scoped build into the stylesheet

The utility generator discovers against one compiled target, `application`. If a
scale lives in a scoped build instead, such as a size, density or breakpoint
target, name the extra targets and the generator folds them into one tree.

```ts
utilities: {
  targets: ['application', 'size/mid/application'],
}
```

The names are the keys of `targets`, since the compiled files are spelled the
same way in every platform. The first target wins every key it defines, and a
later one only contributes keys the earlier ones lack. A scoped build restates
the same variable *names* under a different selector, so the only useful thing
it can add is a key the base target does not have at all.

`utilities.variables` is the other key the generator takes from this file. Left
alone it reads the variables tree, whose leaves are `var(--app-*)` strings, so
the emitted utilities point at custom properties. See the
[CLI reference](/overview/cli#mirror-css) for what turning it off costs.

## What the CLI fixes

Some of the layout is not the config’s to decide.

::docs-table
---
columns:
  - label: Fixed
  - label: What
rows:
  - items:
      - label: '<dist>/tokens/'
        description: Every platform writes into a directory of its own underneath it, and `dist` defaults to `.maas`.
      - label: The compiled tokens
        plaintext: true
  - items:
      - label: '<dist>/.temp/'
        description: Where a Git source is downloaded before it is compiled, and removed again afterwards.
      - label: The fetched source
        plaintext: true
  - items:
      - label: 'index.css, index.js'
        description: Written into every generated directory, importing its files and its subdirectories, so one import reaches a whole tree.
      - label: Barrel files
        plaintext: true
  - items:
      - label: '<dist>/tailwind.preset.css'
        description: The generated stylesheet, alongside a `utilities.json` listing every class name and theme key the run emitted.
      - label: The preset
        plaintext: true
  - items:
      - label: figma.config.json
        description: Copied from the project root into `dist` when it exists, so the folder can be handed to the Figma plugin as is.
      - label: Figma handoff
        plaintext: true
---
::

Token sources are read as JSON, and the `component` and `default` segments are
dropped from every generated name in every platform.

## Further reading

- [Extending utilities](/utilities/extending): the other half of the
  file, under `utilities`.
- [CLI](/overview/cli): the commands that read all of this.
