# Define a Primitive

Give your primitive options, expose state to its parts, and work with elements.

`definePrimitive` brings a component's options, state, methods, and parts together. Use its slots to expose attributes and state, handle events, and work with mounted elements.

In [Build a Component](/docs/core/build-a-component), you defined a mute button. This page goes through each field of a definition, using a copy button. Each demo's Code tab has the full example.

Here's the copy button this page builds, as one definition. Each section below covers one of its fields in more detail.

```ts title="copy-button.ts"
import { definePrimitive } from "@ui-sdk/core"

type CopyButtonOptions = { text?: string; onCopy?: (text: string) => void }

const copyButton = definePrimitive({
  name: "copy-button",
  options: { text: "string", onCopy: "callback" },
  state: (options: CopyButtonOptions) => ({ copied: false }),
  api: (context) => ({
    async copy() {
      const { text = "", onCopy } = context.options()
      await navigator.clipboard.writeText(text)
      context.store.set({ copied: true })
      onCopy?.(text)
    },
  }),
  slots: (slot) => ({
    root: slot<{}, { copied: boolean }>({
      state: (state) => ({ copied: state.copied }),
      attrs: () => ({ type: "button" }),
      press: ({ api }) => api.copy(),
    }),
  }),
})

export { copyButton }
```

## Options

Let's give a copy button some text to copy. Describe the options as a type, list each one's kind in `options`, and type `state`'s parameter with it:

```ts
type CopyButtonOptions = { text?: string; onCopy?: (text: string) => void }
```

```ts
options: { text: "string", onCopy: "callback" },
state: (options: CopyButtonOptions) => ({ copied: false }),
```

Typing `state`'s options is what types `context.options()` in `api`, so the type is written once. The kinds tell adapters how to handle each value. For example, web components read `text` from an attribute and turn `onCopy` into an event. TypeScript checks that every option has a kind. If your primitive has no options, leave `options` out.

Read the current values with `context.options()`. Here, `copy` reads the text when the button is pressed:

```ts
api: (context) => {
  return {
    async copy() {
      const { text = "", onCopy } = context.options()
      await navigator.clipboard.writeText(text)
      context.store.set({ copied: true })
      onCopy?.(text)
    },
  }
},
```

`api` runs once per instance, so reading options inside the method avoids holding on to an old value.

### When an Option Changes

Use `context.onOptions` when a change needs some extra work. For the copy button, new text should clear the previous "Copied" state. Add this inside `api`, before returning its methods:

```ts
let text = context.options().text

context.onOptions((options) => {
  if (options.text === text) return
  text = options.text
  context.store.set({ copied: false })
})
```

