# Modular Ds Base Components

> 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.

- Skill: `primer/modular-ds-base-components` (Agent Skill)
- Install (CLI): `npx skillmds@latest add primer/modular-ds-base-components`
- Raw SKILL.md: https://api.skillmd.com/api/skills/primer/modular-ds-base-components/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: primer (https://skillmd.com/u/primer)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/primer/modular-ds-base-components

---


# 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](https://base-ui.com/) or [Radix Primitives](https://www.radix-ui.com/primitives).

```tsx
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`.

