diff --git a/.github/agents/modular-ds-implementer.agent.md b/.github/agents/modular-ds-implementer.agent.md new file mode 100644 index 00000000000..78e58b74b15 --- /dev/null +++ b/.github/agents/modular-ds-implementer.agent.md @@ -0,0 +1,77 @@ +--- +name: modular-ds-implementer +description: Builds Primer React components using the modular design system patterns in the Primer style guide. +tools: + - read + - search + - edit + - execute +skills: + - style-guide +--- + +You are a Primer React implementer specializing in the modular design system +model. Before designing or changing a component, read +`contributor-docs/style.md` and align the implementation to its React component +guidance, especially the spectrum of abstraction model. + +Follow these rules: + +- Start with presentational components for new flexible component APIs. Use + behavior hooks to keep implementation logic reusable, but do not make those + hooks public unless the requested API or a clear consumer need requires it. +- Add config components over time when common use-cases and opinionated defaults + are established, and ensure they compose presentational components and hooks + instead of duplicating behavior. +- Use base components, shared hooks, utilities, and behaviors for accessibility + primitives and low-level behavior before creating custom one-off + implementations. +- Prefer existing base primitives over recreating native elements and their + reset styles. For example, use shared button primitives such as `ButtonBase` + when a component needs Primer-owned button semantics, interaction behavior, + and reset styling instead of hand-rolling a button reset in CSS. +- Consolidate accessibility primitives for established patterns, such as ARIA + Authoring Practices Guide patterns, instead of reimplementing them across + components. +- Match the accessibility pattern to the component contract. For established + ARIA Authoring Practices Guide patterns such as accordions, prefer the APG + semantics and structure expected by the pattern. Treat optional APG semantics + such as accordion panel `role="region"` as opt-in when the component can + render many panels or landmarks. +- Keep markup and accessibility semantics flexible. Preserve native semantics, + including heading structure, and expose presentational pieces or slots when + consumers need control over content, appearance, or semantics. +- Search for existing Primer components, hooks, utilities, and accessibility + primitives before adding new ones. +- Do not add slots, `useSlots`, or `__SLOT__` markers by default. Prefer normal + React composition for flexible presentational components. Only add slots when + the requested API explicitly needs child extraction or a parent component must + identify a specific child part. Do not reorder consumer-authored children in + presentational components; preserve author order and document recommended + structure instead. +- Do not expose public hooks for subcomponent internals unless there is a clear + consumer need. Internal hooks are fine when they keep behavior reusable without + expanding the public API. The style guide's recommendation to pair + presentational components with behavior hooks does not mean every internal + behavior hook should become a package export. +- Do not expose `data-component` as a customizable prop. Primer owns + `data-component` values as component identifiers. +- Avoid inventing visual styling without a concrete design reference, image, or + specification. If styling is not specified, keep styles minimal and structural + so the component API and accessibility model can be evaluated independently. +- Prefer `HTMLElement` for default root refs and polymorphic component typing. + Use narrower element types only when the API or behavior requires a specific + element. +- Include the surfaces needed for adoption: source exports, tests, stories, docs + metadata, and changesets when the published package behavior changes. +- New components and new public exports require a `minor` changeset for the + affected package. +- Public export changes must update any matching export snapshots or tests. +- When an accessibility pattern includes optional semantic tradeoffs, cover the + chosen default and opt-in behavior in tests, stories, or docs metadata. +- When using base primitives, avoid passing opinionated layout props unless the + component API exposes that choice or a concrete design reference requires it. + +When proposing or implementing work, explain which API type changed, why that +level of abstraction is appropriate, and how the implementation can be extended +without forking or overriding Primer internals. diff --git a/.github/agents/modular-ds-reviewer.agent.md b/.github/agents/modular-ds-reviewer.agent.md new file mode 100644 index 00000000000..6354d3b0cc5 --- /dev/null +++ b/.github/agents/modular-ds-reviewer.agent.md @@ -0,0 +1,68 @@ +--- +name: modular-ds-reviewer +description: Reviews Primer React component changes for alignment with the modular design system patterns in the Primer style guide. +tools: + - read + - search + - execute +skills: + - style-guide +--- + +You are a read-only reviewer for the modular design system model. Review the +current changes and determine whether they follow the patterns in +`contributor-docs/style.md`, especially the React component guidance and +spectrum of abstraction model. Never modify files. + +Check for: + +- Whether the change starts with presentational components and behavior hooks + before introducing config-driven APIs. +- Whether behavior hooks are kept internal unless a public hook is explicitly + requested or clearly justified by consumer needs. +- Whether config components compose presentational components and hooks instead + of duplicating state, behavior, or markup. +- Whether base components or existing utilities should be reused for + accessibility primitives, state, refs, timers, escape handling, or similar + cross-component behavior. +- Whether the change reuses existing base primitives, such as `ButtonBase`, when + it needs Primer-owned native element semantics, interaction behavior, or reset + styling instead of recreating those details with custom CSS. +- Whether accessibility primitives for established patterns, such as ARIA + Authoring Practices Guide patterns, are consolidated instead of reimplemented + across components. +- Whether established ARIA Authoring Practices Guide patterns use the expected + pattern semantics and structure instead of defaulting to native elements that + produce a different component model. +- Whether optional APG semantics, such as accordion panel `role="region"`, are + opt-in when defaulting them would create landmark proliferation or other + semantic side effects. +- Whether consumers can customize appearance, content, semantics, and behavior + without forking Primer or relying on overrides. +- Whether native accessibility semantics are preserved and low-level + accessibility behavior is reusable, including heading-first structures when a + heading labels an interactive control. +- Whether slots, `useSlots`, and `__SLOT__` markers are avoided by default and + only added when the requested API explicitly needs child extraction or a parent + must identify a specific child part. Flexible presentational components should + preserve consumer-authored child order rather than reordering children. +- Whether public hooks are justified by a clear consumer need instead of + exposing subcomponent internals by default, and whether any public hook has + docs metadata and tests matching its API. +- Whether `data-component` values are owned by Primer and not exposed as + customizable public props. +- Whether visual styling is supported by a concrete design reference, image, or + specification instead of being invented by the implementation. +- Whether root refs and element types default to `HTMLElement` unless a narrower + element type is required. +- Whether exports, stories, tests, docs metadata, and changesets match the public + API impact of the change, including a `minor` changeset for new components or + new public exports and updated export snapshots when public exports change. +- Whether base primitives are used without locking in opinionated layout unless + the component API exposes that choice or a concrete design reference requires + it. + +Report only actionable findings that show a concrete mismatch with the style +guide. For each finding, cite the file and line, name the relevant API type or +principle, explain the impact, and suggest the smallest design-level correction. +If the change follows the style guide, say so directly. diff --git a/.github/skills/style-guide/SKILL.md b/.github/skills/style-guide/SKILL.md new file mode 100644 index 00000000000..7d9fb9348f6 --- /dev/null +++ b/.github/skills/style-guide/SKILL.md @@ -0,0 +1,18 @@ +--- +name: style-guide +description: 'Use when: authoring, modifying, or reviewing Primer React components, hooks, utilities, or component APIs. Covers applying contributor-docs/style.md, including the spectrum of abstraction, presentational components, behavior hooks, config components, accessibility primitives, props, hooks, SSR, and CSS conventions.' +--- + +# Primer React Style Guide + +Use this skill when creating, modifying, or reviewing Primer React components, +hooks, utilities, or component APIs. + +Before authoring component code, read `contributor-docs/style.md` and apply the +guidelines that are relevant to the change. The style guide is the source of +truth for component API design and implementation conventions in this +repository. + +When proposing, implementing, or reviewing a component change, explicitly apply +the relevant style-guide principles to the design. If a change intentionally +deviates from the style guide, call out the reason. diff --git a/contributor-docs/adrs/adr-024-modular-ds.md b/contributor-docs/adrs/adr-024-modular-ds.md new file mode 100644 index 00000000000..718847c24ba --- /dev/null +++ b/contributor-docs/adrs/adr-024-modular-ds.md @@ -0,0 +1,324 @@ +# Modular Design System + +📆 Date: 2026-05-17 + +## Status + +| Stage | State | +| -------------- | ----------- | +| Status | Proposed ⚠️ | +| Implementation | Pending ⚠️ | + +## Context + +There is a proposal for enabling better pattern creation via modular building +blocks in: https://github.com/github/core-ux/issues/2238. In this proposal, it +details several layers that Primer could be structured into: + +- Layer 3: Ready-made, props-based components that are ready to use +- Layer 2: Parts, presentational-style components that one combines with hooks to + create components +- Layer 1: Foundations, unstyled primitives to allow for full markup and style + control +- Layer 0: Hooks, state management, and other logic that can be used to build components + +We took this proposal into a Modular Design System Workshop: +https://github.com/github/primer/discussions/6703. In this workshop, we +identified several scenarios where teams run into challenges when building with +Primer: + +- It can be difficult to extend or add new functionality to a component +- It can be difficult to customize the appearance, content, or semantics of a + component +- It can be difficult to build new experiences using established patterns (like accessibility primitives) + +These challenges lead to custom implementations, forks, or overrides of Primer +components that result in adoption challenges and ecosystem fragmentation. + +As a result, we applied the layer model to several situations in a +[FigJam](https://www.figma.com/board/mgApQG7vL3imdUIhgyhG2n/Modular-DS-Workshop?node-id=84-152&t=dBKm8jx1UsKRXyzu-1). From this work, we pulled out insights about this model that we want to adopt in Primer today. + +## Decision + +From the Modular Design System workshop, we identified several key areas that we +want to focus on in order to improve Primer: + +- Embrace layer 2 from the layer model, offering parts for our components that + can be combined with hooks for improved flexibility +- Provide low-level components and hooks to make it easier to build customized, + quality, accessible experiences +- Create a model for upstreaming work where people have already built on top of Primer + +To do this, we will: + +- Identify and deliver components that can be broken down into parts, which can be used to create more + flexible and extensible components +- Identify and deliver low-level components, hooks, and utilities that can be used to quickly build out accessible experiences +- Embrace the spectrum of abstraction model, authoring presentational components + that combine with hooks to provide flexible patterns and opinionated defaults. + +### Embracing the spectrum of abstraction model + +We will author components with a spectrum of abstraction in mind, specifically +beginning with presentational components that can combine with hooks to provide +opinionated, config-driven components. + +This model allows us to offer opinionated defaults that work for common +scenarios while still allowing teams to extend and customize components as needed. + +Default to building presentational components, then add behavior through hooks. +As opinions or defaults are established, create config-driven components that +combine presentational components and hooks to offer a higher level of +abstraction for common use-cases. + +#### Config components + +These components are "all-in-one"; they provide a high level of abstraction that +support common use-cases making it simple to quickly build out established +patterns and experiences. However, they are often inflexible and difficult to +extend. + +```tsx + { + /* ... */ + }} +/> +``` + +- High-level abstraction: consumers describe intent through props/data rather than composing markup directly. +- Opinionated defaults: Primer owns the default structure, behavior, accessibility, styling, and interaction patterns. +- Fast path for common use cases: optimized for teams that want to implement an established pattern quickly and correctly. +- Props-driven customization: extension happens through supported props, slots/render props, or configuration—not arbitrary internal composition. +- Limited flexibility by design: they should not try to support every variation; unusual needs may require dropping down to presentational parts. +- Stable product pattern: best suited for patterns Primer understands well and expects many teams to reuse. +- Integrated behavior: state management, keyboard behavior, selection, filtering, validation, etc. are usually bundled in. +- Clear escape boundary: when consumers need to change structure, semantics, or behavior beyond the config API, they should move to presentational components. + +#### Presentational components + +These components provide a lower level of abstraction and are more flexible and extensible, but require more work to build out common patterns and experiences. + +```tsx +function Example({items}) { + const [state, actions] = useList({ + defaultSelected: [], + }) + + return ( + + {items.map(item => { + return ( + { + actions.toggleSelect(item.label) + }} + > + + + + {item.label} + + ) + })} + + ) +} +``` + +Presentational components are accompanied by behavior or state hooks for a +component's core functionality. Oftentimes, config components are built by +combining presentational components with the corresponding behavior and state +hooks needed for a feature. + +These components are defined by: + +- Mid-level abstraction: consumers compose Primer-provided parts directly while Primer still owns styling and semantics for each part. +- Structure-first API: consumers control layout, ordering, conditional rendering, and content by assembling parts. +- Behavior via hooks: state and interactions are usually provided separately through hooks, letting teams choose how much behavior to adopt. +- Flexible composition: supports variants that config components cannot reasonably expose through props. +- Primer-owned building blocks: parts still encode design-system quality—styling, accessibility expectations, data attributes, and component contracts. +- Best for emerging patterns: useful when a pattern is known, but the right high-level API has not stabilized yet. +- Foundation for config components: config components should often be implemented by composing presentational parts plus behavior hooks. +- More consumer responsibility: consumers gain flexibility but must wire state, events, filtering, selection, and edge cases themselves + +#### Base components + +Base components are unstyled and may optionally provide behavior. These +components are fundamental primitives that are used to build components, such as +accessibility primitives. + +```tsx +function Example() { + return ( + + + + + + + + ) +} +``` + +Other examples include: + +- Combobox (filtering, selection) +- Listbox (selection) +- Popover +- Tabs +- Treeview + +#### Utilities + +There are core utilities, hooks, functions, etc that are commonly used when +building out components or React applications. We provide hooks for established +patterns so that teams can build on solid foundations when authoring new +experiences. These can include hooks such as `useMergedRefs`, +`useOnEscapePress`, `useTimeout` and more. + +These utilities may extend beyond hooks, such as `@primer/behaviors` or custom +elements, where appropriate. + +#### Layered example + +When using this model, the config component should cover the default version of +a pattern, while presentational components let teams extend the pattern when +they need behavior that is not yet part of the high-level API. + +For example, we may have a `List` config component that supports selection as +the default interaction: + +```tsx + { + /* ... */ + }} +/> +``` + +If a team needs to add filtering, they can build on the presentational `List` +parts and a lower-level behavior hook instead of forking the config component or +asking the config API to support every possible variation. For example: + +```tsx +function FilterableList({items}) { + const { getInputProps, getListboxProps, getOptionProps } = useCombobox({ + items, + getItemLabel: item => item.label, + selectionMode: 'multiple', + }) + + return ( + + + + {combobox.items.map(item => { + return ( + + + + + {item.label} + + ) + })} + + + ) +} +``` + +The important part is that the `List` parts continue to provide the selected +item structure while the team layers filtering on top of it. If that filtering +control needs richer accessibility or interaction behavior, the team can keep +using the same `List` parts while moving more of the interaction model into +lower-level primitives or hooks. + +As this filtering behavior becomes common and well-understood, we could decide +to move it up the spectrum by adding it to the config component. Until then, the +presentational API gives teams a supported path to compose the experience +without relying on internal implementation details. + +## Consequences + +This approach is a consolidation of our existing approach to building +components. It highlights changes that need to happen in existing components, +namely SelectPanel and Dialog, that would require changes in order for them to +accommodate the spectrum of abstraction model. + +This decision also impacts future component development as components must start +with this in mind. One critical part to this is that authoring components as +parts makes migration over time more difficult. For example, consider an +existing Card component implementation: + +```tsx + + Title + Body + Footer + +``` + +Over time, there may be a request to add support for a `Card.Action`. This may +require us to add a `Card.Header` part to the Card component. + +```tsx + + + Title + Edit + + Body + Footer + +``` + +As a result, we would need to manage how we support this kind of breaking +change. We could coordinate migrating existing implementations to +the new structure as a part of the change. We could also consider supporting +both structures which would increase the surface area of the component. + +## Alternatives + +The main alternatives to this approach would be: + +- Do nothing, continue our current approach which is causing the challenges + mentioned at the beginning of the ADR +- Fully adopt the layer model as proposed + +Most likely we'll never find a model that will exactly work across components. +As a result, we should develop techniques and learn how to apply them as the +design system grows. + +## Questions + +### Will this be the de facto standard for all components in the Design System going forward? + +Yes, our team will prioritize authoring components as presentational components +with hooks as the starting point for new components. As patterns and opinions +emerge, we create config components to simplify adoption of those patterns. + +### Does this only apply to certain type of components? + +Not every component will need to be authored with config, presentational, and +base components in mind. Not all components will need their own hooks, either. +Generally, when writing a component we will want to start with the +presentational components first. We'll focus on markup and styles before adding +in behavior and state. For some components, this can be baked into the +component. For others, they will need to be extracted into hooks. + +There is no clear test for this yet, but one concept that seems to work is when +state does not have a clear component that owns it. For example, if we have a +`List` component and we want to manage selection then we have two options: + +- Bake it into the `List` component (this would be our config approach) +- Provide a `useSelection` hook (or bake selection into a `useList` hook) that + can then be used with `List` components to provide selection diff --git a/contributor-docs/style.md b/contributor-docs/style.md index d15375bd5bf..42b67a5defb 100644 --- a/contributor-docs/style.md +++ b/contributor-docs/style.md @@ -61,6 +61,11 @@ row before the line. - [Server-side rendering](#server-side-rendering) - [Prefer managing focus through event handlers instead of effects](#prefer-managing-focus-through-event-handlers-instead-of-effects) - [Prefer the `useControllableState` hook when authoring components that can be controlled or uncontrolled](#prefer-the-usecontrollablestate-hook-when-authoring-components-that-can-be-controlled-or-uncontrolled) + - [Prefer building components across a spectrum of abstraction](#prefer-building-components-across-a-spectrum-of-abstraction) + - [Config components](#config-components) + - [Presentational components](#presentational-components) + - [Base components](#base-components) + - [Utilities](#utilities) - [Props](#props) - [Prefer applying component rest parameters to the root element rendered by a component](#prefer-applying-component-rest-parameters-to-the-root-element-rendered-by-a-component) - [Prefer authoring callback prop types with arguments that can be extended](#prefer-authoring-callback-prop-types-with-arguments-that-can-be-extended) @@ -152,6 +157,80 @@ function Example({defaultValue, onChange, value: controlledValue}: ExampleProps) } ``` +### Prefer building components across a spectrum of abstraction + +When authoring Primer components, prefer starting with flexible presentational +components and companion behavior hooks. As opinions or defaults become +established, create config components that compose those presentational +components and hooks instead of duplicating their behavior. This keeps common +patterns easy to adopt while still giving consumers a clear path when they need +to customize appearance, content, semantics, or behavior. + +Use the following API types when designing component APIs: + +| API type | Use for | +| ------------------------- | ----------------------------------------------------------------------------------- | +| Config components | Ready-made, props-based components for stable product patterns and common use-cases | +| Presentational components | Styled pieces that consumers compose directly, often with companion behavior hooks | +| Base components | Unstyled primitives, often for accessibility structure or low-level behavior | +| Utilities | Hooks, state management, behaviors, and functions used to build components | + +#### Config components + +Config components are "all-in-one" APIs that let consumers describe intent +through props or data rather than composing markup directly. They should provide +opinionated defaults for structure, behavior, accessibility, styling, and +interactions so teams can implement established patterns quickly and correctly. + +Use config components when Primer understands the pattern well and expects many +teams to reuse it. Keep the supported customization surface explicit through +props, slots, render props, or configuration. Do not make config components +support every variation; if consumers need to change structure, semantics, or +behavior beyond the config API, they should be able to drop down to +presentational components and behavior hooks. + +#### Presentational components + +Presentational components are styled pieces that consumers compose directly. +They should let consumers control layout, ordering, conditional rendering, and +content while Primer still owns the styling, accessibility expectations, data +attributes, and component contracts for each piece. + +Prefer presentational components and behavior hooks for emerging patterns or +flexible APIs where a single high-level config API has not stabilized. Build +config components over time by composing those components and hooks after common +use-cases and defaults become clear. + +Do not add slots by default. Prefer normal React composition first, and only +introduce slots when a parent component must identify a specific child part or +the requested API explicitly needs child extraction. Do not reorder +consumer-authored children in flexible presentational components just to enforce +a preferred structure; document the recommended structure or add a dev warning +instead. + +#### Base components + +Base components are unstyled primitives used to build higher-level components. +Use them for accessibility primitives and low-level behaviors that need full +markup and style control. Accessibility primitives for established patterns, +such as ARIA Authoring Practices Guide patterns, should be consolidated and +reused rather than reimplemented across components. Before adding custom +behavior to a component, look for an existing base component, hook, utility, or +behavior that can provide the foundation. + +Prefer existing base primitives over recreating native elements and their reset +styles. For example, use shared button primitives such as `ButtonBase` when a +new component needs Primer-owned button semantics, interaction behavior, and +reset styling instead of hand-rolling a button with custom reset CSS. + +#### Utilities + +Utilities include hooks, functions, behaviors, and other reusable logic. Provide +utilities for established patterns so teams can build accessible experiences on +solid foundations. Examples include hooks such as `useMergedRefs`, +`useOnEscapePress`, and `useTimeout`, or lower-level packages such as +`@primer/behaviors`. + ### Props #### Prefer applying component rest parameters to the root element rendered by a component