Composable Components
Design the public composition contract before arranging files. A namespace and
Root / Trigger / Content names do not make an API composable: consumers
must be able to use the advertised parts without the implementation silently
replacing, ignoring, or trapping them behind fixed defaults.
This skill is self-contained. Do not require another skill to make the API,
state, rendering, accessibility, or layout decisions described here.
1. Classify behavior and rendering ownership
| Kind |
Signals |
Public shape |
Contract |
| Primitive |
reusable behavior, asChild, dialog/select/accordion anatomy |
Root plus semantic parts such as Trigger, Content, Portal, Overlay, Title, Description, Close |
Primitive contract |
| Product |
domain data, feature variants, mode-prop growth, local or synced state |
Provider + Frame when the state scope differs from the visual frame; otherwise Root plus domain parts |
Product contract |
Installing or merely assembling already-authored component packages is outside
scope. This skill is for authoring or refactoring component APIs.
A feature may contain both: a primitive in components/ui/dialog/ and a
product composer in features/composer/ that consumes it. Keep their ownership
boundaries distinct even when they live near each other.
Separately decide who owns repeated data:
- If the consumer owns iteration, it maps explicit
Item parts.
- If the component must filter, group, virtualize, or otherwise supply each
item, expose one render-function or function-children boundary with the item
and stable public state. Do not hardcode a private
Item beneath a public
Results or List and then claim that exporting Item makes it replaceable.
2. House decisions (do not mix)
Read house-rules.md before writing code. It contains the full
composition guidance needed by this skill. Defaults:
- Detect React from the project. 19+:
ref is a prop, use(), no forwardRef. 18: forwardRef + useContext.
- One production caller is neither evidence for nor against a good abstraction.
Derive seams from real responsibilities and known variation axes, and do not
wait for a second caller before honoring a public component contract.
- Every public host-rendering part accepts its host's props, forwards its ref,
and deliberately composes internal and consumer events, classes, and styles.
- Every exported part is reachable in normal composition or replaceable through
an explicit render boundary. Do not export decorative customization points.
- Coordinate parts through context and explicit slots, not
child.type
inspection, first-match selection, or silent child dropping.
- When the API chooses
asChild, use @radix-ui/react-slot, never an ad hoc
cloneElement merger.
- Context has no silent default. Hooks throw outside the owner.
- No boolean props for layout or product mode. Compose explicit variants
instead; intrinsic binary state such as
disabled, open, and asChild is
still valid.
- Prefer
children over renderX. Render props only when the parent must pass item data back.
- Primitive state:
value / defaultValue / onValueChange (or open / onOpenChange) via controllable state.
- Product state: only providers know the store implementation. Parts depend on
a typed
state / actions / meta context contract.
- Primitive behavior includes semantics, keyboard interaction, focus, and ARIA;
data-* attributes expose styling hooks but never replace accessibility.
3. File-type folders
One kebab-case folder per component. Underscore directories are private
implementation (not a public import path; not an App Router route).
Create a _ folder only when it has files. Never leave empties. Never invent
typos (_ultils). index.ts at the component root is the only public
barrel.
| Folder |
Put here |
_components/ |
React parts, one file per part |
_types/ |
shared types and public prop types |
_constants/ |
literals, keys, data-state unions, selectors |
_hooks/ |
context hook and local behavior hooks |
_utils/ |
pure generic helpers (no feature types) |
_helpers/ |
feature-specific non-hook functions |
_lib/ |
third-party / browser adapters only |
_styles/ |
cva, recipes, CSS modules |
_providers/ |
product state providers that implement the context contract |
_icons/ _assets/ _mocks/ _tests/ _stories/ |
only when that artifact exists |
Full taxonomy, naming, and trees: folder-layout.md.
4. Authoring workflow
Task progress:
- [ ] Kind is primitive or product (or split into two folders)
- [ ] Data iteration is consumer-owned or has an explicit render boundary
- [ ] Public seams follow responsibilities, not the number of current callers
- [ ] React baseline detected from package.json
- [ ] Folder exists; only needed _* directories created
- [ ] Types and constants extracted before parts grow
- [ ] Context hook throws outside owner
- [ ] Parts live in _components; barrel re-exports a namespace
- [ ] Every host part forwards native props/ref and composes events/class/style
- [ ] Every exported part is actually reachable, replaceable, or intentionally a default facade
- [ ] No child.type scanning, first-child-only selection, or silent child dropping
- [ ] Primitive semantics, asChild, data-state, focus, and keyboard behavior are covered
- [ ] Product parts know the context contract, not the store implementation
- [ ] No boolean mode props; explicit variants compose parts
- [ ] External imports go through index.ts only
Done when: the advertised composition works with at least one meaningful
alternative arrangement or renderer, public parts preserve their host contract,
internals are in the needed _ folders, house rules hold, and unused _
folders do not exist. A namespace object by itself is not completion.
5. Scope boundaries
- Do not expand this into design tokens, npm publishing, registries,
marketplaces, or component documentation workflows.
- Do not add generic React-version guidance beyond the local baseline rule.
- Do not recreate full Radix internals such as
DismissableLayer, Presence,
or createContextScope. If same-primitive nesting requires extra isolation,
use an existing project primitive or a narrowly owned local scope instead of
inventing another primitive library.
1---2name: composable-components3description: Authors or refactors accessible primitive and product compound React components with genuinely configurable subparts, transparent host props and refs, data-rendering boundaries, state ownership, asChild/Slot behavior, and private underscore file-type folders. Use for compound component APIs, composable UI primitives, product composers, polymorphic parts, or their component-local _components/_types/_helpers/_utils layout.4---56# Composable Components78Design the public composition contract before arranging files. A namespace and9`Root` / `Trigger` / `Content` names do not make an API composable: consumers10must be able to use the advertised parts without the implementation silently11replacing, ignoring, or trapping them behind fixed defaults.1213This skill is self-contained. Do not require another skill to make the API,14state, rendering, accessibility, or layout decisions described here.1516## 1. Classify behavior and rendering ownership1718| Kind | Signals | Public shape | Contract |19| --- | --- | --- | --- |20| **Primitive** | reusable behavior, `asChild`, dialog/select/accordion anatomy | `Root` plus semantic parts such as `Trigger`, `Content`, `Portal`, `Overlay`, `Title`, `Description`, `Close` | [Primitive contract](house-rules.md#primitive-contract) |21| **Product** | domain data, feature variants, mode-prop growth, local or synced state | `Provider` + `Frame` when the state scope differs from the visual frame; otherwise `Root` plus domain parts | [Product contract](house-rules.md#product-contract) |2223Installing or merely assembling already-authored component packages is outside24scope. This skill is for **authoring or refactoring** component APIs.2526A feature may contain **both**: a primitive in `components/ui/dialog/` and a27product composer in `features/composer/` that consumes it. Keep their ownership28boundaries distinct even when they live near each other.2930Separately decide who owns repeated data:3132- If the consumer owns iteration, it maps explicit `Item` parts.33- If the component must filter, group, virtualize, or otherwise supply each34 item, expose one render-function or function-children boundary with the item35 and stable public state. Do not hardcode a private `Item` beneath a public36 `Results` or `List` and then claim that exporting `Item` makes it replaceable.3738## 2. House decisions (do not mix)3940Read [house-rules.md](house-rules.md) before writing code. It contains the full41composition guidance needed by this skill. Defaults:4243- Detect React from the project. 19+: `ref` is a prop, `use()`, no `forwardRef`. 18: `forwardRef` + `useContext`.44- One production caller is neither evidence for nor against a good abstraction.45 Derive seams from real responsibilities and known variation axes, and do not46 wait for a second caller before honoring a public component contract.47- Every public host-rendering part accepts its host's props, forwards its ref,48 and deliberately composes internal and consumer events, classes, and styles.49- Every exported part is reachable in normal composition or replaceable through50 an explicit render boundary. Do not export decorative customization points.51- Coordinate parts through context and explicit slots, not `child.type`52 inspection, first-match selection, or silent child dropping.53- When the API chooses `asChild`, use `@radix-ui/react-slot`, never an ad hoc54 `cloneElement` merger.55- Context has **no silent default**. Hooks throw outside the owner.56- No boolean props for layout or product mode. Compose explicit variants57 instead; intrinsic binary state such as `disabled`, `open`, and `asChild` is58 still valid.59- Prefer `children` over `renderX`. Render props only when the parent must pass item data back.60- Primitive state: `value` / `defaultValue` / `onValueChange` (or `open` / `onOpenChange`) via controllable state.61- Product state: only providers know the store implementation. Parts depend on62 a typed `state` / `actions` / `meta` context contract.63- Primitive behavior includes semantics, keyboard interaction, focus, and ARIA;64 `data-*` attributes expose styling hooks but never replace accessibility.6566## 3. File-type folders6768One kebab-case folder per component. Underscore directories are **private69implementation** (not a public import path; not an App Router route).7071Create a `_` folder only when it has files. Never leave empties. Never invent72typos (`_ultils`). `index.ts` at the component root is the **only** public73barrel.7475| Folder | Put here |76| --- | --- |77| `_components/` | React parts, one file per part |78| `_types/` | shared types and public prop types |79| `_constants/` | literals, keys, `data-state` unions, selectors |80| `_hooks/` | context hook and local behavior hooks |81| `_utils/` | pure generic helpers (no feature types) |82| `_helpers/` | feature-specific non-hook functions |83| `_lib/` | third-party / browser adapters only |84| `_styles/` | cva, recipes, CSS modules |85| `_providers/` | product state providers that implement the context contract |86| `_icons/` `_assets/` `_mocks/` `_tests/` `_stories/` | only when that artifact exists |8788Full taxonomy, naming, and trees: [folder-layout.md](folder-layout.md).8990## 4. Authoring workflow9192```93Task progress:94- [ ] Kind is primitive or product (or split into two folders)95- [ ] Data iteration is consumer-owned or has an explicit render boundary96- [ ] Public seams follow responsibilities, not the number of current callers97- [ ] React baseline detected from package.json98- [ ] Folder exists; only needed _* directories created99- [ ] Types and constants extracted before parts grow100- [ ] Context hook throws outside owner101- [ ] Parts live in _components; barrel re-exports a namespace102- [ ] Every host part forwards native props/ref and composes events/class/style103- [ ] Every exported part is actually reachable, replaceable, or intentionally a default facade104- [ ] No child.type scanning, first-child-only selection, or silent child dropping105- [ ] Primitive semantics, asChild, data-state, focus, and keyboard behavior are covered106- [ ] Product parts know the context contract, not the store implementation107- [ ] No boolean mode props; explicit variants compose parts108- [ ] External imports go through index.ts only109```110111**Done when:** the advertised composition works with at least one meaningful112alternative arrangement or renderer, public parts preserve their host contract,113internals are in the needed `_` folders, house rules hold, and unused `_`114folders do not exist. A namespace object by itself is not completion.115116## 5. Scope boundaries117118- Do not expand this into design tokens, npm publishing, registries,119 marketplaces, or component documentation workflows.120- Do not add generic React-version guidance beyond the local baseline rule.121- Do not recreate full Radix internals such as `DismissableLayer`, `Presence`,122 or `createContextScope`. If same-primitive nesting requires extra isolation,123 use an existing project primitive or a narrowly owned local scope instead of124 inventing another primitive library.