Get started
Built for agents
Agents meet macbetter twice: as users of the interfaces you build, and as the engineers building them. Both depend on the same thing, controls with names.
Agents that use your interface
Every interactive macbetter component is a ui-sdk component with a class and some data attributes added. macbetter doesn't replace ui-sdk's behavior, so it doesn't hide anything from ui-sdk's agent bridge either.
The bridge comes from @ui-sdk/react/agent. Create it once, in the browser:
import { createAgent } from "@ui-sdk/react/agent"
const agent = createAgent()
export { agent }It finds the ui-sdk components on the page and registers new ones as they mount. agent.snapshot() lists each component with its kind, its accessible name, its state and the actions it supports. For the settings form on the introduction, with just its Wi-Fi Switch, the Appearance SegmentedControl and the Apply Button, it returns:
[
{ "ref": "c1", "component": "switch", "label": "Wi-Fi", "checked": true, "value": "on", "actions": ["setChecked"] },
{
"ref": "c2",
"component": "segmented-control",
"label": "Appearance",
"value": "auto",
"items": [
{ "value": "light", "label": "Light" },
{ "value": "dark", "label": "Dark" },
{ "value": "auto", "label": "Auto" }
],
"actions": ["setValue"]
},
{ "ref": "c3", "component": "button", "label": "Apply", "actions": ["press"] }
]An agent then calls actions by label or ref:
await agent.act({
steps: [
{ ref: "Wi-Fi", action: "setChecked", args: [false] },
{ ref: "Apply", action: "press" },
],
})The result reports what each step changed. Actions go through the component's own API, so your onCheckedChange or onValueChange handler runs as it would for a person, with details.source === "agent", and can refuse the change with details.cancel().
agent.query() finds components and the choices inside lists without reading the whole page. agentTools describes snapshot, act and query as tool definitions for your own model, and registerTools() offers them to a browser agent through WebMCP, in browsers that support it.
The exact state keys and action names depend on each component. Read them from a snapshot rather than guessing, and see ui-sdk's agent docs for the full reference.
What you control
- Names. The bridge identifies controls by their accessible names, the same ones a screen reader announces. A
Switchwrapped in a<label>, aSegmentedControlwitharia-label, and aButtonwith text all have one. An icon-only button withoutaria-labeldoesn't, and neither a person using VoiceOver nor an agent can tell what it does. - Permissions.
createAgent({ allow })approves or refuses each requested action, and can return a promise while you ask the person. - Marks.
data-agent-confirm="…"on an element makes any action inside it wait for the person.data-agent-hiddenkeeps a region out of snapshots.data-agent-secretkeeps a field's value redacted. Password, card and one-time-code fields are redacted without a mark.
<Button variant="filled" tone="destructive" data-agent-confirm="Deletes the project and its 14 files">
Delete project
</Button>macbetter passes every extra prop to the ui-sdk part, so these attributes work on any component.
Agents that build with macbetter
The repository is set up for coding agents. Its AGENTS.md is the working agreement every component follows, and its rules are the ones that keep the system consistent:
- Wrap ui-sdk, don't reimplement it. A component is a ui-sdk part plus a class and data attributes. No hand-written
role=for anything ui-sdk ships. - Classes and variants.
mb-<component>andmb-<component>-<part>; variants asdata-variant,data-sizeanddata-tone. - Tokens only. Every color, radius, shadow, duration and font is a
--mb-*token, written to work in light and dark and on every platform. - Platforms nest. Per-platform numbers are inherited variables; per-platform rules use
@scope … to (:scope [data-platform]). - Name every control, in demos and in app code.
- Look at it. A change is checked in a real browser, in light and dark, on macOS and iOS.
A prompt for your agent
Paste this into a coding agent working in an app that uses macbetter:
This app uses macbetter (macbetter), a design system built on ui-sdk
that renders macOS, iPadOS and iOS from one set of React components.
Before writing UI:
1. Read node_modules/@ui-sdk/react/docs/llms.txt for how ui-sdk components behave,
and follow the ui-sdk skill if it's installed.
2. Read the macbetter component you're about to use in
node_modules/macbetter/src/components/<name>.tsx.
When you write UI:
- Import components from their own entries, like macbetter/button.
- Use macbetter components for anything interactive. Don't hand-write role= for
something ui-sdk or macbetter already ships.
- Give every control an accessible name: visible text, a <label>, or aria-label.
- Style your own layout with macbetter's tokens (var(--mb-space-*), --mb-label-*,
--mb-bg-*, --mb-radius-*), never raw hex colors or pixel font sizes.
- Size things with platform tokens (--mb-control-height, --mb-row-height,
--mb-hit-target, --mb-text-*) so the layout adapts to macOS, iPadOS and iOS.
- Wrap hover styles in @media (hover: hover) and (pointer: fine).
- Check the result in a browser in light and dark, with data-platform set to
macos and to ios.Install the ui-sdk skill if your agent supports skills:
npx skills add vercel-labs/ui --skill ui-sdk