---
language: "en"
title: "Component workflow"
canonical: "https://uniaria.tenphi.me/contributing/component-workflow/"
source: "docs/contributing/component-workflow.md"
index: true
---

# Component workflow

A component is one task, built in small commits, in this order:

1. **Spec.** What the component does, approved by the owner before anything
   else.
2. **Conformance scenarios.** The tests every adapter must pass, written from
   the spec.
3. **Definition.** The behavior, iterated until the scenarios pass in both
   adapters.
4. **Adapters and docs.** The element entry, React or Vue sugar if it is
   needed, and the component's page with a live demo.

The binding version of this workflow is in the
[root `AGENTS.md`](https://github.com/tenphi/uniaria/blob/main/AGENTS.md)
(Component workflow, Conformance, Stop and ask the owner, Dependencies).

## 1. Spec

Copy
[`docs/_internal/component-spec-template.md`](https://github.com/tenphi/uniaria/blob/main/docs/_internal/component-spec-template.md)
to `packages/core/src/components/<name>/spec.md` and fill it in:

| Section                              | What it settles                                                                                                                                                           |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Summary                              | What the component is for, the native element underneath, the WAI-ARIA pattern, what it builds on (`ctx.use()`, `dom/`), its owner, and whether it gets a custom element. |
| Anatomy                              | The parts, their elements and the attributes UniAria owns on each. Element-owned parts, whose content UniAria writes, are marked.                                         |
| Props                                | Type, default, whether it is bindable and its change event.                                                                                                               |
| Public state                         | What `expose()` returns. Selectors read it, so it is public API: adding a field is a feature, changing one is a breaking change.                                          |
| Keyboard                             | Each key, where it applies, and what it does.                                                                                                                             |
| ARIA and state attributes per state  | The attributes on each part in each state.                                                                                                                                |
| Focus                                | Where focus goes on open, close, selection and dismissal, and what restores it.                                                                                           |
| Pointer and touch                    | Press behavior, press and hold, outside interaction, hover.                                                                                                               |
| Styling tokens                       | What the consumer's CSS can bind to: state attributes, CSS variables and public state. Tokens, never a look; styles UniAria writes only where a behavior needs them.      |
| Expected spoken output               | What the virtual screen reader should announce in the main flows. These become conformance checks.                                                                        |
| Deviations from the WAI-ARIA pattern | None, or each one with its reason.                                                                                                                                        |
| Open questions                       | Questions for the owner.                                                                                                                                                  |

Then:

* **Commit the spec on its own and ask the owner to review it.** Do not go
  further until the owner sets its status line to
  `Status: approved by @tenphi on <date>`. A spec with open questions is not
  approved; the owner's answers go under a Resolved questions section, which
  the code cites by number.
* **Complex components start from Cube UI Kit**, not React Aria: the
  overlays and collections (Dialog, Popover, Tooltip, Listbox, Select,
  Combobox). Cube UI Kit is UniAria's first consumer, and it replaced React
  Aria patterns that hurt its users: its ComboBox is its own implementation,
  and one click outside a popover acts. Read the component's source, tests
  and docs in `references/cube-ui-kit`, and list in the spec every place it
  differs from React Aria or the APG, with the reason. The owner decides each
  one. React Aria stays the reference for browser and assistive-technology
  edge cases.
* **Interactions** have a catalogue: before building or changing one in
  `dom/`, read its section of `docs/_internal/interaction-layer.md` (the
  behaviors, the browser bugs, where UniAria differs) and update the status
  there.

## 2. Conformance scenarios

Scenarios are written from the approved spec in `conformance/src/<name>/`, in
commits of their own:

* `types.ts` declares the suite's `Props` and `State` types from the spec,
  not from a definition, so the suite exists before the implementation does.
* `<name>.conformance.ts` defines the suite with
  `defineConformance<Props, State>('<name>', …)`, mirroring the spec's
  keyboard map and ARIA table, and including spoken-output checks with the
  virtual screen reader.
* `conformance/src/index.ts` exports the suite and adds it to `suites`, which
  every adapter's conformance run reads.

Every change under `conformance/` needs the owner's approval, and no commit
or pull request changes both `conformance/` and implementation under
`packages/*/src/`. Scenarios never import a definition or an adapter. See
[Testing](/contributing/testing/#conformance) for the mount contract, phases and the
checks CI runs.

## 3. Definition

The definition lives in `packages/core/src/components/<name>/`: the
definition module, an `index.ts` that exports it with its `<Name>Props` and
`<Name>State` types (`PropsOf` and `StateOf`), and its unit tests. Each
module starts with a JSDoc block stating its responsibility and the design
decision it implements.

* **Iterate until the scenarios pass in every adapter.** Never edit a
  scenario to make the implementation pass. If a scenario looks wrong, stop
  and describe the problem to the owner.
* **Compose, don't duplicate.** A composite component composes other
  definitions with `ctx.use()`; separate instances link through owners. See
  [Architecture](/contributing/architecture/#composition).
* **Register the subpath in three places:** an entry in
  `packages/core/tsdown.config.ts`, an `exports` entry in
  `packages/core/package.json` with the `uniaria-source` condition first,
  and a line in `packages/core/src/ssr-safety.test.ts`.
* **Make it known to every conformance run:** add the definition to the
  `definitions` maps in `packages/react/src/conformance-adapter.tsx` and
  `packages/vue/src/conformance-adapter.ts`, and import its element entry in
  `packages/elements/src/conformance-adapter.ts`.
  Each adapter's `conformance.browser.test.ts` runs suites up to the roadmap
  phase it declares; raise it as the roadmap advances.

## 4. Adapters and docs

* **Element entry.** `packages/elements/src/<name>.ts` calls
  `defineElement(definition)` with the default `uni-` prefix, exports the
  class and its element type, and declares the tag in
  `HTMLElementTagNameMap`. Like a core subpath it needs an entry in
  `tsdown.config.ts`, an `exports` entry and a line in
  `src/ssr-safety.test.ts`, plus a place in `sideEffects` in
  `packages/elements/package.json`, because importing it registers the
  element.
* **React or Vue sugar** only if it is needed. `@uniaria/react` and
  `@uniaria/vue` hold no component-specific behavior; sugar may only wrap
  `useBehavior` and `useBehaviorState`.
* **Docs.** A page in `docs/components/<name>.mdx` with a live demo, listed
  in the site's navigation. See [Documentation](/contributing/documentation/).

## Naming

| What                           | Rule                                        | Example                                                         |
| ------------------------------ | ------------------------------------------- | --------------------------------------------------------------- |
| Folder and subpath             | kebab-case                                  | `checkbox-group`, `@uniaria/core/checkbox-group`                |
| Definition export              | camelCase                                   | `checkboxGroup`                                                 |
| Definition `name`              | kebab-case                                  | `name: 'checkbox-group'`                                        |
| Element                        | `uni-` plus the name                        | `<uni-checkbox-group>`                                          |
| Root part's marker             | `data-uni` with the name                    | `data-uni="checkbox-group"`                                     |
| A part outside the root        | `data-uni-part` with the name and the part  | `data-uni-part="popover-trigger"`                               |
| Part names                     | camelCase; `data-element` capitalizes them  | `hiddenInput`, `data-element="HiddenInput"`                     |
| A name that is a reserved word | A descriptive export instead                | `toggleSwitch` for `name: 'switch'`                             |
| Booleans                       | Predicates                                  | `isDisabled`, `isOpen`, `shouldPreventFocusOnPress`, `hasArrow` |
| Events                         | camelCase; DOM lowercase; props `on` + name | `pressStart`, `pressstart`, `onPressStart`                      |
| Bindable change events         | `<name>Change`, without the `is` prefix     | `isOpen` → `openChange`, seeded by `defaultOpen`                |

* **Booleans read as predicates** everywhere, in public API and inside the
  code: `is…` for states; `should…`, `has…`, `allows…` or `can…` for
  behavior. That covers props, public state, options, variables, parameters
  and functions that return booleans. Names the DOM defines stay as they are
  (`shiftKey`, the `disabled` attribute).
* **The brand stays out of APIs.** Exports get descriptive names.
* **No near-duplicate components.** Button covers links (link mode with
  `href`). A Field plus a text control is `<Name>Field` (TextField,
  NumberField), and TextField covers `<textarea>` too.

## Stop and ask the owner

Before any of these, stop and ask the owner in the conversation:

* Starting a component without an approved spec.
* Any change to `conformance/`.
* Changing the runtime contract (`defineBehavior`, bindables, the store,
  part props) or the custom-element contract.
* Adding a dependency that isn't on the approved list.
* Changing the boundary rules in `eslint.config.js`, `tsconfig.core.json`,
  `types/host.d.ts`, `pnpm-workspace.yaml`, or anything in `scripts/` or
  `.github/`.
* Deviating from the WAI-ARIA Authoring Practices pattern for a component.
* Anything that contradicts `AGENTS.md`.

## Dependencies

Small utilities are built in-house; only large, domain-heavy packages are
reused. The approved runtime dependencies:

| Package                     | Used for                                                 |
| --------------------------- | -------------------------------------------------------- |
| `@internationalized/number` | Locale-aware number parsing and formatting (NumberField) |
| `@internationalized/date`   | Calendars and date math, for later date components       |
| `@tanstack/virtual-core`    | Virtualization, bundled into `@uniaria/core/virtual`     |

`@tanstack/virtual-core` is a development dependency, pinned to one exact
version and bundled into `@uniaria/core/virtual` with its development checks
compiled out. Upgrade it by hand once a newer release passes the 14-day
release-age policy and the virtualization scenarios.

The approved development dependencies for testing are
`@guidepup/virtual-screen-reader`, `@guidepup/guidepup`,
`@guidepup/playwright`, and `react-aria` in `@uniaria/react`, used only for
the comparison with React Aria. **Any other dependency needs the owner's
approval**, including small utility packages, Lit, state libraries
(nanostores, signals) and Zag.js packages.

The web platform has a policy too. Use only features that are Baseline
widely available. A newer feature is allowed only behind feature detection
with a fallback that keeps the behavior working, or behind a page-wide
setting the consumer opts into (`configure()`, such as CSS mode for
positioning). Check a feature with the `web-features` development
dependency:

```sh
node -e "import('web-features').then(({features: f}) => console.log(f['popover'].status))"
```

`baseline: 'high'` is widely available; `'low'` and `false` need the
fallback. A feature's `status.by_compat_key` covers its single APIs, which
can lag the feature.
