| name | modular-ds-base-components |
| description | Use when: building or evaluating unstyled Primer React primitives, especially accessibility primitives or low-level behavior that other components should build on. Covers when to create a base component instead of baking behavior into a styled component, the unstyled CSS-reset convention, deciding which parts need a base equivalent, and consolidating ARIA Authoring Practices Guide patterns. |
Modular DS — Base Components
Base components are unstyled primitives used to build higher-level components. They carry no visual styling and enforce structural accessibility constraints, similar in spirit to Base UI or Radix Primitives.
function Example() {
return (
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Close />
</Dialog.Content>
</Dialog.Root>
)
}
Other examples of things suited to base components: Combobox (filtering, selection), Listbox (selection), Popover, Tabs, Treeview.
Shown above with dot-notation for readability — ship flat named exports per the RSC-safe convention in modular-ds-presentational-components, not an Object.assign composed export.
When to use a base component
Use base components for accessibility primitives and low-level behaviors that need full markup and style control. Before adding custom behavior to a component, look for an existing base component, hook, or utility that can already provide the foundation — don't reimplement it.
Prefer existing base primitives over recreating native elements and their reset styles. Where a component needs Primer-owned button semantics, interaction behavior, and reset styling, build on a shared primitive such as ButtonBase rather than hand-rolling a button reset in CSS. When you do build on a base primitive, don't pass opinionated layout props through to it unless the component's own API deliberately exposes that choice — the primitive should stay visually neutral.
Accessibility primitives for established patterns (e.g. ARIA Authoring Practices Guide patterns) should be consolidated and reused rather than reimplemented across components. If you find yourself re-solving a pattern that already has a base component elsewhere in the repo, use it instead of writing a parallel implementation.
What a base component covers
- Structural accessibility constraints (e.g. a title must be a descendant of a dialog).
- ARIA wiring via internal context, so consumers get correct semantics "for free" while retaining full markup control.
- No visual opinion at all — presentational components (see
modular-ds-presentational-components) build on top and add Primer's design tokens and layout.
Deciding which parts need a base equivalent
Not every presentational sub-part needs a base component. A base primitive earns its place when there's accessibility behavior or interactivity tied to it (e.g. a dialog root, a close control, an overlay). Purely structural parts — a label, a heading, a message wrapper — usually don't need one, since consumers can render their own markup and the surrounding base components continue to wire ARIA correctly via context.
When it's not obvious whether a given part warrants its own base component, surface the decision explicitly rather than assuming.
CSS reset convention
Where a base component needs to remove browser defaults (e.g. native <dialog>/<button> styling) without adding visual opinion, ship a minimal CSS reset using :where() selectors so the state part of the selector contributes no specificity and consumer/presentational styles always win. Note .Component:where([data-variant='x']) is 0,1,0 overall, not zero — the class still counts; it's the wrapped part that stops escalating. ADR-021 proposes replacing this convention with CSS layers (primer.components.<component-name>, per ADR-022), but both are still 🚧 and a single .module.css file in the package uses @layer today, against dozens still using :where(). Follow :where() for now, and expect these files to be retrofitted if ADR-021 is adopted.
Relationship to hooks
Base components wrap the compound behavior hook for their component (see modular-ds-utilities) — they are the thin, JSX-shaped API most consumers reach for, while the hook itself stays available directly for the rarer "I need full markup control, no wrapping component at all" case. Both are first-class; a base component should not duplicate behavior that already lives in its hook. Where the hook wraps a native element with its own built-in behavior, follow the controlled component contract in modular-ds-utilities.