Two layers

How the headless package and the styled copy-paste layer divide the work.

Intentface chat is split the way Base UI and shadcn/ui are split, applied to chat: a headless npm package for behavior, and a copy-paste layer for looks.

The headless package

@intentface/chat ships the parts that are hard to get right and rarely need restyling:

  • the composer’s contenteditable editor engine, prefix-triggered command palette, fuzzy filtering, ask-user state machine, and attachment handling,
  • the thread's scroll subsystem — landing, auto-follow, composer-inset measurement,
  • message part segmentation, turn grouping, and the chip wire format,
  • the reasoning/steps disclosure logic and duration tracking.

These components render no styles of their own. They emit structure and expose behavior three ways: data-* attributes for state, context hooks (useComposer, useThread, useReasoning, …) for reading and driving, and render props / children for slotting your own elements in. Their only peer dependency is React.

The styled layer

Each styled component is a thin wrapper around a headless primitive: it adds Tailwind classes, icons, and motion, and re-exports the same API. You copy the source from the docs into your repo, and you own every class from then on. Because the wrapper only supplies presentation, restyling never risks the behavior — that stays in the package, and package updates fix behavior without touching your styles.

Why this split

One codebase, two audiences. Teams that want the Intentface look copy the styled layer and start immediately. Teams with their own design system depend on @intentface/chat directly and write their own thin wrappers — the behavior is identical either way.

The pages in this section cover the three seams that make the split work: the owned types the components read, the store model behind the composer, and the animation contract that keeps motion in the styled layer.