Skip to main content

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.

Source facts

Repository
primer/react
Last source activity
July 28, 2026 at 16:23
Detected SKILL.md language
English
Stars
3,915
Forks
696

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
modular-ds-presentational-components
description
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.
# 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.
View on GitHub