# Modular Ds Presentational Components

> Use when: building or evaluating flexible, composable Primer React parts that consumers assemble directly, or deciding whether a pattern is ready to become a config component. Covers structure-first composition, pairing presentational components with behavior hooks, data-component attributes, sub-component export conventions, and when to promote a pattern up the spectrum.

- Skill: `primer/modular-ds-presentational-components` (Agent Skill)
- Install (CLI): `npx skillmds@latest add primer/modular-ds-presentational-components`
- Raw SKILL.md: https://api.skillmd.com/api/skills/primer/modular-ds-presentational-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-presentational-components

---


# Modular DS — Presentational Components

Presentational components are styled pieces that consumers compose directly. Primer still owns the styling, accessibility expectations, data attributes, and component contracts for each piece — consumers control layout, ordering, conditional rendering, and content.

The sample below is **illustrative only** — `useList` and the `List` parts don't exist in the repo, and it's shorthand for the shape, not code to copy. Verify any hook you plan to use, per `modular-ds-utilities`.

```tsx
function Example({items}) {
  const [state, actions] = useList({defaultSelected: []})
  return (
    <List>
      {items.map(item => (
        <List.Item key={item.label} onClick={() => actions.toggleSelect(item.label)}>
          <List.ItemLeadingVisual>
            <List.ItemSelection selected={state.selected.has(item.label)} />
          </List.ItemLeadingVisual>
          <List.ItemLabel>{item.label}</List.ItemLabel>
        </List.Item>
      ))}
    </List>
  )
}
```

Shown above with dot-notation for readability — see "Sub-component export conventions" below for the actual RSC-safe export shape to ship.

## When to use presentational components

- The pattern is **emerging** — it's known to exist, but the right high-level config API hasn't stabilized yet.
- Consumers need more flexibility than a config component's props surface can reasonably expose (variants, custom ordering, conditional rendering).

Presentational components are usually the starting point for a new component area — default to building these first, then add behavior through hooks, and only add a config component later once patterns and defaults are established (see `modular-ds-config-components`).

## Behavior via hooks

State and interactions are usually provided separately through a behavior/state hook, letting consumers choose how much behavior to adopt. Keep behavior hooks internal (not part of the public API) unless the requested API or a clear consumer need requires making them public — see `modular-ds-utilities` for hook conventions.

## Composition rules

- Prefer ordinary React children over render props or `React.Children` + `React.cloneElement` for presentational composition. `cloneElement` in particular is fragile and breaks when consumers wrap children. Render props remain a legitimate extension point where a config component genuinely needs them (see `modular-ds-config-components` and `contributor-docs/style.md`) — this is a default, not a prohibition.
- Don't reach for the slots system by default. `useSlots` and `__SLOT__` markers are for the narrow case where a parent must identify a specific child part or extract a child out of the tree — not a general composition mechanism. Prefer plain children first, and only introduce slots when the requested API genuinely needs child extraction. See the `slots` skill for the mechanics if you do.
- Preserve consumer-authored child order. A presentational component should never reorder the children it's given — document the recommended structure instead.
- Use context (`use<Component>Context()`) for ARIA wiring between sub-components — never expose that context to consumers.
- Keep sub-components composable — don't bake one sub-component into another. For example, `Header` should accept `Title` and `CloseButton` as children rather than rendering `CloseButton` internally, so consumers control placement and omission.
- Use existing Primer components where appropriate (e.g. `Button`, `IconButton`, Octicons) instead of re-implementing native elements with custom styling. 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, don't pass opinionated layout or variant props through to that primitive unless the component's own API exposes the choice, or a concrete design reference requires it — otherwise you're hard-coding an appearance decision the consumer can't reach.
- Use CSS Modules (`.module.css`) with Primer design tokens for styling, and `clsx` for className merging.

## `data-component` attributes

All presentational parts must include `data-component` attributes for stable selectors (testing, agents):

- Root: `data-component="ComponentName"`
- Sub-components: `data-component="ComponentName.PartName"`