To keep a controlled value in sync instead, use `sync`, as shown in [Handle Changes](/docs/core/kernel/handle-changes#control-the-state).

### Option Kinds

The same kinds work in a slot's `props` schema, which gives each part values of its own, like a star's `value` in [Compose Behaviors](/docs/core/compose-behaviors#add-a-star).

| Kind                                | Use it for                                                                                       |
| ----------------------------------- | ------------------------------------------------------------------------------------------------ |
| `"string"`, `"number"`, `"boolean"` | Values that can be read from attributes.                                                         |
| `"list"`                            | A string or a list of strings. An attribute uses commas between values.                          |
| `"value"`                           | Other values, such as objects, elements or functions. Set these as properties on web components. |
| `"callback"`                        | A callback that reports something. Vue treats it as an emit; web components fire an event.       |
| `"action"`                          | A function whose return value matters, such as a Promise. Vue keeps it as a function prop.       |
| `"id"`                              | A string the adapter generates when it's omitted.                                                |
| `"key"`                             | A list item's value, of any type. The part is keyed by it, and the parts inside see the key.     |

For a part nested inside another part, `{ kind: "string", from: "item" }` inherits the same prop from the enclosing `item` when it's omitted. Use `from: "group.id"` to inherit a different prop, and add `required: true` when the part only makes sense inside its parent, so using it anywhere else throws. A function `(attribute) => value` can parse a web component attribute when none of the built-in kinds fit.

## What a Part Shows

Attributes are enough for styling. To let the component's content read state too, add `state` to its slot:

```ts
root: slot<{}, { copied: boolean }>({
  state: (state) => ({ copied: state.copied }),
  attrs: () => ({ type: "button" }),
  press: ({ api }) => api.copy(),
}),
```

The first type argument describes the part's props; the second describes what its `state` returns. In React, the copy button's children can now read `copied`:

```tsx
<CopyButton text="npm add @ui-sdk/core">
  {({ copied }) => (copied ? "Copied" : "Copy")}
</CopyButton>
```

Try it. This version also resets after a short timeout.

Press Copy. The button reads Copied while the command is copied, then goes back to Copy.

## Parts That Come and Go

Use `present` when a part should only appear in some states. This panel is present while `open` is `true`:

```ts
panel: slot({
  present: (state) => state.open,
  attrs: (_state, part) => ({ id: `${part.id}-panel` }),
}),
```

The trigger points to the panel with `aria-controls`:

```ts
trigger: slot({
  attrs: (state, part) => ({
    type: "button",
    "aria-expanded": String(state.open),
    "aria-controls": `${part.id}-panel`,
  }),
  press: ({ api }) => api.toggle(),
}),
```

`part.id` is the instance's unique id. Both parts use it to build the same panel id. If an id includes an item value, use `idPart(value)` from `@ui-sdk/core` to escape spaces and percent signs.

An id the author writes on a part's element wins over the one its slot makes, in every framework and on web components. The parts that render after it point at the author's id from their first render, on a server too, and once the element binds, every part does: `aria-controls`, `aria-labelledby`, `aria-describedby`, `aria-activedescendant`, `for` and the other references to the made id point at it, so build references from `part.id` and the core keeps them right.

Leave `present` out for parts that are always there. Framework adapters remove absent parts; web components keep the markup and bind or unbind its behavior as presence changes.

## Events

Use `press` for button activation. For other DOM events, use `on`. Here's a search field that clears on Escape:

```ts
input: slot({
  attrs: (state) => ({ type: "search", value: state.query }),
  on: {
    input: (event, { api }) => api.search((event.target as HTMLInputElement).value),
    keydown: (event, { api }) => {
      if (event.key !== "Escape") return
      event.preventDefault()
      api.search("")
    },
  },
}),
```

Each handler receives the event and the part's context: `el`, `id`, `key`, `props`, `store` and `api`. A handler passed by the component's user runs first. It can call `event.preventDefault()` to skip the primitive's handler.

## Roles, Focus and Keys

Declare each part's role on its slot, not in `attrs`. Core writes it, and a role that takes focus, activates or moves with keys makes the slot say how. Here's an option:

```ts
item: slot({
  role: "option",
  focus: "roving",
  press: ({ api, key }) => api.choose(key),
}),
```

Leave one out and TypeScript names it: an option without `focus` fails with "Property 'focus' is missing". The role decides what's required:

- **`focus`** for roles that take focus, like `option`, `tab` or `menuitem`: `"roving"`, `"tab-stop"`, `"active-descendant"` or `"native"`. Development builds check the bound element can take focus that way, like a `"native"` part that renders a `<div>`.
- **`press`** for roles that activate, like `button`, `option` or `tab`.
- **`keys`** for roles whose pattern moves with keys, like `listbox`, `menu` or `slider`: the keys its `on.keydown` handles, or a function of state when they depend on it.

```ts
list: slot({
  role: "listbox",
  keys: (state) => [...(state.orientation === "horizontal" ? ["ArrowLeft", "ArrowRight"] : ["ArrowUp", "ArrowDown"]), "Home", "End", "typeahead"],
  on: { keydown: (event, { api }) => api.navigate(event) },
}),
```

Listing keys without an `on.keydown` to handle them fails to compile too, on any slot. When another element meets a need, say which with `{ none: "why" }`, like `keys: { none: "The combobox's input handles them." }`. A role that changes with state is a function, with every role it can pick in `roles`:

```ts
root: slot({
  role: (state) => (state.decorative ? "none" : "separator"),
  roles: ["none", "separator"],
}),
```

The role decides the part's ARIA too. Its `attrs` returns the states the role can't work without, like `aria-checked` on a checkbox or `aria-selected` on an option, and nothing the role doesn't take: `aria-orientation` on a group fails with "aria-orientation isn't allowed on a group".

```ts
box: slot({
  role: "checkbox",
  focus: "native",
  press: ({ api }) => api.toggle(),
  attrs: (state) => ({ "aria-checked": String(state.checked) }),
}),
```

A state that `live` keeps current is `undefined` in `attrs`, so `attrs` still names it. Development builds warn about anything the types can't see, like a spread slot or JavaScript, and about a required state that renders without a value. Set `globalThis.__UI_SDK_STRICT__ = true` in your test setup to turn every warning into an error.

## Working with the Element

Use `bind` for work that needs an element, such as measuring it or attaching an observer. It runs in the browser once the part is bound. Return a cleanup function:

```ts
root: slot({
  attrs: () => ({ rows: "1" }),
  bind: (el) => {
    const fit = () => {
      el.style.height = "auto"
      el.style.height = `${el.scrollHeight}px`
    }
    fit()
    el.addEventListener("input", fit)
    return () => el.removeEventListener("input", fit)
  },
}),
```

This textarea measures its content after each input and adjusts its height.

Keep `attrs` and slot `state` pure: they also run on the server. Read from state and the part's props; do DOM work in `bind`. Slot `state` can return new objects and arrays on every call, since parts compare them by value, but other values, like a `Date` or a `Map`, should stay the same object until they change.

`bind` also receives the part as its second argument. Its `props` stay current, and `part.store.subscribe` lets you follow state changes. Use `connect` for work that must stay active while an element remains in the page, even when it isn't present, as with a hidden custom element.

### Cleaning up the Instance

Work shared by all parts belongs in `api`. Use `context.onDestroy` to clean it up when the instance is destroyed. For example, the copy button clears its reset timer:

```ts
let timer: ReturnType<typeof setTimeout> | undefined

context.onDestroy(() => clearTimeout(timer))
```

Clear the previous timer before starting another one, too.

## Reading Parts in the Api

Sometimes a method needs to find a part. `elements(context, slot)` returns the bound parts in page order, with their elements, keys and props:

```ts
import { elements } from "@ui-sdk/core"

api: (context) => ({
  next() {
    const fields = elements<{ value: string }>(context, "field")
    const empty = fields.find((field) => (field.el as HTMLInputElement).value === "")
    empty?.el.focus()
  },
}),
```

Try leaving a field empty. The button finds it from the current page order, so there's no separate index to keep up to date.

For one part, use `context.element(slot, key)`. To read its latest props, use `context.props(slot, key)`, naming them the same way: `context.props<{ title: string }>("window", key)`. The `key` is the part's `id` prop, or its `value`, as with the stars in [Compose Behaviors](/docs/core/compose-behaviors#add-a-star).

## What Agents Can Do

The `agent` key tells [the agent bridge](/docs/agents/connect-an-agent/reference#custom-primitives) which api methods an agent can call, and a little about the component, like whether its value must be one of its items:

```ts
agent: { actions: ["setValue", "clear"], closed: true },
```

Put it last, after `slots`, so TypeScript has read the api by then. Without it, the bridge picks methods named like changes, such as `setValue` or `toggle`. [Your Own Components](/docs/agents/connect-an-agent/your-components) shows a rating an agent can use, and every part of the key.

## Naming

A primitive's `name` prefixes each part's `data-slot`, like `tabs-list` for the tabs' `list` slot. The `root` slot gets the name alone, so `name: "copy-button"` gives its root `data-slot="copy-button"`.

Warnings and devtools use this name too. Set `label` when they should show a different name; it doesn't change `data-slot`.

For state attributes, `openAttrs(open)` returns `data-open` or `data-closed`. `checkedAttrs(checked)` returns `data-checked`, `data-unchecked` or `data-indeterminate`. Both are helpers from `@ui-sdk/core`.

## API Reference

### definePrimitive

- `name` (`string`): Prefixes `data-slot`, and names the component in warnings and the devtools. Kebab case. Example: [Naming](#naming).
- `state` (`(options: Options) => State`): The starting state, from the first options. Read `default<Key>` here. Example: [Define a Primitive](/docs/core/build-a-component#define-a-primitive).
- `live?` (`(keyof State)[]`): State keys that change often and never affect rendering, like a highlighted item. Example: [Live State](/docs/core/kernel/handle-changes#live-state).
- `asleep?` (`((options: object) => boolean)`): Whether an instance with these options starts asleep: its parts render and run `asleep`, while its api and each part's `bind` wait for the first event a part handles or the first read of `instance.api`. Like a closed tooltip, which costs little until its trigger is hovered or focused.
- `sync?` (`(options: Options) => Partial<State>`): Maps controlled options onto state. Runs whenever options change. Example: [State a User Controls](/docs/core/kernel/handle-changes#state-a-user-controls).
- `controlled?` (`(keyof State)[]`): State keys a user can control with `<key>`, `default<Key>` and `on<Key>Change`. Development builds warn when one starts with both `<key>` and `default<Key>`, or was controlled and becomes undefined. Defaults to whichever of `open`, `value` and `checked` the state has. Example: [State a User Controls](/docs/core/kernel/handle-changes#state-a-user-controls).
- `commits?` (`(keyof State)[]`): State keys an interaction changes in steps and then commits, through `createCommit`, like a slider's `value` while it's dragged. `observe` reports each of them once per commit rather than per step. Example: [Commits](/docs/core/kernel/handle-changes#commits).
- `api` (`(context: PrimitiveContext) => Api`): Runs once per instance and returns its api, which slots, adapters and a controller call. Example: [Define a Primitive](/docs/core/build-a-component#define-a-primitive).
- `slots` (`(slot: SlotFactory) => Record<string, SlotDefinition>`): The parts, built with `slot`. `slot<Props, State>({ ... })` types a slot's props and the state its render functions get. Example: [Define a Primitive](/docs/core/build-a-component#define-a-primitive).
- `label?` (`string`): What warnings, `debug` and the devtools call it, when that isn't `name`, which prefixes `data-slot`. Like a checkbox group's parts in `checkbox`. Example: [Naming](#naming).
- `agent?` (`object: actions, closed, items, reveals, shown, secret`): What `@ui-sdk/core/agent` offers an agent. Without it, the agent takes the api's methods that change something by their names, like `setValue`. Example: [What Agents Can Do](#what-agents-can-do).
- `options?` (`PropSchema<Options>`): Each option, with its kind, for renderers to read. A primitive without options leaves it out. Example: [Options](#options).

### Slots

- `present?` (`((state: State, context: { id: string; key: string; props: Props; api: Api; }) => boolean)`): Whether the slot's element should exist. Adapters that cannot unmount markup, like custom elements, bind and unbind on it. Defaults to always. Keyed parts, like one panel among many, decide from their own props. May read live state: adapters check presence on every change. Example: [Parts That Come and Go](#parts-that-come-and-go).
- `attrs?` (`((state: State, context: { id: string; key: string; props: Props; api: Api; }) => Attrs)`): Pure. Runs on the server and on every render. Never reads live state. Example: [Define a Primitive](/docs/core/build-a-component#define-a-primitive).
- `state?` (`((state: State, context: { id: string; key: string; props: Props; api: Api; }) => Public)`): What `render` functions and state-based class names see, like { highlighted, disabled }. May read live state: adapters only re-render a part for live changes when its render function actually read them. Example: [What a Part Shows](#what-a-part-shows).
- `live?` (`Live<State, Api, Props>`): Attributes written straight to the element on every change, built by `live()`. Example: [Live State](/docs/core/kernel/handle-changes#live-state).
- `direct?` (`{ names: readonly string[]; attach: (part: Part, el: HTMLElement) => () => void; }`): Attributes of `attrs` core keeps current on a framework's element, built by `direct()`. Example: [Live State](/docs/core/kernel/handle-changes#live-state).
- `selects?` (`Selects<State, Api, Props>`): Narrows which changes reach the slot's parts, built by `selects()`. Example: [Live State](/docs/core/kernel/handle-changes#live-state).
- `press?` (`(context: { el, id, key, props, store, api }, event?: Event) => void`): Activation: a click, which keys and screen readers send too. `{ none: "why" }` for a role that activates when another handler does it, like the author's own click. Example: [Define a Primitive](/docs/core/build-a-component#define-a-primitive).
- `on?` (`Partial<Record<keyof HTMLElementEventMap, (event: Event, context: { el, id, key, props, store, api }) => void>>`): DOM events. Adapters attach these so user handlers run first and can call preventDefault() to skip them. Example: [Events](#events).
- `bind?` (`((el: HTMLElement, part: Part<State, Api, Props>) => (() => void) | void)`): Client only, web only. Focus, positioning, observers. Gets the part, whose `props` are its newest render's. Return a cleanup. Follow state with `part.store.subscribe`, or write attributes that change often with `direct`. Example: [Working with the Element](#working-with-the-element).
- `asleep?` (`((el: HTMLElement, part: Part<State, Api, Props>) => (() => void) | void)`): Client only, web only. Like `bind`, but runs at once while the instance is asleep, for what a part can't wait for, like naming its trigger. Its `part.api` is undefined until the instance wakes.
- `connect?` (`((el: HTMLElement) => () => void)`): Client only. Runs while the element is in the page, present or not, for adapters that keep parts that aren't present, like custom elements. A popup's content registers here, so shortcuts inside a closed one don't run. Example: [Working with the Element](#working-with-the-element).
- `props?` (`PropSchema<Props>`): Each prop the slot's parts take, with its kind, like `{ value: "string" }`. Example: [Add a Star](/docs/core/compose-behaviors#add-a-star).

### Roles

- `role?` (`Role | ((state, context) => Role | undefined)`): The part's WAI-ARIA role, which core writes. A role that takes focus, activates or moves with keys makes the slot say how, with `focus`, `press` and `keys`. A function picks the role from state, with every role it can pick in `roles`. Never put `role` in `attrs`. Example: [Roles, Focus and Keys](#roles-focus-and-keys).
- `roles?` (`array`): Every role a `role` function can pick. Development builds check what each one needs. Example: [Roles, Focus and Keys](#roles-focus-and-keys).
- `focus?` (`Unneeded | ((state: State, context: { id: string; key: string; props: Props; api: Api; }) => Focus | Unneeded) | Focus`): How the part gets focus, for a role that takes it, like an option or a tab. `{ none: "why" }` when another element takes it instead. Example: [Roles, Focus and Keys](#roles-focus-and-keys).
- `keys?` (`HandledKey[] | ((state) => HandledKey[]) | { none: string }`): The keys the part's `on.keydown` handles, for a role whose pattern moves with keys, or a function of state when they depend on it, like the arrows of an orientation. `{ none: "why" }` when another element handles them, like a combobox's input for its listbox. Example: [Roles, Focus and Keys](#roles-focus-and-keys).

## Next

The copy button has everything it needs. Next, [Render a Primitive](/docs/core/kernel/render-a-primitive) puts it on the page in your framework.
