Skip to content

Headless

@arun-dev/headless ships behaviour, keyboard handling and accessibility, and nothing else — no CSS, no class names, no colour. Components project their state onto the DOM as data-* attributes, so any styling approach reads them: plain CSS, CSS modules, utility classes, or a component library of your own.

Terminal window
npm install @arun-dev/headless

Peer dependencies: react >= 19, react-dom >= 19. Nothing else — it has no runtime dependencies and loads no stylesheet.

flowchart TB
  subgraph behaviour["@arun-dev/headless"]
    direction TB
    B1["Button · Switch"]
    B2["useRender · mergeProps · useControlled"]
  end

  subgraph tokens["@arun-dev/tokens"]
    T1["CSS custom properties"]
  end

  ui["@arun-dev/ui<br/>class names + component CSS"]

  behaviour -- "data-* attribute names" --> ui
  tokens -- "custom property names" --> ui

  classDef pkg fill:transparent,stroke-dasharray:4 3
  class behaviour,tokens pkg
Three independent packages. The arrows are name contracts, not imports — each is checked by a test.

Nothing in @arun-dev/headless knows that @arun-dev/ui exists. The only thing the two agree on is the spelling of the data-* attributes, which is why you can put your own styling layer in that position instead.

render lets you replace the element while keeping the behaviour, so a component never has to guess which tag or which library you need.

data-* attributes carry state out to CSS, so styling reacts to checked or disabled without the component owning a single class name.

Everything else on these pages is a consequence of those two.

Switch.Root and Switch.Thumb are the two parts of Switch. A part is a component in its own right: it renders one element and takes its own className, ref and render.

<Switch.Root>
<Switch.Thumb />
</Switch.Root>

Parts are not independent. The root owns the state and shares it through context, so they cannot disagree about whether the switch is checked, and you cannot set one without the other following.

The word is used in that sense throughout these pages. A component with nothing to compose — Button — is a single part, and you write it as one element.

One styling layer built on these primitives is @arun-dev/ui, which uses @arun-dev/tokens for its colours. The two packages are independent — take whichever fits, and use both if you want the styled components in most places and raw behaviour where you need something custom.