Concepts

What an intent is

An intent is a named purpose: one entry in your registry that says what something is for, what it resolves to, and how to tell it apart from its neighbours. A button, a failure and a log line are all described the same way.

Anatomy

An intent looks the same whatever it describes. A button and a failure are both one entry in the registry:

TS
'action.delete': {
kind: 'semantic',
description: 'Permanently destroys data',
refs: ['control.destructive'],
keywords: ['delete', 'destroy', 'erase'],
notFor: [
{ context: 'closing or hiding something', instead: 'action.dismiss' },
{ context: 'stopping a task without deleting anything', instead: 'action.cancel' },
],
},
TS
'conflict.duplicate': {
kind: 'semantic',
description: 'It would create something that exists: a taken handle or email, a second follow',
refs: ['status.conflict'],
binding: { publicMessage: 'That already exists.' },
keywords: ['taken', 'exists', 'already', 'duplicate'],
},

id: domain.meaning, with an optional third part for a structural variant (field.password.shown). The domain says what kind of thing it is, and which profile gives it meaning.

description: one sentence about what it is for. Required, because it is what people and agents choose by.

refs and binding: what it resolves to. A semantic intent inherits from its refs in order, then layers its own binding on top. For UI that is classes, an element, a role and input behaviour; for errors a status and what the caller sees; for logs a level and a shape.

notFor: when not to use it, and what to use instead. These links are what make a set of intents teachable.

keywords: words code uses for it. check uses them to spot an intent that disagrees with its own handler, label or message.

Two layers: meaning and binding

Call sites only ever use meaning intents, named for the job: action.save, conflict.duplicate, audit.created. They refer to shared intents that hold the binding: looks for UI (control.*, typography.*), statuses for errors (status.*), levels for logs (level.*).

TS
// Shared bindings, never used at a call site.
'control.destructive': { kind: 'semantic', refs: ['control.base'], binding: { classes: 'bg-destructive ...' } },
'status.conflict': { kind: 'primitive', binding: { status: 409, expose: 'message' } },

// Meaning, what call sites use, named for the job.
'action.delete': { kind: 'semantic', refs: ['control.destructive'], ... },
'conflict.duplicate': { kind: 'semantic', refs: ['status.conflict'], ... },

This is where reuse happens, and where you change your mind. Twenty actions can share four or five looks, and changing control.primary restyles every action built on it; changing status.internal changes every 500. Meanwhile each call site stays specific about what it does. Shared intents are rejected at call sites, by the types and by check.

Each profile has its own way in

TSX
<Action intent="action.delete" onClick={removeProject}>Delete project</Action>  // UI: a component
return fail('conflict.duplicate', 'That handle is already taken.'); // errors: fail()
log('audit.created', 'Account created', { userId: user.id }); // logs: log()

UI intents render through components, and the intent picks the component and the element: heading.page is an <h1> through <Text>, action.delete a <button> through <Action>, layout.list a <ul> through <Layout>. Only <Layout> takes a className, and only for layout and spacing; the escape hatch, unsafeClassName, needs a written reason and is counted by check. The components reference has the rest.

Error intents go through fail(), which returns the response with the intent’s status and message and writes the log entry it names. Log intents go through log(), which writes one line in a known shape at the intent’s level. Both helpers are copied into your repo, like the components.

In UI: state and data, without styling

TSX
<Action intent="nav.view" pressed={tab === 'following'}>Following</Action>
<Action intent="nav.page" href="/explore" current={path === '/explore'}>Discover</Action>
<Action intent="choice.color" vars={{ '--choice-color': color }} label={name} icon={null} />

State is a prop, not a style. pressed and current set aria-pressed and aria-current, and the intent styles them with Tailwind’s aria-* variants. Other state goes through data-* attributes (data-read, data-compact). Values that come from data, like a colour a user picked, go through vars as CSS custom properties.