# Custom Tokens

Export a token set of your own to Figma, and decide how its collections, text styles and font weights come out.

The Figma export reads its collections from `figma.collections` in your
`mirror.config.ts`, so a token set that shares nothing with ours comes out
as its own variables. What it needs from you is that key, declaring each
collection with the targets its modes read. Text styles additionally need a
typography file authored against your own paths, or `figma.typography: false`
to skip them, and a font weight outside the built-in table needs a face name
in `figma.fonts`. Each of these is walked below, starting with a small set
built end to end.

## A small set, end to end

The token source is two files. `tokens/brand.json` holds the base set, with
no `app.` prefix anywhere, and `tokens/theme/dark.json` restates the four
colors and nothing else.

```json
{
  "brand": {
    "color": {
      "bg": {
        "base": { "$value": "oklch(0.97 0.01 90)", "$type": "color" },
        "raised": { "$value": "oklch(1 0 0)", "$type": "color" }
      },
      "fg": {
        "default": { "$value": "oklch(0.25 0.02 90)", "$type": "color" }
      },
      "accent": { "$value": "oklch(0.65 0.17 45)", "$type": "color" }
    },
    "radius": {
      "card": { "$value": "0.75rem", "$type": "dimension" },
      "pill": { "$value": "62rem", "$type": "dimension" }
    },
    "heading": {
      "size": { "$value": "2rem", "$type": "fontSize" }
    }
  }
}
```

The heading size deliberately declares `$type: "fontSize"`, singular, which
is off the export’s vocabulary; the run below shows how such a token is
handled. The config declares both targets, maps them into a color and a
dimension collection, and turns text styles off, since this source has no
typography file.

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

export default defineMirrorConfig({
  source: { type: 'local', path: './tokens' },
  options: { platforms: ['css', 'json'] },
  figma: {
    typography: false,
    collections: [
      {
        name: 'Color',
        types: ['color'],
        modes: [
          { name: 'Base', targets: ['application'] },
          {
            name: 'Dark',
            targets: ['application', 'theme/dark/application'],
          },
        ],
      },
      {
        name: 'Dimension',
        types: ['dimension'],
        modes: [{ name: 'Base', targets: ['application'] }],
      },
    ],
  },
  targets: {
    application: {
      selector: ':root',
      files: [{ src: 'brand' }],
    },
    'theme/dark/application': {
      include: [{ src: 'brand' }],
      selector: '[data-mode="dark"]',
      files: [{ src: 'theme/dark' }],
    },
  },
})
```

Run `pnpm mirror tokens`, then `pnpm mirror figma`, and the export comes out
as follows.

```
.maas/figma/
  color/base.json
  color/dark.json
  dimension/base.json
  styles.json
