# Facet: everything
Every page of the documentation in reading order. Examples are build inputs: `facet --doc FILE build @NAME.json`.
## Contents
- Facet: A Mac design app your AI can design in. Install it, connect your assistant in one click, and describe what you want.
- Install Facet: Download Facet, drag it to Applications, open it once. Two minutes, no account.
- Your first design: Open the sample, ask your AI to finish it, or build a first card yourself from the command line.
- Connect your AI: Let Claude, ChatGPT, Cursor or VS Code design in Facet. One click from the app, or a few lines by hand.
- The command line: Install the facet command, see what is connected, and use Facet from Terminal and scripts.
- The language: What a build call can contain, how names resolve, and what containers pass down.
- The loop: Build, look, read back with source, change with sync. The fewest calls and tokens per change.
- Kit: Colors, type presets and parts said once, kept with the file and named everywhere after.
- Layout: Rows, columns, grids, stacks and rings; sizes; and placing a layer by words, anchors or angles.
- Text: Strings as texts, the font shorthand, presets, what containers pass down, and sentences with components in them.
- Color: Kit colors, variables, tints, gradients and modes.
- Repeat: One node per item of a list, or a count, with the item's fields substituted.
- Drawing: Circles, lines, arcs, polygons and paths as data, with fills, gradient strokes and dashes.
- Components: Component sets with variants and properties, layers that belong to some variants, and instances.
- Behavior: The states a component moves through, the scenarios that decide them, plays and looks, written beside its variants.
- Changing designs: Read a design back with source, change it in place with sync, edit values with modify, replace a set.
- Checks: What every write verifies, the warnings it reports, and expect for what must hold.
- For agents: How an agent builds the most with the fewest tokens and round trips, and how to recover from mistakes.
- Agent card: The language and the loop on one page, for agents: enough for most builds in a few thousand tokens.
- Updates and uninstalling: Facet updates itself; uninstall removes the app, its settings and its connections, never your designs.
- Troubleshooting: What to do when Facet will not open, an assistant cannot find it, or a command is missing.
- Gallery: Complete designs built with the language, each with its source and token cost.
- Keys: Every key a node can carry, with its forms and what it does.
- Commands: Every command with its arguments, flags and an example.
- Result codes: Every warning and error code a reply can carry, and what to do.
- Font: The grammar of the font shorthand.
- Chart: The compact behavior format, the full chart format it compiles to, and the commands that read them.
---
# Design with your AI, in a real design app
Facet is a design app for the Mac that your AI assistant can work in. Ask Claude, ChatGPT or Cursor for a screen, a component or a change, and watch it appear on the canvas: real layers with auto layout, text styles, color variables and components, that you can keep editing by hand. Every change is one undo step, with the assistant's name on it.
## What your assistant writes
Behind every request, your assistant writes a short description in Facet's build language, and Facet turns it into layers, measures them, checks them and says what did not work. You can write it too.
A whole phone screen in one call: a header, a dial drawn from shapes, a sentence with chips in it and tilted stickers. Everything named here (`phone`, `swara`, `deva`, `@saffron`) comes from a kit sent once per file.
`raag-today.json` (after raag-kit.json, listed at the end):
```json
{
"phone": {
"name": "Today",
"children": [
{
"row": [
{
"draw": [
{"path": "M20 2v14", "stroke": "@ink", "weight": 2},
{
"circle": [20, 30, 14],
"fill": {"linear": ["@saffron", "@rose"], "angle": 160}
}
],
"size": [40, 46],
"name": "Mark"
},
{
"column": [
{"title": "Raag"},
{"deva": "आज का रियाज़", "color": "@mute"}
]
},
{"space": "fill"},
{"icon": "lucide:settings-2", "size": 22}
],
"gap": 12,
"align": "center",
"width": "fill"
},
{
"stack": [
{
"draw": [
{
"circle": [150, 150, 144],
"fill": {"radial": ["#FFFFFF00", "@rose/10%"]},
"stroke": "none"
},
{
"circle": [150, 150, 120],
"stroke": "#1A142333",
"dash": [2, 6]
},
{
"arc": [150, 150, 120, 270, 315],
"stroke": {"linear": ["@saffron", "@rose"], "angle": 45},
"weight": 14
}
],
"size": 300
},
{
"ring": [
{
"each": ["रात", "सुबह", "दोपहर", "शाम"],
"do": {"deva": "{.}", "color": "@mute"}
}
],
"r": 86
},
{
"column": [
{"deva-xl": "यमन"},
{"small": "Yaman's hour", "color": "@mute"}
],
"align": "center"
}
],
"width": "fill"
},
{
"type": "paragraph",
"width": "fill",
"runs": [
"It's evening, so Yaman. Every note is shuddha except ",
{"swara": {"note": "तीव्र म", "tone": "@rose"}},
": sing it a touch sharp, and land on ",
{"swara": "नि"},
" and ",
{"swara": "ग"},
" more than on ",
{"swara": {"note": "सा", "tone": "@mute"}},
"."
]
},
{
"row": [
{"sticker": {"label": "12 दिन लगातार", "turn": -4}},
{
"sticker": {
"label": "Tanpura in C♯",
"tone": "#E3E7FA",
"turn": 3
}
},
{"sticker": {"label": "40 min", "tone": "#F9D7DD", "turn": -2}}
],
"gap": 10,
"align": "center"
}
]
}
}
```
## Why it is short
Every key costs a model tokens and every call costs a round trip, so the language removes structure instead of abbreviating words.
≈ 788tokens for the whole screen above, its kit sent once before
9×fewer tokens to read a design back with source than tree --full
1 callfor a component set with its properties, states and behavior
- **Say it once.** Colors, type and reusable parts live in the file's [kit](kit.md) and are named everywhere after.
- **Intent, not arithmetic.** Rows, columns and stacks place things; `at` names a place, an anchor or an angle. See [Layout](layout.md).
- **Read what you write.** [`source`](update.md) prints any layer in the language, ready to edit and send back with `--sync`.
- **Checked, not hoped for.** Every write verifies what it asked for and reports what it could not do, by layer, with the fix. See [Checks](checks.md).
## Where to go next
---
# Install Facet
Facet is a Mac app. It needs no account and no sign-up, and it keeps your designs as files on your Mac.
**You need** a Mac with Apple silicon (M1 or later) and macOS 13 Ventura or later.
## Download and open
1. Download **Facet.dmg** from [faux.design](https://faux.design).
2. Open the downloaded file. A window shows the Facet icon and an Applications folder.
3. Drag **Facet** onto **Applications**.
4. Open **Facet** from your Applications folder, Launchpad or Spotlight (⌘ Space, type "Facet").
5. macOS asks once whether you want to open an app downloaded from the internet. Click **Open**.
Facet is signed and checked by Apple (notarized), so that is the only question macOS asks.
> **Tip** If you open Facet straight from the downloaded window instead of Applications, Facet offers to move itself there. Say yes: Facet can only update itself, and AI apps can only find it, from Applications.
## What you see first
A welcome screen with three choices:
- **Start with a sample** opens a small, half-finished design you can explore, or ask your AI to finish.
- **New design** starts an empty file.
- **Open…** opens a design you already have.
Beside them, **Let your AI design with you** connects Claude, ChatGPT, Cursor or another assistant in one click. See [Connect your AI](connect.md). You can skip it and come back any time from **Facet › Connect AI Apps…**. **Help › Show Welcome** brings this screen back.
## With Homebrew
If you already use Homebrew:
```sh
brew install --cask uxfreak/tap/facet
```
This installs the app and the `facet` command together. Updates still arrive inside the app.
## Where your designs live
Designs are ordinary files ending in `.facet`. New designs go to **Documents › Facet** unless you choose another folder. Double-click a `.facet` file in Finder to open it, or drop it on the Facet icon. Facet keeps its settings, recent files and design systems in a hidden folder in your home folder, `.facet`.
## Next
- [Your first design](start.md)
- [Connect your AI](connect.md)
---
# Your first design
The quickest start is the sample: a small screen left half-finished on purpose, so there is something to explore and something to ask for.
## With your AI
1. On the welcome screen, choose **Start with a sample**. Facet saves it as **Documents › Facet › Welcome.facet** and opens it.
2. If you have not yet, connect your assistant: **Facet › Connect AI Apps…** ([Connect your AI](connect.md)).
3. In Claude, ChatGPT or Cursor, ask for something on the open design, for example:
> Use Facet to add a pricing card to my open Welcome design.
Your assistant edits the file you have open. Its changes appear on the canvas as it works, with its name beside them, and every change it makes is one step you can undo with ⌘Z.
> **Tip** Ask for what you want to see, not how to build it: "a settings screen for a meditation app, calm and dark" works better than a list of boxes. Ask for a change the same way: "make the header quieter", "add a second plan".
## By hand
Everything an assistant does, you can do on the canvas: select, move, resize, edit text, change colors in the inspector on the right. **View › Command Palette** lists every action with its shortcut.
## From the command line
The `facet` command builds designs from a short JSON description, the same way assistants do. Install it once with **Facet › Install Command Line Tool…** ([The command line](command-line.md)).
A card is a `column` with two texts. The container's `font` reaches every text inside it, so the texts are plain strings.
`hello.json`:
```json
{
"name": "Hello",
"column": [
"Evening practice",
{"text": "Twelve days in a row", "color": "#6F6879"}
],
"font": "Inter 15/22",
"padding": 20,
"gap": 4,
"fill": "#FFFFFF",
"cornerRadius": 14,
"width": 260
}
```
```sh
facet --doc practice.facet build @hello.json --preview hello.png
```
Every command takes `--doc FILE`. If Facet has the file open, the command edits it there and you see the change live; otherwise a helper in the background opens the file, saves after every change and stops when you run `facet --doc practice.facet close`.
`--preview` writes a picture of what was built. The reply lists every layer by name, and any warning names the layer and the fix. Three commands read what is there: `tree` lists layers with their sizes, `inspect NODE` shows one layer, and `shot NODE -o card.png --scale 2` renders it.
```sh
facet open practice.facet
```
opens the file in the app as a tab.
## Next
- [Connect your AI](connect.md)
- [The loop](loop.md): build, look, read back, change in place.
- [The language](language.md): what a build can contain.
---
# Connect your AI
Facet gives your assistant two things: the **Facet tools** (to open designs and build, change, check and render them) and the **Facet skill** (how to use them well, in a few thousand words). Once connected, you ask in plain words and watch the design change.
## One click
1. In Facet, choose **Facet › Connect AI Apps…** (or **Let your AI design with you** on the welcome screen).
2. Facet lists the assistants it finds on your Mac. Click **Connect** beside each one you use.
3. Restart that app when Facet says so: quit it with ⌘Q and open it again.
4. Click **Try it** to open the app with a first request ready to send.
Facet changes only its own entry in each app's settings, keeps a backup of the file it changed beside it, and remembers what it added: **Disconnect** removes exactly that. If you move Facet or an app stops finding it, click **Repair**.
| App | What Connect does |
|---|---|
| Claude (desktop app) | adds the Facet tools to Claude's settings; restart Claude |
| Claude Code | runs `claude mcp add` for you and installs the skill |
| ChatGPT and Codex | adds the Facet tools to Codex's settings and installs the skill; restart the app |
| Cursor | opens Cursor's own install prompt; click **Install** there |
| VS Code | opens VS Code's own install prompt; click **Install** there |
| Another app | **Copy setup** copies the settings to paste into it |
## Check it works
Open a design in Facet, then ask your assistant:
> Use Facet to list my open designs.
It should name the file. If it says it has no Facet tools, see [Troubleshooting](troubleshoot.md).
## By hand
The Facet tools are a local MCP server: the program inside Facet, started with the argument `mcp`. Every app below uses the same two values:
- **command**: `/Applications/Facet.app/Contents/Helpers/facet`
- **arguments**: `mcp`
The tools act on the design that is in front in Facet (they open Facet if it is not running), and can list, open and create designs.
### Claude (desktop app)
Open **Claude › Settings › Developer › Edit Config**, which opens `claude_desktop_config.json`, and add Facet under `mcpServers`:
```json
{"mcpServers": {"facet": {"command": "/Applications/Facet.app/Contents/Helpers/facet", "args": ["mcp"]}}}
```
Keep any servers already there. Quit Claude (⌘Q) and open it again.
To give Claude's chat the Facet skill as well, download **facet-skill.zip** from the release. In Claude, make sure code execution is on (**Settings › Capabilities**), then open **Customize › Skills**, click **+** and choose **Upload a skill**.
### Claude Code
```sh
claude mcp add --scope user facet -- /Applications/Facet.app/Contents/Helpers/facet mcp
```
For the skill, link Facet's copy into Claude's skills folder (Facet's own **Connect** does this):
```sh
mkdir -p ~/.claude/skills && ln -sfn /Applications/Facet.app/Contents/Resources/skill ~/.claude/skills/facet
```
### ChatGPT and Codex
Add to `~/.codex/config.toml`:
```toml
[mcp_servers.facet]
command = "/Applications/Facet.app/Contents/Helpers/facet"
args = ["mcp"]
```
and the skill:
```sh
mkdir -p ~/.agents/skills && ln -sfn /Applications/Facet.app/Contents/Resources/skill ~/.agents/skills/facet
```
Restart the ChatGPT app or Codex.
### Cursor
In Cursor's settings, under MCP, add a new global server with the same command and arguments, or add to `~/.cursor/mcp.json`:
```json
{"mcpServers": {"facet": {"command": "/Applications/Facet.app/Contents/Helpers/facet", "args": ["mcp"]}}}
```
Cursor reads the skill from `~/.claude/skills` or `~/.agents/skills`.
### VS Code
Run **MCP: Add Server** from the command palette, choose **Command (stdio)**, and enter the command and arguments above. VS Code reads the skill from `~/.claude/skills` or `~/.agents/skills`.
## What your assistant can do
Everything on the [For agents](agents.md) page and in the [agent card](card.md): build whole screens and component sets from a short description, change designs in place, check layout and text, render pictures, export assets and design tokens. Every change is one undo step in Facet, shown with the assistant's name.
---
# The command line
Facet includes a command, `facet`, that does everything an assistant does: build, change, check, render and export designs, with or without the app open. You only need it to use Facet from Terminal or scripts; assistants connected through [Connect your AI](connect.md) do not.
## Install it
Choose **Facet › Install Command Line Tool…**. Facet adds `facet` to the folder Terminal looks in:
- If you use Homebrew, Facet links it into Homebrew's folder, with no password.
- Otherwise Facet asks for your Mac password once, to create `/usr/local/bin/facet`, the standard place for commands every app can use.
Open a new Terminal window and check:
```sh
facet --help
```
The command lives inside the app (`/Applications/Facet.app/Contents/Helpers/facet`), so it updates with Facet and is always the same version as the app. If you installed Facet with Homebrew, `facet` is already installed.
## See what is set up
```sh
facet setup
```
lists each assistant Facet found and whether it is connected, whether the skill is installed, and where `facet` is. The same command connects and disconnects them:
```sh
facet setup connect claude-code # or claude-desktop, codex, cursor, vscode, all
facet setup disconnect all
facet setup repair # after moving Facet
facet setup cli # the same as Install Command Line Tool…
```
Add `--json` to any of them for output a script can read.
## Use it
Start with [Your first design](start.md) and [The loop](loop.md). Every command is on the [Commands](commands.md) page; `facet guide` prints the same reference, and `facet COMMAND --help` explains one.
---
# The language
A build takes one of three shapes:
| You send | Meaning |
|---|---|
| a node | build it |
| a list of nodes | build each, as separate top-level layers |
| `{"kit": {...}, "nodes": [...]}` | keep the kit with the file, then build the nodes (either part may be left out) |
## Nodes
A node is an object, or a string, which is a text. Containers are written with the container as the key:
```json
{"column": ["Title", {"row": ["Left", {"space": "fill"}, "Right"], "width": "fill"}], "padding": 16, "gap": 8}
```
| Container | What it does |
|---|---|
| `row`, `column` | auto layout along one axis |
| `grid` | rows of `columns` |
| `stack` | children overlap; each is placed with [`at`](layout.md) and centered by default |
| `ring` | children spread around a circle of radius `r` |
Any other node uses `type` (`text`, `rectangle`, `ellipse`, `vector`, `frame`), or one of the shorthands: `text`, `icon`, `draw`, `space`, `set`.
## Names
A key that is not part of the language is a name, looked up in this order:
1. a **type preset** of the kit: `{"title": "Today"}` is a text in the preset `title`;
2. a **part** of the kit: `{"chip": "May 2027"}` builds the part with that argument;
3. a **component** in the file: `{"Button": {"Kind": "Primary", "Label": "Pay"}}` is an instance.
A node uses at most one name. A name that matches nothing is reported with the nearest key, preset, part or component.
Keys of the language come first, so a preset or part cannot be called `name`, `text`, `row`, `fill` or any other key on the [Keys](keys.md) page; the kit refuses such a name. A name is either a preset or a part, never both.
A component set built earlier in the same call is already a name: one call can hold a set and the screen that uses it.
## Passing things down
A container's `font`, `style` and `color` reach every text inside it that does not set its own. That is why most texts can be plain strings.
A build into an existing layer (`--parent ID`) starts from what the texts already inside that layer share: their most common font and color. A row added to a card in Georgia comes out in Georgia without saying so.
## Names of layers
Name every layer you will address later (`"name": "Price"`). A text nobody named is named after its content (its first 40 characters), and keeps following it when the text changes; instances nobody named are named after their variant (`State=Locked`). The reply to every build lists the ids of the layers by name; a component set's layers list one id per variant, joined by `|`.
## Long forms
Every short form has a long form, and both work in one call: `{"row": [...]}` is `{"type": "row", "children": [...]}`, and `@ink` is `"@Kit/ink"`. The long forms reach a few things the short ones do not name, such as `$nodes` overrides. The [Keys](keys.md) page lists every key.
> **Why** One builder and one set of checks read every form, so nothing behaves differently depending on how it was written.
---
# The loop
Most work is four steps, and each is one call.
1. **Build** what you mean: `facet --doc f.facet build @screen.json --preview s.png`.
2. **Look** at the preview and read every warning. Warnings name the layer and the fix.
3. **Read back** what is there, in the language: `facet --doc f.facet source Screen`.
4. **Change** it in place: edit the JSON, then `build @screen.json --sync`. Layers keep their ids, instances stay linked, and the change is one undo step.
```sh
$ facet source Hello
{"font":"Inter 15/22","column":["Evening practice",{"text":"Twelve days in a row","color":"#6F6879"}],"gap":4,"padding":20,"fill":"#FFFFFF","name":"Hello","width":260,"at":[0,0],"cornerRadius":14}
```
`source` prints the shortest form that builds the layer again. Containers come back as `{"row": [...]}`, plain texts as strings, kit colors and text styles by name. It costs about a ninth of the tokens `tree --full` does, so reading before a change is cheap.
When a layer looks wrong, `tree NODE` shows each layer's size and place, `inspect NODE` every property of one, and `shot NODE -o x.png --scale 2` a close look.
> **Tip** Send the kit once per file. Later builds name its colors, presets and parts without sending them again; `facet kit` prints what the file holds.
## Small changes
For one or two values, `modify` is shorter than a sync:
```sh
facet --doc f.facet modify Hello '{"fill": "@paper", "font": "Inter 16/24"}'
facet --doc f.facet modify Hello '{"$nodes": {"Evening practice": {"text": "Morning practice"}}}'
```
## Undo
Each call is one undo step, whatever it changed: `facet --doc f.facet undo`.
---
# Kit
The kit holds what a file's designs share. Send it with any build, or alone; it merges into the file's kit by name and is saved with the file.
The kit behind the Raag examples: a default font, six colors, six type presets and four parts.
`raag-kit.json`:
```json
{
"kit": {
"font": "Inter 15/22",
"colors": {
"ink": "#1A1423",
"paper": "#F6F1EA",
"saffron": "#F29F05",
"rose": "#D9485F",
"mute": "#6F6879",
"board": "#ECE6DD"
},
"type": {
"display": "Fraunces 40/44 600 -1",
"title": "Fraunces 22/26 600",
"deva": "Tiro Devanagari Hindi 14/20",
"deva-xl": "Tiro Devanagari Hindi 40/44",
"eyebrow": "Inter 12/16 700 +1.6 caps",
"small": "Inter 13/18 500"
},
"parts": {
"swara": {
"params": {"note": "", "tone": "@saffron"},
"node": {
"row": [{"text": "{note}", "style": "deva"}],
"padding": [2, 8],
"cornerRadius": 8,
"fill": "{tone}/20%"
}
},
"sticker": {
"params": {"label": "", "tone": "#FBE3B5", "turn": 0},
"node": {
"row": [{"text": "{label}", "font": "Inter 13/18 700"}],
"padding": [6, 12],
"cornerRadius": 14,
"fill": "{tone}",
"rotation": "{turn}"
}
},
"status": {
"params": {"ink": "@ink"},
"node": {
"row": [
{"text": "9:41", "font": "Inter 15/20 600", "color": "{ink}"},
{"space": "fill"},
{"icon": "lucide:signal", "size": 16, "color": "{ink}"},
{"icon": "lucide:battery-full", "size": 16, "color": "{ink}"}
],
"gap": 6,
"align": "center",
"width": "fill",
"padding": [16, 26, 6, 26]
}
},
"phone": {
"params": {"name": "Phone", "bg": "@paper", "children": []},
"node": {
"name": "{name}",
"column": [
{"status": {}},
{
"column": "{children}",
"padding": [10, 22, 28, 22],
"gap": 18,
"width": "fill",
"height": "fill"
}
],
"width": 390,
"height": 844,
"cornerRadius": 44,
"fill": "{bg}"
}
}
}
}
}
```
| Section | Holds | Used as |
|---|---|---|
| `font` | the default font of every text | nothing to write: texts start from it |
| `colors` | named colors, one value or one per mode | `@ink`, `@ink/40%` |
| `modes` | mode names for the colors (`["Light", "Dark"]`) | `"mode": "Dark"` on a layer |
| `type` | named text presets (a [font](font.md) or text keys) | `{"title": "Today"}`, `"style": "title"` |
| `parts` | nodes with parameters | `{"chip": "May 2027"}` |
## Colors become variables
Kit colors are variables in the collection `Kit`, so a layer painted `@ink` stays bound: change `ink` in the kit and every layer follows, and a layer shown in another mode takes that mode's value. A tint such as `@rose/20%` is a variable of its own (`Kit/rose-20`, made the first time it is used), so tints follow modes too. See [Color](color.md).
## Presets become text styles
Each preset is a text style of the same name. `{"title": "Today"}` makes a text that follows the style; send the preset again with new values and every text using it changes at once, with no rebuild.
## Parts
A part is a node with parameters. Inside it, `{name}` stands for a parameter: a string that is only `{name}` takes the argument as it is (a number, a color, a list), and `{name}` inside longer text is replaced by its text.
```json
{"kit": {"parts": {"chip": {"params": {"label": "", "tone": "@saffron"},
"node": {"row": [{"text": "{label}"}], "padding": [2, 8], "cornerRadius": 8, "fill": "{tone}/20%"}}}}}
```
- `{"chip": "May 2027"}` passes one value to the first parameter.
- `{"chip": {"label": "May 2027", "tone": "@rose"}}` names them.
- Keys beside the name apply to the part's root: `{"chip": "Late", "rotation": -4}`.
- A parameter called `children` takes the children written at the use: put `"{children}"` where they go.
- A list parameter whose default is `[]`, written alone among children (`"{status}"`), adds nothing when the use leaves it out and its items when it is given.
- An argument may be a node or a list of nodes, not only text: `{"card": {"title": "Today", "children": [{"chip": "Late"}]}}`.
- The part's root is named after the part unless its node names it: `"name": "{title}"` names each use by its argument.
- Parts use presets, other parts and components. A part that uses itself is stopped at 24 levels.
A part is not a component: it expands where it is used and leaves no link. When the same thing must stay in sync across a file, make it a [component](components.md).
## Seeing and changing the kit
```sh
$ facet kit
{"font":"Inter 15/22","colors":{"ink":"#1A1423","paper":"#F6F1EA","saffron":"#F29F05","rose":"#D9485F","mute":"#6F6879","board":"#ECE6DD"},"type":{"display":{"fontFamily":"Fraunces","fontSize":40.0,"lineHeight":44.0,"fontWeight":600,"letterSpacing":-1.0},"title":{"fontFamily":"Fraunces","fontSize":22.0,"lineHeight":26.0,"fontWeight":600},"deva":{"fontFamily":"Tiro Devanagari Hindi","fontSize":14.0,"lineHeight":20.0},"deva-xl":{"fontFamily":"Tiro Devanagari Hindi","fontSize":40.0,"lineHeight":44.0},"eyebrow":{"fontFamily":"Inter","fontSize":12.0,"lineHeight":16.0,"fontWeight":700,"letterSpacing":1.6,"textCase":"upper"},"small":{"fontFamily":"Inter","fontSize":13.0,"lineHeight":18.0,"fontWeight":500}},"parts":{"swara":{"params":{"note":"","tone":"@saffron"},"node":{"row":[{"text":"{note}","style":"deva"}],"padding":[2,8],"cornerRadius":8,"fill":"{tone}/20%"}},"sticker":{"params":{"label":"","tone":"#FBE3B5","turn":0},"node":{"row":[{"text":"{label}","font":"Inter 13/18 700"}],"padding":[6,12],"cornerRadius":14,"fill":"{tone}","rotation":"{turn}"}},"status":{"params":{"ink":"@ink"},"node":{"row":[{"text":"9:41","font":"Inter 15/20 600","color":"{ink}"},{"space":"fill"},{"icon":"lucide:signal","size":16,"color":"{ink}"},{"icon":"lucide:battery-full","size":16,"color":"{ink}"}],"gap":6,"align":"center","width":"fill","padding":[16,26,6,26]}},"phone":{"params":{"name":"Phone","bg":"@paper","children":[]},"node":{"name":"{name}","column":[{"status":{}},{"column":"{children}","padding":[10,22,28,22],"gap":18,"width":"fill","height":"fill"}],"width":390,"height":844,"cornerRadius":44,"fill":"{bg}"}}}}
```
Send a kit section again to change it; names you send replace those entries and the rest stay. `null` removes an entry: `{"kit": {"parts": {"badge": null}}}`. A removed color keeps its variable, because layers may still use it; delete the variable with `facet call variables '{"delete": "Kit/rose"}'` once nothing does.
A name lives in one section: a preset and a part cannot share it, because `{"badge": ...}` could mean either. The kit refuses the second one and says which to rename.
---
# Layout
Containers do the placing. You say how things flow and how they size; Facet does the arithmetic.
Three rows in a column. The first pushes `40 min` right with a `space` that fills; the second wraps its chips, made with `each`; the third ends at the right with `justify`.
`layout-flow.json`:
```json
{
"name": "Session",
"font": "Inter 15/22",
"width": 340,
"padding": 20,
"gap": 16,
"fill": "#FFFFFF",
"cornerRadius": 16,
"column": [
{
"row": [
{"text": "Riyaz session", "font": "17/22 700"},
{"space": "fill"},
{"text": "40 min", "color": "#6F6879"}
],
"width": "fill",
"align": "center"
},
{
"row": [
{
"each": ["Sa", "Re", "Ga", "Ma", "Pa", "Dha", "Ni"],
"do": {
"row": [{"text": "{.}", "font": "13/16 600"}],
"padding": [6, 10],
"cornerRadius": 8,
"fill": "#F3EFF7"
}
}
],
"gap": 6,
"wrap": true,
"width": "fill"
},
{
"row": [
{"text": "Skip"},
{
"row": [{"text": "Start", "color": "#FFFFFF", "font": "15/20 700"}],
"padding": [10, 20],
"cornerRadius": 12,
"fill": "#1A1423"
}
],
"gap": 16,
"justify": "end",
"align": "center",
"width": "fill"
}
]
}
```
## Flow keys
| Key | Forms | What it does |
|---|---|---|
| `gap` | number, `{"row", "column"}`, `"auto"` | space between children; `"auto"` spreads them |
| `padding` | number, `[v, h]`, `[top, right, bottom, left]` | room inside the edge |
| `align` | `start`, `center`, `end`, `stretch` | across the flow |
| `justify` | `start`, `center`, `end`, `space-between` | along the flow (needs room) |
| `wrap` | `true` | a row breaks into lines |
| `columns` | number or tracks | a `grid`'s columns |
## Sizes
`width` and `height` take a number, `"fill"` (the room the parent's flow leaves) or `"hug"` (the content; the default for containers). `size` is both at once: a number, or `[width, height]`.
A text with `"width": "fill"` wraps at the width its row or column gives it and takes the height of its lines; when the parent resizes, the text measures its lines again. `"maxLines": 1` keeps it to one line with an ellipsis.
## Space
`{"space": 12}` is a fixed gap in a row or column; `{"space": "fill"}` takes the free room, so whatever follows is pushed to the end.
## Stacks
In a `stack` the children overlap. Each is centered unless it says otherwise with `at`, and the stack takes the size of its largest child.
An avatar: a gradient circle, initials centered on it, and a badge pinned to the top right corner.
`layout-stack.json`:
```json
{
"name": "Avatar",
"stack": [
{
"type": "ellipse",
"size": 72,
"fill": {"linear": ["#F29F05", "#D9485F"], "angle": 135}
},
{"text": "AK", "font": "Inter 24/28 700", "color": "#FFFFFF"},
{
"row": [{"text": "3", "font": "Inter 11/14 700", "color": "#FFFFFF"}],
"padding": [2, 6],
"cornerRadius": 10,
"fill": "#1A1423",
"at": ["top right", -2, 2]
}
]
}
```
## Placing a layer: `at`
| Form | Places the layer |
|---|---|
| `[x, y]` | with its top left corner at x, y |
| `[x, y, "center"]` | with that point of itself (any place word) at x, y |
| `"bottom right"` | in its parent by words: `top`, `bottom`, `left`, `right`, `center`, or two of them |
| `["bottom right", -16, -16]` | by words, then moved by an offset (x to the right, y down, so `-16` lifts it) |
| `{"angle": 90, "r": 80}` | its center at that angle (degrees clockwise from the top) and distance from the parent's center |
In a row or column, `at` takes the layer out of the flow. Places pin the layer with constraints, so it keeps its place when the parent changes size.
## Rings
A `ring` places its children around a circle of radius `r`, the first at `start` degrees (clockwise from the top), the rest evenly after it. A child may set its own `angle`.
A clock face: twelve numbers from `each` on a ring starting at 30°, over a circle and a center dot, all in one stack.
`layout-ring.json`:
```json
{
"name": "Clock",
"stack": [
{
"type": "ellipse",
"size": 220,
"fill": "#F6F1EA",
"stroke": "#1A142322"
},
{
"ring": [
{
"each": 12,
"do": {
"text": "{#}",
"font": "Inter 15/20 600",
"color": "#1A1423"
}
}
],
"r": 88,
"start": 30
},
{"type": "ellipse", "size": 8, "fill": "#D9485F"}
]
}
```
## Rotation
`rotation` turns a layer. In a row or column, a rotated layer keeps its own size and the flow makes room for its turned box.
---
# Text
A string in a list of children is a text. It takes its font and color from the nearest container that sets them, or from the kit's `font`.
One container sets `Inter 15/22`; each text changes only what differs. `17/26` keeps the family and changes the size; `italic` and a weight add to it.
`text-fonts.json`:
```json
{
"name": "Type",
"font": "Inter 15/22",
"gap": 8,
"width": 360,
"padding": 20,
"fill": "#FFFFFF",
"column": [
{"text": "Practise the raga", "font": "Fraunces 36/40 600 -1"},
{
"text": "Evening · first prahar",
"font": "Inter 12/16 700 +1.6 caps",
"color": "#D9485F"
},
{"text": "Every note is shuddha except tivra Ma.", "font": "17/26"},
{"text": "यमन · कल्याण थाट", "font": "Tiro Devanagari Hindi 20/28"},
{
"text": "Last riyaz 2 days ago",
"font": "13/18 500 italic",
"color": "#6F6879"
}
]
}
```
## The font shorthand
`"Family Size/LineHeight Weight"` and optional words, in any order after the size: `italic`, letter spacing with a sign (`+0.4`, `-2%`) and a case (`caps`, `lower`, `title`). Leave the family out to keep the inherited one. The full grammar is in [Font](font.md).
| You write | Means |
|---|---|
| `"Inter 15/22"` | Inter, 15 px, 22 px line height |
| `"Fraunces 44/1.1 600 -1"` | line height 1.1 × size, weight 600, letter spacing −1 px |
| `"12/16 700 +1.6 caps"` | inherited family, uppercase, spaced |
## Presets
A kit `type` preset is a text style. `{"title": "Today"}` is a text in it; `"style": "title"` on a container makes it the default for the texts inside. A text's own `font` replaces a preset.
## What wins
From strongest to weakest: keys written on the text (`fontSize`, `color`), the text's own `font`, its own `style`, then what the nearest container passes down (`font` or `style`, and `color`), then the kit's `font`.
## Sentences with components
`"type": "paragraph"` with `runs` mixes text and nodes. It wraps like text: each word is a text in the paragraph's font, nodes stay in place, and punctuation that touches a node stays on its line.
A chip part used twice inside a sentence. `wordSpacing` and `lineGap` tune the spacing when the defaults (0.27 × size and 0.3 × size) do not suit the font.
`text-paragraph.json`:
```json
{
"kit": {
"parts": {
"tag": {
"params": {"label": "", "tone": "#D9485F"},
"node": {
"row": [{"text": "{label}", "font": "Inter 13/18 600"}],
"padding": [1, 7],
"cornerRadius": 6,
"fill": "{tone}/18%"
}
}
}
},
"nodes": [
{
"name": "Digest",
"font": "Inter 16/24",
"width": 340,
"padding": 20,
"fill": "#FFFFFF",
"column": [
{
"type": "paragraph",
"width": "fill",
"runs": [
"Checkout errors rose ",
{"tag": "+38%"},
" on Saturday, the most since ",
{"tag": {"label": "Tue 14 Oct", "tone": "#6F6879"}},
". Two agents stopped sending traces overnight."
]
}
]
}
]
}
```
## Scripts and symbols
Devanagari and other scripts that reorder letters shape as written. Some symbols draw as color emoji by default (zodiac signs, ☕, ⚡, ⭐); put U+FE0E after one to get the font's glyph. A build notes each such symbol.
---
# Color
Anywhere a color goes (`fill`, `stroke`, `color`, a gradient stop), you can write:
| Form | Is |
|---|---|
| `#1A1423`, `#1A142380` | a color, optionally with alpha |
| `@ink` | the kit color `ink`, bound to its variable |
| `@Theme/surface` | a variable in another collection |
| `@Theme/surface\|#FFFFFF` | a variable, painting the fallback until it exists |
| `@ink/40%`, `#1A1423/40%` | the color at 40% of its opacity |
| `{"linear": [stops], "angle": 90}` | a linear gradient; angle 0 runs left to right, 90 top to bottom |
| `{"radial": [stops]}` | a radial gradient from the center |
Stops are colors spread evenly, or `[color, position]` pairs with positions from 0 to 1.
A linear gradient between two kit colors, a radial one with a stop placed at 0.6, a tint, and a stroke.
`color-paints.json`:
```json
{
"kit": {
"colors": {"saffron": "#F29F05", "rose": "#D9485F", "dusk": "#2B1E4A"}
},
"nodes": [
{
"name": "Paints",
"gap": 16,
"row": [
{
"type": "rectangle",
"size": 96,
"cornerRadius": 20,
"fill": {"linear": ["@saffron", "@rose"], "angle": 45}
},
{
"type": "ellipse",
"size": 96,
"fill": {"radial": ["@saffron", ["@rose", 0.6], "@dusk"]}
},
{
"type": "rectangle",
"size": 96,
"cornerRadius": 20,
"fill": "@dusk/20%"
},
{
"type": "rectangle",
"size": 96,
"cornerRadius": 20,
"fill": "@rose",
"stroke": "@dusk",
"strokeWidth": 3
}
]
}
]
}
```
## Modes
Give the kit `modes` and a value per mode for each color. `"mode": "Dark"` on any layer shows it, and everything inside it, in that mode.
The same card twice; the second says `"mode": "Dark"`. The tint `@accent/15%` follows the mode because it is a variable of its own.
`color-modes.json`:
```json
{
"kit": {
"modes": ["Light", "Dark"],
"colors": {
"bg": ["#FFFFFF", "#15121C"],
"fg": ["#1A1423", "#F6F1EA"],
"accent": ["#D9485F", "#FF7A8F"]
}
},
"nodes": [
{
"name": "Light",
"at": [0, 0],
"color": "@fg",
"fill": "@bg",
"padding": 20,
"gap": 10,
"cornerRadius": 16,
"column": [
{"text": "Evening practice", "font": "Inter 18/24 700"},
{
"row": [{"text": "Yaman", "color": "@accent"}],
"padding": [4, 10],
"cornerRadius": 10,
"fill": "@accent/15%"
}
]
},
{
"name": "Dark",
"mode": "Dark",
"at": [220, 0],
"color": "@fg",
"fill": "@bg",
"padding": 20,
"gap": 10,
"cornerRadius": 16,
"column": [
{"text": "Evening practice", "font": "Inter 18/24 700"},
{
"row": [{"text": "Yaman", "color": "@accent"}],
"padding": [4, 10],
"cornerRadius": 10,
"fill": "@accent/15%"
}
]
}
]
}
```
For a variable collection that is not the kit's, write `"mode": {"Theme": "Dark"}`.
## Exporting
`facet --doc f.facet tokens export --format css` writes the variables as CSS custom properties, one block per mode; `--format dtcg` writes Design Tokens Community Group JSON.
---
# Repeat
`{"each": data, "do": node}` stands for one node per item, in place, wherever a child can go.
Seven bars from seven objects. `"height": "{m}"` takes the number itself, so the bar heights are the data.
`repeat-bars.json`:
```json
{
"name": "Week",
"gap": 12,
"padding": 20,
"fill": "#FFFFFF",
"cornerRadius": 16,
"column": [
{
"text": "Minutes practised",
"font": "Inter 13/18 600",
"color": "#6F6879"
},
{
"row": [
{
"each": [
{"d": "M", "m": 40},
{"d": "T", "m": 25},
{"d": "W", "m": 55},
{"d": "T", "m": 30},
{"d": "F", "m": 60},
{"d": "S", "m": 15},
{"d": "S", "m": 45}
],
"do": {
"column": [
{
"type": "rectangle",
"width": 24,
"height": "{m}",
"cornerRadius": 6,
"fill": "#F29F05"
},
{"text": "{d}", "font": "Inter 12/16 600", "color": "#6F6879"}
],
"gap": 6,
"align": "center"
}
}
],
"gap": 10,
"align": "end"
}
]
}
```
## What can be substituted
| Write | Becomes |
|---|---|
| `{.}` | the item itself |
| `{field}` | a field of an object item |
| `{i}` | the index from 0 |
| `{#}` | the number from 1 |
| `{n}` | how many items there are |
`data` may be a list or a count: `{"each": 12, "do": {"text": "{#}"}}` makes 1 to 12. A string that is only `{name}` keeps the value's type (a number stays a number, a list is spread into the list around it); inside longer text the value is written as text.
## With parts
`each` and [parts](kit.md) combine: `{"each": ["Sa", "Re", "Ga"], "do": {"chip": "{.}"}}`.
---
# Drawing
`draw` lists shapes in a box of `size`, with coordinates from its top left corner, y down. Use it for what layout cannot express: dials, charts, marks. For symbols, use an [icon](#icons) first.
A gauge: a track arc, a value arc with a gradient stroke, a needle and its hub. Arcs run clockwise from the top, as on a clock face.
`drawing-gauge.json`:
```json
{
"name": "Gauge",
"size": [200, 170],
"draw": [
{"arc": [100, 100, 80, 225, 495], "stroke": "#ECE6DD", "weight": 14},
{
"arc": [100, 100, 80, 225, 405],
"stroke": {"linear": ["#F29F05", "#D9485F"], "angle": 0},
"weight": 14
},
{"line": [100, 100, 100, 40], "stroke": "#1A1423", "weight": 3},
{"circle": [100, 100, 6], "fill": "#1A1423"}
]
}
```
## Shapes
| Shape | Numbers |
|---|---|
| `circle` | `[cx, cy, r]` |
| `ellipse` | `[cx, cy, rx, ry]` |
| `rect` | `[x, y, width, height]`, with `radius` |
| `line` | `[x1, y1, x2, y2]` |
| `poly` | `[x1, y1, x2, y2, ...]`, with `"closed": true` for a polygon |
| `arc` | `[cx, cy, r, from, to]` in degrees clockwise from the top |
| `path` | SVG path data |
## Paint
Each shape takes `fill` (none by default) and `stroke` (the node's `color` by default when there is no fill), with `weight` (1.5), `dash` (`[dash, gap]`), `cap` (`round` by default; `none`, `square`) and `join`. Colors and gradients read as anywhere else; kit colors are written as their values, since a drawing's shapes do not bind to variables.
> **Note** A drawing becomes vector layers, one per shape. Read back with `source`, it comes back as paths.
## Icons
`{"icon": "lucide:clock", "size": 16, "color": "@mute"}` draws an icon from the Iconify collections (`lucide`, `mdi`, `tabler`, `ph` and the rest), fetched once and cached. The first use of a name makes a component `Icons/lucide:clock` in a frame named `Icons` beside your work, and the build notes it; every use is an instance of it, so `color` and `size` are per use and one edit to the component changes them all. Swap an icon with `modify Icon '{"icon": "lucide:moon"}'`, or from an instance with `{"Icon": "lucide:moon"}`.
---
# Components
A set is a component with variants. `set` names it; `variants` lists each variant property and its values; `template` is the node every variant starts from.
Three variants of a card. `{Name}`, `{Hour}` and `{Last}` bind texts to the set's properties; `only` keeps a layer to the variants that use it; the Locked variant sets its own `Last`.
`raga-card.json` (after raag-kit.json, listed at the end):
```json
{
"set": "Raga/Card",
"props": {
"Name": "राग भैरव",
"Hour": "MORNING",
"Last": "Last riyaz 2 days ago · 25 min"
},
"variants": {
"State": {
"Learning": {},
"New": {},
"Locked": {
"fill": "#F3EFF7",
"props": {"Last": "Opens after 21 days of Yaman"}
}
}
},
"template": {
"width": 300,
"padding": 18,
"gap": 8,
"fill": "#FFFFFF",
"cornerRadius": 20,
"column": [
{"eyebrow": "{Hour}", "color": "@mute"},
{"deva-xl": "{Name}", "font": "Tiro Devanagari Hindi 24/30"},
{
"row": [{"swara": "रे॒"}, {"swara": "ध॒"}],
"gap": 6,
"only": "Learning"
},
{"small": "{Last}", "color": "@mute", "only": ["Learning", "Locked"]},
{
"name": "Progress",
"only": "Learning",
"stack": [
{
"type": "rectangle",
"width": 264,
"height": 6,
"cornerRadius": 3,
"fill": "@ink/8%"
},
{
"type": "rectangle",
"width": 180,
"height": 6,
"cornerRadius": 3,
"fill": "@saffron",
"at": "left"
}
]
},
{
"row": [
{
"text": "Start learning",
"font": "Inter 14/20 700",
"color": "#FFFFFF"
}
],
"padding": [8, 14],
"cornerRadius": 16,
"fill": "@ink",
"only": "New"
}
]
},
"behavior": {
"axes": {
"history": ["fresh", "practised"],
"streak": {
"values": ["short", "long"],
"when": {"history": ["practised"]}
}
},
"start": "Loading",
"states": {
"Loading": {"LOADED": "route"},
"route": [
"history == 'fresh' -> New", "streak == 'long' -> Learning", "Locked"
],
"New": {"START": "Learning"},
"Learning": {"PAUSE": "Locked"},
"Locked": {"UNLOCK": "Learning"}
},
"plays": {"Begin a new raga": ["LOADED", "START"]},
"targets": {"START": "Start learning"}
}
}
```
## Variants
```json
"variants": {"State": ["Learning", "New", "Locked"]}
"variants": {"State": {"Learning": {}, "Locked": {"fill": "#F3EFF7"}}}
```
The first value of each property is the default. A value's map changes that variant: keys for the root, `$nodes` for named layers, and `props` for its own property values.
A text that names a variant property shows each variant's own value: `{"text": "{Tier} plan"}` reads "Free plan" in `Tier=Free` and "Pro plan" in `Tier=Pro`. No property needs declaring for it.
The set is drawn with a dashed purple outline around its variants in the app and in previews; each variant is a component of its own inside it.
## Properties
`props` declares the set's properties once, with their defaults. A text written `"{Name}"` follows `Name`; a layer with `"visible": "{Show retry}"` shows or hides with a boolean. Instances set them by name.
A variant that sets its own value (`"props": {"Last": "Opens after 21 days of Yaman"}`) keeps that text; the property's value applies in the other variants. When every value of a variant property sets its own, no instance can change it, and the build warns `INPUT_IGNORED`: write the text in each variant instead, or leave one variant to follow the property.
A part's argument can be a property: `{"chip": "{Hour}"}` inside a template binds the chip's text to `Hour`.
## Layers in some variants: `only`
`"only": "Learning"`, `"only": ["Learning", "Locked"]`, or `"only": {"State": ["Learning"]}` when a set has several properties. Other variants leave the layer out instead of hiding it, so a set with many views stays small. A layer only some variants hold can make those variants larger; the set lays its variants out side by side, so that is fine.
## Instances
Use a component by its name. Keys that name a variant property choose the variant; the rest set properties, or override a layer's text by name. `name` beside the component names the instance, which matters when a screen holds several: `{"Plan": {"Tier": "Pro"}, "name": "Pro card"}`.
Three instances; each chooses its State and sets the raga's name and hour.
`raga-library.json` (after raag-kit.json, raga-card.json, listed at the end):
```json
{
"name": "Library",
"gap": 20,
"align": "start",
"row": [
{"Raga/Card": {"State": "Learning"}},
{
"Raga/Card": {
"State": "New",
"Name": "राग भीमपलासी",
"Hour": "AFTERNOON"
}
},
{
"Raga/Card": {
"State": "Locked",
"Name": "राग मालकौंस",
"Hour": "LATE NIGHT"
}
}
]
}
```
A bare value sets a component's only text property: `{"Button": "Pay now"}`.
## Changing a set
Build it again with `--replace`: the new set takes the old one's place, its instances move to the variants with the same names, and they keep what they set. Or change it in place with [`--sync`](update.md).
---
# Behavior
A set can say how it behaves: which state shows first, which events move it on, and what decides the way in each scenario. One build stores the set, its statechart, the binding of states to variants and its design axes, as a feature named after the set in lower case with dashes (`Raga card` is `raga-card`). The build's reply names it; every chart command takes that name.
The [raga card](components.md) above carries this behavior:
```json
"behavior": {
"axes": {"history": ["fresh", "practised"],
"streak": {"values": ["short", "long"], "when": {"history": ["practised"]}}},
"start": "Loading",
"states": {
"Loading": {"LOADED": "route"},
"route": ["history == 'fresh' -> New", "streak == 'long' -> Learning", "Locked"],
"New": {"START": "Learning"},
"Learning": {"PAUSE": "Locked"},
"Locked": {"UNLOCK": "Learning"}
},
"plays": {"Begin a new raga": ["LOADED", "START"]},
"targets": {"START": "Start learning"}
}
```
## States
- A state named like a value of the set's variant property **shows that variant** (`New`, `Learning`, `Locked`).
- `{"EVENT": "Target"}` moves on when the event happens. A target may be `"guard -> Target"`, a list of those (the first whose guard holds wins), or `{"target", "guard", "assign"}`.
- A state written as a **list** moves on at once: `route` picks the next state by the scenario. It is never on screen, so it needs no variant.
- `note`, nested `states`, `initial` and `type` (`parallel`, `final`) are written as in the full chart format.
## Scenarios
`axes` name what varies between situations. Every combination is a scenario; guards read the axis values. An axis with `when` applies only where earlier axes take those values and keeps its first value elsewhere, so `streak` does not double the fresh scenarios.
```sh
$ facet chart raga-card
3 scenarios over history (fresh, practised) × streak (short, long): 3 to 4 configurations each
Views:
New reachable where history is fresh, streak is short
Learning reachable in every scenario
Locked reachable in every scenario
Coverage: 4/10 transitions over every scenario
untested in 3 scenario(s): Learning --PAUSE--> Locked
untested in 3 scenario(s): Locked --UNLOCK--> Learning
Play skeletons (shortest paths to what no play reaches):
Covers Learning --PAUSE--> Locked: LOADED → START → PAUSE
Covers Locked --UNLOCK--> Learning: LOADED → START → PAUSE → UNLOCK
Reaches Locked: LOADED → START → PAUSE
Covers Locked --UNLOCK--> Learning: LOADED → UNLOCK
Covers Learning --PAUSE--> Locked: LOADED → UNLOCK → PAUSE
Reaches Learning: LOADED → UNLOCK
Covers Learning --PAUSE--> Locked: LOADED → PAUSE
Covers Locked --UNLOCK--> Learning: LOADED → PAUSE → UNLOCK
Reaches Locked: LOADED → PAUSE
```
`facet chart` says where each view is reachable by axis values, what is not covered, and the shortest event sequences to reach what no play reaches (`--full` lists every scenario). Its words:
| Word | Means |
|---|---|
| scenario | one combination of axis values; an axis with `when` adds values only where it applies |
| configuration | the set of states active at once; a chart has a few per scenario |
| transition | one event moving one state to another, counted once per scenario where it can happen |
| coverage | the transitions the plays take, of all transitions |
| reachable in every scenario | some event sequence shows the view whatever the axis values |
| play skeleton | the shortest event sequence to a view or transition no play reaches; name it under `plays` to keep it |
## Plays and targets
`plays` name event sequences to replay on the canvas: `facet plays run raga-card "Begin a new raga"` runs one in every scenario and says where it ends. `targets` name the layer each event acts on, so replays click the right thing; they are optional, and events without one replay without a click.
## Looks
`looks` are design axes: `{"Look": {"Light": "Kit/Light", "Dark": "Kit/Dark"}}` shows each option in a mode of the kit's variables. Lattice views (`facet lattice`) cross them with the scenarios.
The full chart format, for what the compact one does not cover, is in [Chart](chart.md).
---
# Changing designs
Three ways to change what is there, from smallest to largest.
| Change | Use | Keeps |
|---|---|---|
| a few values | `modify NODE '{...}'` | everything else |
| a tree, by writing it again | `build @tree.json --sync` | ids, instances, comments of every layer that still matches |
| a component set | `build @set.json --replace` | its instances and what they set |
## Read it back first
`source` prints a layer as you would write it: the shortest form, with containers as `{"row": [...]}`, plain texts as strings, the font and color a container passes down lifted back to it, kit colors and styles by name, instances as `{"Component": {...}}`.
```sh
$ facet source Library
{"row":[{"Raga/Card":null},{"Raga/Card":{"State":"New","Hour":"AFTERNOON","Name":"राग भीमपलासी"}},{"Raga/Card":{"State":"Locked","Hour":"LATE NIGHT","Name":"राग मालकौंस"}}],"gap":20,"name":"Library","at":[1020,0]}
```
`--depth N` stops after N levels and counts the children it left out.
`source` prints what is there. Parts come back expanded, because a part leaves no link, and values equal to their defaults are left out. So for a screen you built from parts, keep editing your own JSON and send it with `--sync`; read with `source` what you did not write, or what someone changed by hand. A kit tint prints as you write it (`@rose/20%`).
## Sync
`build --sync` matches what you write against the existing tree and changes only what differs:
- a written node matches the existing child with the same `key` or name (unnamed nodes match the next unnamed child of the same kind; several children with the same name match in order, so give repeated parts a `key`);
- a match of the same kind is updated in place; a match of another kind, and anything new, is built;
- existing children that nothing matches are deleted;
- children end in the written order.
Keys you write are applied. A key you leave out keeps its value; write `"none"` to clear a fill or stroke. The whole sync is one undo step, and the reply counts what it did: `Synced: 2 changed, 1 added, 0 removed (14 already as written).` `--preview` renders the synced tree as it does a build.
To add one child, `build @child.json --parent ID` appends it last in its parent. To put it anywhere else, sync the parent with the child where it belongs.
> **Tip** Give layers you will change a `name` or a `key`. Matching by name keeps a layer's id even when it moves to another place in the list.
## Modify
`modify` takes the same keys as a node, for one layer and, with `$nodes`, its named descendants:
```sh
facet --doc f.facet modify Card '{"fill": "@paper", "$nodes": {"Title": {"text": "Tuesday", "font": "Inter 20/26 700"}}}'
```
## Replace a set
`build @set.json --replace` builds the set again in the old one's place. Instances move to the new variants with the same names and keep the properties they set.
---
# Checks
Every write checks what it asked for and reports what it could not do. Each warning names the layer, what it asked, what it got and the fix. The app shows the same warnings as badges on the canvas.
- **Verification.** Sizes and text a node asked for are compared with what was built (`NOT_APPLIED`).
- **Layout audit.** Content a fixed frame clips (`LAYOUT_OVERFLOW`) and content an edge-pinned layer covers (`LAYOUT_OCCLUDED`). `"overflow": "hidden"` marks clipping as intended.
- **Text.** A text box its content outgrows (`TEXT_TRUNCATED`), a font that had to be substituted (`FONT_SUBSTITUTED`).
The [Result codes](codes.md) page lists every code.
A design system's rules are a separate check: `facet --doc f.facet call lint` lists every layer that breaks the `DESIGN.md` beside the file. With no `DESIGN.md` it checks nothing, and says `exists: false`.
## Saying what must hold: `expect`
`expect` on any node states what must be true after layout. A miss is an `EXPECT_FAILED` warning, so a build can be trusted without a screenshot.
The headline is meant to fit on one line in a 240 px card. It takes three, and the build says so.
`checks-expect.json`:
```json
{
"name": "Headline",
"width": 240,
"padding": 16,
"fill": "#FFFFFF",
"column": [
{
"text": "Checkout errors rose on Saturday after the release",
"font": "Inter 18/24 700",
"width": "fill",
"expect": {"lines": 1}
}
]
}
```
| Key | Holds when |
|---|---|
| `lines` | a text has exactly this many lines |
| `maxLines` | a text has at most this many |
| `fits` | nothing in the layer is clipped or cut short |
| `width`, `height` | the layer is this size, or within `{"min", "max"}` |
| `noOverlap` | the children in a container's flow do not overlap |
## Previews
`--preview out.png` on any write renders what that call wrote: each top-level layer it built or synced, whole, or the layer it modified, not the page (a component set shows every variant). `facet shot NODE -o out.png --scale 2` renders any layer at any time. Previews draw in layer order, so scrims and sheets cover what is under them, as on the canvas.
---
# For agents
The language was measured against real agent sessions. These habits keep both tokens and round trips low.
## The cheapest loop
1. **Kit first, once per file.** Colors, type presets and every repeated shape as parts. Later calls send only nodes.
2. **Build whole screens in one call.** A list of nodes builds several top-level layers at once.
3. **Read the reply, not a screenshot.** Warnings and `expect` misses name the layer and the fix. Ask for `--preview` when you need to see the result, not after every call.
4. **Read back with `source`, change with `--sync`.** Never rebuild from scratch to change a value.
## Budget
| Task | Typical cost |
|---|---|
| A kit for an app | 600 to 1,000 tokens, once |
| A phone screen using the kit | 600 to 1,000 tokens |
| Reading a screen back (`source`) | 1,500 to 2,000 tokens |
| A component set with states and behavior | 800 to 1,500 tokens |
## When something goes wrong
| Reply says | Do |
|---|---|
| `INPUT_IGNORED` with a suggestion | fix the key it names |
| `"x" is not a key, a kit preset or part, or a component` | check `facet kit` and `facet components` |
| `NOT_APPLIED: width "fill" needs an auto-layout parent` | put the layer in a row or column, or give a number |
| `LAYOUT_OVERFLOW` | let the frame hug, make it larger, or say `"overflow": "hidden"` if the clipping is meant |
| `EXPECT_FAILED` | change the layout until the expectation holds |
| `Outcome unknown` after a long build | the host may still be working: wait, then `find` before retrying |
## Machine-readable documentation
- [llms.txt](llms.txt) lists every page with a one-line summary.
- [llms-full.txt](llms-full.txt) is all of it in one file, examples included.
- `facet guide` prints the whole reference; `facet guide reference` prints it as JSON.
## MCP
`facet mcp` serves the same tools over MCP on stdio, acting on the design in front in Facet (and opening Facet if it is not running); `facet --doc f.facet mcp` serves one file instead. [Connect your AI](connect.md) sets it up in each app.
---
# Agent card
Everything most builds need, in the order you need it. The other pages go deeper, and [the full documentation](llms-full.txt) holds all of them in one file.
## Commands
```sh
facet --doc f.facet build @screen.json --preview s.png # build, then read every warning
facet --doc f.facet source Screen # a layer, in the language
facet --doc f.facet build @screen.json --sync # change in place
facet --doc f.facet modify Card '{"fill": "@paper"}' # one or two values
facet --doc f.facet tree Screen # ids, sizes, places
facet --doc f.facet inspect 42 # every property of one layer
facet --doc f.facet shot Screen -o s.png --scale 2 # render any layer
facet --doc f.facet undo # each call is one undo step
facet --doc f.facet close # save and stop the host
```
`--doc` and the path are two arguments. A file the app has open is edited live in the app; add `--headless` after the path for a host of your own. `@file.json` reads a file; inline JSON works too.
## A build call
A node, a list of nodes, or `{"kit": {...}, "nodes": [...]}`. Send the kit once per file; it is saved with the file and every later call names it.
A kit and a card in one call. Every later call names `@ink`, `title` and `chip` without sending them again.
`card-start.json`:
```json
{
"kit": {
"font": "Inter 15/22",
"colors": {
"ink": "#1A1423",
"paper": "#FBF7F0",
"rose": "#D9485F",
"mute": "#6B6470"
},
"type": {"title": "Inter 22/28 700", "cap": "Inter 12/16 600 +0.4 caps"},
"parts": {
"chip": {
"params": {"label": "", "tone": "@rose"},
"node": {
"row": [{"text": "{label}", "color": "{tone}"}],
"padding": [2, 8],
"cornerRadius": 8,
"fill": "{tone}/12%"
}
}
}
},
"nodes": [
{
"name": "Card",
"width": 360,
"padding": 16,
"gap": 8,
"fill": "@paper",
"color": "@ink",
"cornerRadius": 16,
"column": [
{"title": "Today"},
{
"row": [
{"cap": "Raag Yaman"},
{"space": "fill"},
{"chip": "Evening"}
],
"width": "fill",
"align": "center"
},
{
"text": "Long text wraps at the card's width and takes the height of its lines.",
"width": "fill",
"color": "@mute"
},
{"icon": "lucide:clock", "size": 16, "color": "@mute"}
]
}
]
}
```
- **Strings are texts.** Containers pass `font`, `style` and `color` down to every text inside.
- **Kit colors** are variables: `@ink`, a tint `@ink/40%`, one value per mode with `"modes": ["Light", "Dark"]` and `"mode": "Dark"` on a layer.
- **Type presets** are text styles: `{"title": "Today"}` is a text in `title`; `"style": "cap"` sets a container's default.
- **Parts** take parameters: `{"chip": "Evening"}` (first parameter) or `{"chip": {"label": "Late", "tone": "@mute"}}`. `{param}` alone takes the value as it is (a node, a list); inside longer text it is text. Parts use parts and pass their parameters on: `{"avatar": {"initials": "{initials}"}}` inside `member`. A `children` parameter takes the children written at the use (`"{children}"`). A list parameter defaulting to `[]`, written alone among children (`{"row": [{"text": "{who}"}, {"space": "fill"}, "{badges}"]}`), adds nothing when left out and its nodes when given: `{"member": {"who": "Ben", "badges": [{"chip": "Blocked"}]}}`.
- **Names:** a key that is not a language key is a preset, then a part, then a component. Keys win, so never name a preset or part `name`, `text`, `size`, `row` or any other key. A preset and a part cannot share a name. `{"kit": {"parts": {"chip": null}}}` removes one.
- **Font shorthand:** `"Family Size/Line Weight"` plus `italic`, tracking `+0.4` or `-2%`, a case `caps`. Weights are 100 to 900 in hundreds, or `light`, `medium`, `semibold`, `bold` and the rest. `"17/26"` keeps the family; `"600"` changes only the weight. Any Google Font downloads on first use. Change a preset in the kit and every text in it follows at once.
## Layout
| Write | For |
|---|---|
| `{"row": [...]}`, `{"column": [...]}` | auto layout; `gap`, `padding` (number, `[v, h]`, `[t, r, b, l]`), `align` (cross: start, center, end, stretch), `justify` (start, center, end, space-between) |
| `{"grid": [...], "columns": 2}` | rows of columns (`[120, "fill"]` sizes them) |
| `{"stack": [...]}` | children overlap, centered; place each with `at` |
| `{"ring": [...], "r": 80}` | children around a circle |
| `"width"`, `"height"` | a number, `"fill"` (the room the flow leaves), `"hug"` (default for containers); `"size": 40` or `[w, h]` |
| `{"space": "fill"}`, `{"space": 12}` | push siblings apart, or a fixed gap |
| `"at"` | `[x, y]`, `[x, y, "center"]`, `"bottom right"`, `["top right", -8, 8]` (x right, y down), `{"angle": 90, "r": 60}`; takes the layer out of the flow |
| `"grow": 1` | take the leftover room along the flow |
Never compute positions: `at` with a place word, an anchor or an angle does it.
## Look
`fill` (a color, `"none"`, or a gradient `{"linear": ["@rose", "@ink"], "angle": 160}`), `"$image": "photo:latte art"` or a path for a picture, `stroke` and `strokeWidth`, `cornerRadius` (number or `[tl, tr, br, bl]`), `opacity`, `effects` (`{"type": "drop-shadow", "x": 0, "y": 4, "blur": 12, "color": "#0000001A"}`), `overflow: "hidden"`, `rotation`. Texts: `text`, `font`, `style`, `color`, `textAlign`, `maxLines: 1` (one line with an ellipsis), `"type": "paragraph"` with `"runs": ["Due ", {"chip": "Fri"}, " at noon."]` for chips inside a sentence.
`{"icon": "lucide:bell", "size": 20, "color": "@mute"}`: any Iconify set; each name becomes a component in an `Icons` frame, every use an instance. `{"draw": [{"circle": [20, 20, 18], "stroke": "@rose", "weight": 3}], "size": 40}` draws shapes (circle, ellipse, rect, line, poly, arc, path).
## Repeat
`{"each": ["Mon", "Tue", "Wed"], "do": {"chip": "{.}"}}`: `{.}` the item, `{field}` a field of an object item, `{i}` its index, `{#}` its number. `{"each": 5, "do": ...}` repeats a count.
## Components
A set with a property per text, a variant property shown in a text, a layer only one variant holds, and two instances in the same call.
`card-set.json` (after card-start.json, listed at the end):
```json
{
"nodes": [
{
"set": "Plan",
"props": {"Price": "$0", "Note": "Billed monthly"},
"variants": {"Tier": ["Free", "Pro"]},
"template": {
"name": "Plan",
"width": 240,
"padding": 16,
"gap": 6,
"fill": "#FFFFFF",
"column": [
{"title": "{Tier} plan"},
{"text": "{Price}", "name": "Price"},
{"text": "{Note}", "name": "Note", "width": "fill"},
{"chip": "Popular", "only": "Pro"}
]
},
"x": 0,
"y": 0
},
{
"name": "Plans",
"x": 0,
"y": 260,
"gap": 12,
"row": [
{"Plan": {"Tier": "Free"}},
{
"Plan": {"Tier": "Pro", "Note": "Billed yearly"},
"name": "Pro card"
}
]
}
]
}
```
- `"{Prop}"` binds a text to a property; `"visible": "{Show}"` binds visibility to a boolean one. `"{Tier} plan"` names a variant property, so each variant shows its own value.
- `only` keeps a layer to some variants. A variant's map changes it: `{"Tier": {"Free": {}, "Pro": {"fill": "#F3EFF7", "props": {"Price": "$12"}}}}`.
- A prop every variant sets for itself cannot be set by instances; the build warns.
- Give exposed texts `"width": "fill"` (or a number) so longer values wrap rather than widen the component.
- **Instances:** `{"Plan": {"Tier": "Pro", "Note": "Billed yearly"}, "name": "Pro card"}`. Variant property keys choose the variant; the rest set properties. One call can hold the set and the screen that uses it.
- **Behavior** on a set (`"behavior": {"start", "states", "axes", "plays", "targets"}`) is stored as a feature named after the set (`plan`); `facet chart plan` checks it. See [Behavior](behavior.md).
## Changing what is there
| Change | Do |
|---|---|
| a value or two | `modify NODE '{...}'`; `$nodes` reaches named descendants: `{"$nodes": {"Title": {"text": "Tue"}}}` |
| a tree | edit the JSON you built it from, then `build --sync`: matched layers keep ids and instances; the reply counts what changed |
| a component set | `build @set.json --replace`: instances follow to the new variants and keep what they set |
| add one layer | `build @row.json --parent ID` appends it last; it takes the font and color the parent's texts share |
Name the layers you will change, or give repeated ones a `key`: sync matches by key or name, and same-named siblings in order. `source` prints a layer in the language with parts expanded; for a screen built from parts, keep editing your own JSON.
## Checks
Every write verifies itself and reports, by layer, what did not happen: `NOT_APPLIED`, `INPUT_IGNORED` (a key not read, with the nearest one), `LAYOUT_OVERFLOW`, `LAYOUT_OCCLUDED`, `TEXT_TRUNCATED`, `FONT_SUBSTITUTED`, `TOKEN_FALLBACK`. Fix each one; the app shows them as badges. State what must hold instead of looking: `"expect": {"lines": 1}`, `{"fits": true}`, `{"noOverlap": true}`, `{"width": {"max": 320}}`; a miss is `EXPECT_FAILED`.
Look at the `--preview` PNG when the result matters visually. It renders the top-level layers the call built or synced, whole, not the page.
## Traps
- Frames have no fill until you give one; `"fill": "none"` removes one (`null` is not a value).
- `"fill"` width needs a row, column or grid parent; a top-level layer gets a number.
- A text in a stack or with `at` keeps its own width; give it a `width` to wrap.
- Overlapping top-level frames hide each other: put component sets to the side (`"x": 1400`).
- `"♈︎"` draws a symbol as text; without U+FE0E some symbols draw as color emoji.
---
# Updates and uninstalling
## Updates
Facet checks for a new version once a day. When there is one, it shows what changed and offers **Install Update**; Facet restarts in a few seconds with your files reopened. Choose **Facet › Check for Updates…** to check now.
The `facet` command and the skill your assistants use live inside the app, so they update with it.
With Homebrew, updates still arrive inside the app; `brew upgrade --cask --greedy facet` also works.
## Uninstall
Choose **Facet › Uninstall Facet…**. It removes:
- the `facet` command it installed,
- the Facet entries it added to your AI apps (with a backup of each file it changes) and the skill links,
- Facet's settings, recent files, thumbnails and caches,
- the app itself, once Facet quits.
It never touches your designs: `.facet` files stay where they are, in Documents › Facet or wherever you saved them.
From Terminal, `facet uninstall` does the same; `facet uninstall --keep-data` keeps settings, recent files and design systems for a later reinstall.
If you installed with Homebrew:
```sh
brew uninstall --cask facet # the app and the command
brew uninstall --zap --cask facet # also settings, caches and skill links
```
> **Note** A skill you uploaded to Claude's chat lives in your Claude account: remove it in Claude under **Settings › Capabilities › Skills**.
---
# Troubleshooting
## "Facet can't be opened"
Facet is notarized by Apple, so a downloaded copy asks once and opens. If macOS refuses instead, open **System Settings › Privacy & Security**, scroll to the message about Facet and click **Open Anyway**. Download Facet again from [faux.design](https://faux.design) if the message says the file is damaged: the download was cut short.
## Facet asks to move to Applications
Facet is running from the download window or the Downloads folder. Click **Move to Applications**: from anywhere else it cannot update itself, and your AI apps cannot find it.
## My assistant says it has no Facet tools
1. In Facet, open **Facet › Connect AI Apps…**. If the app shows **Repair**, click it; if it shows **Connect**, click it.
2. Quit the assistant completely (⌘Q, not just closing its window) and open it again. Claude, ChatGPT and Codex read their tools only at startup.
3. In Terminal, `facet setup` shows what Facet set up for each app.
4. Claude (desktop app) writes what went wrong to its logs folder, `~/Library/Logs/Claude/` (the file with `facet` in its name).
## My assistant edits the wrong design
The Facet tools act on the design in front in Facet. Click the design's tab, or name the file in your request ("in Welcome.facet, …"); assistants can list your open designs and open others.
## macOS asks whether an app may access a folder
When an assistant opens a design in Documents, Desktop or Downloads for the first time, macOS asks whether that app (for example Claude) may access the folder. Allow it, or keep designs in **Documents › Facet** and allow it once.
## "facet: command not found"
The command is not installed, or Terminal was open before it was. Choose **Facet › Install Command Line Tool…**, then open a new Terminal window. The command always works by its full path: `/Applications/Facet.app/Contents/Helpers/facet`.
## Text shows in a different font
Facet downloads Google Fonts the first time a design uses them; until then, and offline, it uses a stand-in and the design shows a font warning. Reconnect to the internet and reopen the design. Fonts that are not on Google Fonts must be installed on your Mac (double-click the font file).
## Start over
`facet setup disconnect all` removes Facet from every assistant; **Facet › Connect AI Apps…** connects them again. To reset Facet completely, [uninstall](updates.md) and install again; your designs are never removed.
---
# Gallery
Each of these is built by the docs build from the source beside it.
## A phone screen
Raag's Today screen. With the kit sent once, the screen itself is about 900 tokens.
`raag-today.json` (after raag-kit.json, listed at the end):
```json
{
"phone": {
"name": "Today",
"children": [
{
"row": [
{
"draw": [
{"path": "M20 2v14", "stroke": "@ink", "weight": 2},
{
"circle": [20, 30, 14],
"fill": {"linear": ["@saffron", "@rose"], "angle": 160}
}
],
"size": [40, 46],
"name": "Mark"
},
{
"column": [
{"title": "Raag"},
{"deva": "आज का रियाज़", "color": "@mute"}
]
},
{"space": "fill"},
{"icon": "lucide:settings-2", "size": 22}
],
"gap": 12,
"align": "center",
"width": "fill"
},
{
"stack": [
{
"draw": [
{
"circle": [150, 150, 144],
"fill": {"radial": ["#FFFFFF00", "@rose/10%"]},
"stroke": "none"
},
{
"circle": [150, 150, 120],
"stroke": "#1A142333",
"dash": [2, 6]
},
{
"arc": [150, 150, 120, 270, 315],
"stroke": {"linear": ["@saffron", "@rose"], "angle": 45},
"weight": 14
}
],
"size": 300
},
{
"ring": [
{
"each": ["रात", "सुबह", "दोपहर", "शाम"],
"do": {"deva": "{.}", "color": "@mute"}
}
],
"r": 86
},
{
"column": [
{"deva-xl": "यमन"},
{"small": "Yaman's hour", "color": "@mute"}
],
"align": "center"
}
],
"width": "fill"
},
{
"type": "paragraph",
"width": "fill",
"runs": [
"It's evening, so Yaman. Every note is shuddha except ",
{"swara": {"note": "तीव्र म", "tone": "@rose"}},
": sing it a touch sharp, and land on ",
{"swara": "नि"},
" and ",
{"swara": "ग"},
" more than on ",
{"swara": {"note": "सा", "tone": "@mute"}},
"."
]
},
{
"row": [
{"sticker": {"label": "12 दिन लगातार", "turn": -4}},
{
"sticker": {
"label": "Tanpura in C♯",
"tone": "#E3E7FA",
"turn": 3
}
},
{"sticker": {"label": "40 min", "tone": "#F9D7DD", "turn": -2}}
],
"gap": 10,
"align": "center"
}
]
}
}
```
## A component library
Three instances of a set whose states, properties and behavior come from one build.
`raga-library.json` (after raag-kit.json, raga-card.json, listed at the end):
```json
{
"name": "Library",
"gap": 20,
"align": "start",
"row": [
{"Raga/Card": {"State": "Learning"}},
{
"Raga/Card": {
"State": "New",
"Name": "राग भीमपलासी",
"Hour": "AFTERNOON"
}
},
{
"Raga/Card": {
"State": "Locked",
"Name": "राग मालकौंस",
"Hour": "LATE NIGHT"
}
}
]
}
```
## Metrics
Three metric cards from one part. Each sparkline is a `poly` drawn in the part, with its points passed as a parameter.
`gallery-metrics.json`:
```json
{
"kit": {
"font": "Inter 14/20",
"colors": {
"ink": "#14161B",
"muted": "#5B606B",
"line": "#E3E5EA",
"good": "#1E7A55",
"bad": "#C2306F",
"cobalt": "#2B4ACB"
},
"type": {
"kpi": "Schibsted Grotesk 30/34 800 -0.5",
"label": "Inter 12/16 600 +0.6 caps"
},
"parts": {
"metric": {
"params": {
"label": "",
"value": "",
"delta": "",
"tone": "@good",
"points": [0, 0]
},
"node": {
"column": [
{"label": "{label}", "color": "@muted"},
{"kpi": "{value}"},
{
"row": [
{"text": "{delta}", "font": "13/18 600", "color": "{tone}"},
{"space": "fill"},
{
"draw": [
{"poly": "{points}", "stroke": "{tone}", "weight": 2}
],
"size": [80, 28]
}
],
"width": "fill",
"align": "center"
}
],
"width": 220,
"padding": 16,
"gap": 6,
"fill": "#FFFFFF",
"stroke": "@line",
"cornerRadius": 14
}
}
}
},
"nodes": [
{
"name": "Metrics",
"gap": 12,
"color": "@ink",
"row": [
{
"metric": {
"label": "Traces today",
"value": "48,210",
"delta": "+12% vs Sat",
"points": [0, 22, 13, 18, 27, 20, 40, 12, 53, 14, 67, 8, 80, 4]
}
},
{
"metric": {
"label": "Error rate",
"value": "2.4%",
"delta": "+0.9 pts",
"tone": "@bad",
"points": [0, 20, 13, 21, 27, 18, 40, 16, 53, 10, 67, 9, 80, 3]
}
},
{
"metric": {
"label": "p95 latency",
"value": "812 ms",
"delta": "−40 ms",
"points": [0, 6, 13, 9, 27, 8, 40, 14, 53, 16, 67, 20, 80, 22]
}
}
]
}
]
}
```
## Settings
A settings list from two parts. The third row passes its own control in place of the toggle.
`gallery-settings.json`:
```json
{
"kit": {
"parts": {
"toggle": {
"params": {"on": "#2B4ACB", "at": "right"},
"node": {
"stack": [
{
"type": "rectangle",
"width": 44,
"height": 26,
"cornerRadius": 13,
"fill": "{on}"
},
{
"type": "ellipse",
"size": 20,
"fill": "#FFFFFF",
"at": ["{at}", 0, 0]
}
],
"width": 44,
"height": 26,
"padding": 3
}
},
"setting": {
"params": {"title": "", "detail": "", "control": {"toggle": {}}},
"node": {
"row": [
{
"column": [
{"text": "{title}", "font": "15/20 600"},
{"text": "{detail}", "color": "#5B606B"}
],
"width": "fill",
"gap": 2
},
"{control}"
],
"width": "fill",
"align": "center",
"gap": 16,
"padding": [14, 0]
}
}
}
},
"nodes": [
{
"name": "Settings",
"font": "Inter 14/20",
"color": "#14161B",
"width": 380,
"padding": [8, 20],
"fill": "#FFFFFF",
"cornerRadius": 16,
"column": [
{
"setting": {
"title": "Daily digest",
"detail": "Email and Slack at 09:00"
}
},
{
"setting": {
"title": "Only when something changed",
"detail": "Skip quiet days",
"control": {"toggle": {"on": "#C9CDD6", "at": "left"}}
}
},
{
"setting": {
"title": "Time zone",
"detail": "Asia/Kolkata",
"control": {
"row": [
{"text": "Change", "font": "14/20 600", "color": "#2B4ACB"}
]
}
}
}
]
}
]
}
```
---
# Keys
Every key a node can carry, grouped as `facet guide` prints them. This page is generated from the same table the builder reads, so it lists exactly what is applied.
## Language (write these first)
| Key | Forms | What it does |
|---|---|---|
| `kit` | `{"font", "modes", "colors", "type", "parts"}` | beside "nodes" (or alone): say colors, type presets and parts once; kept with the file and named everywhere after |
| `nodes` | `[NODE, ...]` | several top-level nodes in one call (a bare node or a list also works) |
| `row` | `[NODE, ...]` | also column, grid, stack (children overlap, placed with at) and ring (children around a circle, "r") |
| `font` | `"Family Size/Line Weight italic +0.4 caps"` | on a text; on a container it is the font of every text inside |
| `style` | `preset or text style name` | on a text or a container; {"preset": "text"} is a text in that preset |
| `color` | `@name \| @name/40% \| #hex` | on a container: the color of every text inside; @name is a kit color, /40% a tint (its own variable) |
| `at` | `[x, y] \| [x, y, "center"] \| "bottom right" \| [place, dx, dy] \| {"angle", "r"}` | place a layer: absolute, anchored, by words in its parent, or around the parent's center (degrees from the top) |
| `each` | `list \| count` | with "do": NODE, one node per item; {.} is the item, {i} its index, {#} its number, {field} a field |
| `space` | `number \| "fill"` | an empty gap in a row or column |
| `icon` | `"set:name"` | an icon (same as $icon); size sets both sides |
| `draw` | `[{"circle"\|"ellipse"\|"rect"\|"line"\|"poly"\|"arc"\|"path": ...}]` | shapes as data in a box of "size"; fill, stroke, weight, dash, cap per shape |
| `set` | `name` | a component set: "variants", "template", "props" (bound with "{Prop}"), "behavior" (a statechart) and "looks" |
| `variants` | `{Property: [values]} \| {Property: {Value: {keys, "$nodes", "props"}}}` | in a set: its variant properties; the first value is the default; a value's map changes that variant |
| `props` | `{Name: default}` | in a set: its properties; a layer binds with "text": "{Name}" or "visible": "{Name}"; a text naming a variant property ("{Tier} plan") shows each variant's value |
| `behavior` | `{"start", "states", "axes", "plays", "targets"}` | in a set: the states it moves through, stored as a feature named after the set |
| `looks` | `{Axis: {Option: "Collection/Mode"}}` | in a set with behavior: design axes shown in variable modes |
| `only` | `value \| [values] \| {Property: [values]}` | in a set's template: the layer exists only in those variants |
| `expect` | `{"lines", "maxLines", "fits", "width", "height", "noOverlap"}` | what must hold after layout; a miss is EXPECT_FAILED |
| `key` | `string` | what build --sync matches the layer by (else its name) |
| `mode` | `"Dark" \| {"Collection": "Mode"}` | show this layer and everything in it in a mode of the kit's (or a collection's) variables |
## Structure
| Key | Forms | What it does |
|---|---|---|
| `type` | `"frame" \| "row" \| "column" \| "grid" \| "text" \| "rectangle" \| "ellipse" \| "vector" \| "line" \| "component-set"` | what to build; omitted means "column" (a frame with vertical auto layout) |
| `name` | `string` | layer name; results return ids by name, and $nodes/$override address layers by it |
| `children` | `[NODE, ...]` | child nodes, in order |
## Size and place
| Key | Forms | What it does |
|---|---|---|
| `width` | `number \| "fill" \| "hug"` | "fill" takes the free room of an auto layout parent; "hug" fits the content ("100%" and "auto" also work) |
| `height` | `number \| "fill" \| "hug"` | as width |
| `minWidth` | `number` | also maxWidth, minHeight, maxHeight |
| `x` | `number` | position in the parent; ignored inside auto layout unless "position": "absolute" |
| `y` | `number` | as x |
| `position` | `"absolute" \| "auto"` | absolute takes a child out of its parent's flow (pinned bars, badges) |
| `rotation` | `degrees` | counterclockwise, about the node's origin |
| `constraints` | `{"horizontal": "left"\|"right"\|"center"\|"left-right"\|"scale", "vertical": "top"\|"bottom"\|"center"\|"top-bottom"\|"scale"}` | how a child follows a resized fixed frame |
## Auto layout
| Key | Forms | What it does |
|---|---|---|
| `gap` | `number \| {"row", "column"} \| "auto"` | space between children; "auto" spreads them (space-between) |
| `padding` | `number \| [vertical, horizontal] \| [top, right, bottom, left] \| {"top","right","bottom","left","horizontal","vertical"}` | inner space |
| `justify` | `"start" \| "center" \| "end" \| "space-between"` | along the flow; needs room (a number or "fill" size) |
| `align` | `"start" \| "center" \| "end" \| "stretch"` | across the flow; stretch makes children fill across |
| `wrap` | `bool` | rows wrap onto new lines |
| `grow` | `1` | fill the free room along the parent's flow |
| `columns` | `number \| [number \| "fill" \| "Nfr", ...]` | grid: column count (equal shares) or tracks: widths and shares of the free width |
| `columnSpan` | `number` | grid child: columns it spans |
## Look
| Key | Forms | What it does |
|---|---|---|
| `fill` | `"#RRGGBB[AA]" \| "rgb(...)" \| color name \| "@Collection/name\|#fallback" \| {"type":"linear"\|"radial"\|"angular"\|"diamond","stops":[{"position","color"}],"angle"} \| {"type":"image","url":...} \| [paint, ...] \| null` | fills of frames and shapes, bottom first; each paint object may add opacity, visible and blendMode; a lone token color stays bound to its variable; text uses color |
| `stroke` | `color \| gradient \| [paint, ...]` | border; with strokeWidth (default 1) and strokeAlign (inside, center, outside) |
| `strokeWidth` | `number \| {"top","right","bottom","left"}` | border width; per side on frames and rectangles |
| `strokeDash` | `[dash, gap] \| "none"` | dashed border; strokeCap (none, round, square) and strokeJoin (miter, bevel, round) shape vector strokes |
| `strokeAlign` | `"inside" \| "center" \| "outside"` | border position |
| `cornerRadius` | `number \| {"topLeft","topRight","bottomRight","bottomLeft"} \| [tl, tr, br, bl]` | rounded corners, one radius or one per corner |
| `effects` | `{"type":"drop-shadow","x","y","blur","spread","color"} \| {"type":"blur","blur"} \| [...]` | drop-shadow and inner-shadow (several of each, bottom first), one blur (layer blur) and one background-blur |
| `opacity` | `0..1` | layer opacity |
| `blendMode` | `"pass-through" \| "normal" \| "multiply" \| "screen" \| "overlay" \| ...` | layer blend mode (18 modes) |
| `clip` | `bool` | clip children to the frame (default: fixed-size or rounded frames clip) |
| `overflow` | `"hidden" \| "vertical" \| "horizontal"` | content beyond the frame is expected: clipped on purpose (hidden) or scrolled; the audit does not flag it |
| `visible` | `bool` | false hides the layer ("hidden": true also works) |
| `locked` | `bool` | lock against pointer edits |
## Text
| Key | Forms | What it does |
|---|---|---|
| `text` | `string` | the characters; \n starts a paragraph |
| `runs` | `["text", NODE, ...]` | with type "paragraph": a sentence with components in it (chips, icons), wrapping like text; text keys style every word; wordSpacing and lineGap tune the spacing |
| `fontSize` | `number` | size in px (default 16) |
| `fontWeight` | `100..900 \| "Regular" \| "Medium" \| "Semi Bold" \| "Bold" \| ...` | weight |
| `fontFamily` | `string` | family name; one this machine lacks is fetched from Google Fonts, otherwise substituted with a warning |
| `color` | `color` | text color (also on $icon) |
| `lineHeight` | `number \| "150%" \| "auto" \| {"unit":"PIXELS"\|"PERCENT"\|"AUTO","value"}` | line height |
| `letterSpacing` | `number \| "5%"` | tracking |
| `textAlign` | `"left" \| "center" \| "right" \| "justified"` | a text that fills its row needs center to look centered |
| `resize` | `"width" \| "height" \| "none" \| "truncate"` | auto width (one line), wrap at the width (auto height), fixed box, or fixed with an ellipsis |
| `spans` | `[{"text", ...character keys}, ...]` | mixed styles in one layer: each span sets fontWeight, fontSize, fontFamily, italic, color, textDecoration, textCase, letterSpacing or lineHeight for its text |
| `textDecoration` | `"none" \| "underline" \| "strikethrough"` | decoration; also textCase (original, upper, lower, title), italic (bool), paragraphSpacing (px) and maxLines (truncates with an ellipsis) |
| `$textStyle` | `style name` | follow a text style; a new name creates the style from this layer |
## Components
| Key | Forms | What it does |
|---|---|---|
| `$instance` | `component or set name` | an instance; a set gives its first variant; "scale": 0.5 scales everything it shows (sizes, text, strokes, radii) |
| `$variant` | `{"Property": "Value"}` | choose a variant |
| `$override` | `{"Layer": "text" \| bool \| {props} \| {"Property": "Value"}}` | per-instance text, visibility, fill, color, stroke, image, width and height, a nested instance's variant, or an icon swap ("Icon": "lucide:moon"); layout follows the component |
| `scale` | `number (0.01 to 100)` | with $instance: show the component scaled, strokes, radii, effects, spacing and text included (a live thumbnail) |
| `template` | `NODE` | component-set: the shape every variant starts from |
| `variantMatrix` | `{"Property": {"Value": {props, "$nodes": {...}}} \| ["Value", ...]}` | component-set: one variant per combination, in written order |
| `$nodes` | `{"Layer": {props}}` | edit named descendants (variantMatrix entries and modify) |
| `$clone` | `node id` | copy an existing node (placed beside it) |
| `$expose` | `{"characters": "Name"} \| {"visible": "Name"}` | with convertToComponent: the layer's text or visibility becomes a component property; instances set it with $override {"Name": value} |
| `$exposeInstance` | `true` | with convertToComponent: a nested instance shows its properties on the component's instances |
| `$variants` | `{"Property": ["Value", ...]}` | inside a component-set template: the layer exists only in variants with those values (other variants leave it out instead of hiding it) |
## Media
| Key | Forms | What it does |
|---|---|---|
| `$image` | `file path \| file:// \| data: URL \| https:// URL \| "photo:QUERY" \| "hash:HEX"` | an image fill (with scaleMode fill, fit, crop or tile); URLs and photo searches are fetched once and cached; hash: reuses an image in the document |
| `scaleMode` | `"fill" \| "fit" \| "crop" \| "tile"` | how $image fits |
| `svgPath` | `SVG path data` | vector: one subpath (M, L, H, V, C, S, Q, T, Z) |
| `$svg` | `