`data-component` is owned by Primer as a component identifier — it must never be exposed as a customizable public prop.

Don't stamp `data-component` onto a composed Primer component. An existing component such as `IconButton` sets its own value before spreading incoming props, so passing your own overwrites it, silently removing that component's identifier from the DOM — a breaking change to a stable selector under ADR-023, with no semver signal. Put your identifier on an element your component owns, or build on `ButtonBase` instead. ADR-023 doesn't currently rule on two Primer components claiming one element, so if you hit a case that genuinely needs it, surface it as a gap rather than resolving it inside a component PR.

Note that the `ComponentName.PartName` value and the flat export names above deliberately diverge: consumers write `<ToolbarSeparator>` while the DOM says `Toolbar.Separator`. ADR-023 asks the value to match the React API, and for a component using dot-notation it does. For a new component shipping flat exports it can't, and the dotted value is still the right one — it stays stable if the component later gains a composed object, and it groups the parts. Don't rename either to make them match.

`data-component` is identity, not a styling hook. Never write CSS that selects on it — least of all another component's, which couples your styles to markup that component is free to change. Per ADR-023 (`contributor-docs/adrs/adr-023-stable-selectors-api.md`), the DOM around a `data-component` element — its parent, children, siblings and attributes — is explicitly **not** public API, so a component may target its own parts but never reach into another component's. Values are PascalCase `ComponentName.PartName`, not camelCase. Wrap `data-*` state and ARIA state selectors in `:where()` so those parts add no specificity and don't outrank a base component's reset — see `modular-ds-base-components` for the ADR-021 caveat on this convention.

A `data-*` state attribute must mean the same thing on every part of a component that carries it. If a part needs a derived or inverted value — a separator in a horizontal toolbar being drawn vertically, say — give it a differently-named attribute rather than reusing the parent's under an opposite meaning. These attributes are the stable selector surface, so two parts one DOM level apart disagreeing about what `data-orientation` means is a trap for every consumer who writes a descendant selector.

## Sub-component export conventions

Flat exports (e.g. `DialogRoot`, `DialogHeader`, `DialogTitle`) are the goal for React Server Components compatibility — the `Object.assign` dot-notation pattern breaks in RSC (property access on a client reference returns `undefined`). Follow whichever convention existing components in the repo currently use for the area you're touching. If starting fresh, ship flat named exports only — add a composed `Object.assign` object solely to preserve an existing dot-notation API, never on a new component, where it buys nothing and adds an RSC trap to the public surface permanently:

```ts
// Flat exports (RSC-safe) — the default for anything new
export {Root as DialogRoot, Content as DialogContent, Header as DialogHeader, Title as DialogTitle}

// Composed export — only to preserve an existing dot-notation API, and it has to keep
// the name consumers already type (`Dialog`), or it preserves nothing.
export const Dialog = Object.assign(Root, {Content, Header, Title})
```

Base and presentational parts for the same component intentionally share `<Component><Part>` names across their different entry points — don't prefix or rename base parts to avoid the clash. A file importing both aliases one at the import site. Note this applies to exported **types** as well as components: `AccordionItemProps` will mean structurally different things depending on the entry point, and TypeScript error messages won't disambiguate them, so alias deliberately.

## Accessibility semantics

Keep markup and accessibility semantics flexible. Preserve native semantics, including heading structure, and expose presentational pieces when consumers need control over content, appearance, or semantics — via plain children composition, per the composition rules above. Match the accessibility pattern to the component contract — for established ARIA Authoring Practices Guide patterns (e.g. accordions), prefer the APG semantics and structure over ad hoc native-element defaults. See `modular-ds-accessibility-contract` for the full responsibility matrix across API types.

## Promoting to a config component

As a pattern (e.g. a filtering behavior layered on top of presentational parts) becomes common and well-understood, consider moving it up the spectrum into a config component. Until then, the presentational API is the supported path — don't force premature abstraction.