```

The two collections come out exactly as declared. The `Dark` mode reads
`application` first and `theme/dark/application` after it, so it holds all
four colors with the restated dark values winning, and the dimension
collection holds the radii in its single `Base` mode. The heading size
reached no collection at all, because neither `types` list claims
`fontSize`, which the run reports rather than keeping quiet about:

```
1 token(s) of $type 'fontSize' are claimed by no collection and will not reach Figma.
```

Inside the mode files, the tree arrives under its own names. This entry in
`color/base.json` imports as the variable `brand/color/accent`, resolved to
sRGB with the token path and the authored value in its description.

```json
"accent": {
  "$type": "color",
  "$value": {
    "colorSpace": "srgb",
    "components": [0.878228, 0.399019, 0.135458],
    "alpha": 1,
    "hex": "#e06623"
  },
  "$description": "brand.color.accent · oklch(0.65 0.17 45)"
}
```

## Declaring collections

`figma.collections` states which collections and modes the export writes,
and the key is required. Running `mirror figma` without it fails, and the
error prints a suggested roster read off your targets, ready to paste: every
collection opens with a `Base` mode reading `application`, and each
`theme/<name>/application` or `breakpoint/<name>/application` target that
declares tokens of a collection’s types is offered as a further mode. A
first config can start by pasting that suggestion and trimming it, and what
ends up in the file is the whole roster; the export adds nothing of its own.

A roster that crosses a theme axis with a density axis looks like this.

```ts
figma: {
  collections: [
    {
      name: 'Color',
      types: ['color'],
      modes: [
        { name: 'Base', targets: ['application'] },
        { name: 'Dark', targets: ['application', 'theme/dark/application'] },
      ],
    },
    {
      name: 'Dimension',
      types: ['dimension'],
      modes: [
        { name: 'Base', targets: ['application'] },
        {
          name: 'Compact',
          targets: ['application', 'density/compact/application'],
        },
      ],
    },
  ],
}
```

`name` is the collection as Figma shows it, and the directory its mode files
are written into. `types` names the source `$type` values that belong to the
collection, and tokens of any other type in those targets are ignored. Each
mode lists keys of `targets`, read left to right with the last target that
declares a token winning it, so an overlay only needs to restate what it
changes. A mode may reference any target in the config, whatever axis its
key sits under, which is what the `Compact` mode above does with
`density/compact/application`.

The run checks the roster against the targets in both directions. A mode
naming a key that `targets` does not have fails the run, and a target under
`theme/` or `breakpoint/` ending in `/application`, nested keys included,
that no mode references is reported on every run:

```
Target 'theme/contrast/application' is referenced by no figma.collections mode and will not reach Figma.
```

Targets under other axes are not checked, so a density target referenced by
no mode passes without a warning.

## Names and types

The variable names are the token paths, slash-joined. The two stripping rules
are written against our tree: a color path starting `app.color.` loses that
prefix and its `component` segment, and any other path loses a leading `app.`
alone. A path that starts with neither passes through whole, with nothing
stripped and no segment folded, so the difference is purely cosmetic.

::docs-table
---
columns:
  - label: Token
  - label: Variable
rows:
  - items:
      - label: app.color.component.primary.bg.solid.default
      - label: primary/bg/solid/default
  - items:
      - label: brand.color.bg.base
      - label: brand/color/bg/base
---
::

Which tokens become variables at all is decided by the `types` your
collections claim. Our compiler writes five typography type names,
`fontFamily`, `fontWeight`, `fontSizes`, `lineHeight` and `letterSpacing`,
and our stock typography collection claims exactly those. Note the plural:
sizes are `fontSizes`, and a source declaring `fontSize` instead sees those
tokens in the warning the small set above tripped rather than in Figma. A
`$type` no collection claims is never dropped silently; it is counted and
reported once per run.

## Text styles

The composite text styles do not come out of the compiled targets. They
resolve from a typography file in your token source, `figma/type.json` unless
`figma.typography` names another path, which `mirror tokens` copies verbatim
to `<dist>/tokens/figma/type.json`. Each token in it is a composite whose
five members are references into your own compiled base, so authoring your
own is a matter of pointing those references at your own paths.

```json
{
  "heading": {
    "lg": {
      "$type": "typography",
      "$value": {
        "fontFamily": "{brand.heading.family}",
        "fontWeight": "{brand.heading.weight}",
        "fontSize": "{brand.heading.size.lg}",
        "lineHeight": "{brand.heading.leading}",
        "letterSpacing": "{brand.heading.tracking}"
      }
    }
  }
}
```

That token becomes the text style `heading/lg`, named by its path in the
file. All five members are required, each one a reference in braces, and each
reference resolves one hop into the compiled base of the typography
collection, the tree its first declared mode reads; a member that is
missing or does not resolve fails the run naming the token and the member. Family, weight
and size have to resolve to tokens the typography collection exports as
variables, because those three fields are bound rather than copied, which is
also why the referenced size has to be a `fontSizes` token even though the
member key is `fontSize`.

If your source has no such file, set `figma.typography: false` and the run
skips text styles and nothing else: the variables and the effect styles still
come out. Left unset with no file on disk, `mirror figma` stops and names the
path it looked for.

## Font weights

A text style needs a concrete face name before Figma can load the font, so
the numeric weight resolves to one through a built-in table covering the
hundred steps 100 through 900, `Thin` up to `Heavy`, plus 450 as `Book`. A
weight outside that table fails the run and names the exact key that fixes
it.

```
Text style heading.lg: no Figma style name for weight 350 of Brand Sans.
Add `figma.fonts['Brand Sans'].weights[350]` to name it.
```

```ts
figma: {
  fonts: {
    'Brand Sans': { family: 'Brand Sans Trial', weights: { 350: 'Light' } },
  },
}
```

`weights` names the face a numeric weight loads, and it also overrides the
table where a family names its faces differently. `family` swaps the family
name everywhere it reaches Figma, in the `fontFamily` variables and the text
styles alike, for a font installed under a different name than the tokens
use.

## Further reading

- [Variables](/figma/variables): the import mechanics, and what a re-run
  changes.
- [Styles](/figma/styles): the plugin that writes the text and effect styles.
- [Configuration](/tokens/configuration): `mirror.config.ts`, targets and
  sources end to end.
- [CLI](/overview/cli#mirror-figma): the command, and the arguments the build
  commands share.
