Public Wiki18,526View on GitHub
Want radix-ui/primitives knowledge in your AI?

React

This is the Radix UI Primitives monorepo — a comprehensive library of unstyled, accessible React UI components published as individual packages under the @radix-ui scope. It provides low-level building blocks (primitives) like Dialog, Menu, Popover, Tooltip, Select, Accordion, Tabs, Slider, and more, designed to be composed and styled by consumers. The monorepo also includes an umbrella `radix-ui` aggregate package that re-exports all components.

Tech Stack

ReactNext.jsVitest

Key Features

  • Dialog & Alert Dialog
  • Menu System
  • Popup Components
  • Overlay Infrastructure
  • Form Components
  • Layout & Navigation
  • React Foundation Primitives
  • Aggregate Package
  • Cypress E2E Testing
  • SSR Smoke Testing

Entry Points

Start here when working with this codebase

Build Pipeline Entry
internal/builder/radix-build.js

CLI entry point that triggers the build orchestration for all Radix UI component packages

Start here when debugging build issues, modifying the build process, or understanding how packages are compiled

Build Orchestrator
internal/builder/builder.js

Core build logic that handles compilation, bundling, and output for each package

Start here when you need to change how packages are bundled, add new build targets, or modify compilation settings

Test Setup
scripts/setup-tests.ts

Initializes Vitest environment with matchers and global polyfills for all unit tests

Start here when adding new test utilities, custom matchers, or global test configuration

Cypress E2E Config
cypress.config.ts

Configures Cypress e2e testing framework with project-specific settings

Start here when adding new Cypress plugins, changing test viewport, or modifying e2e test infrastructure

Cypress E2E Support
cypress/support/e2e.js

Global test configuration and custom command registration for all Cypress tests

Start here when adding custom Cypress commands or global test hooks

SSR Testing App Layout
apps/ssr-testing/app/layout.tsx

Root layout for the Next.js SSR testing app providing navigation and page structure

Start here when adding a new component's SSR smoke test page or modifying SSR test navigation

SSR Testing App Config
apps/ssr-testing/next.config.js

Next.js build and runtime configuration for the SSR testing application

Start here when SSR tests need new Next.js features or transpilation settings

Storybook ESLint Config
apps/storybook/eslint.config.js

ESLint configuration for the Storybook application combining shared and plugin-specific rules

Start here when adding Storybook-specific linting rules

Global ESLint Config
eslint.config.mjs

Root ESLint configuration for the entire monorepo

Start here when changing linting rules that apply across all packages

Shared ESLint Config Package
internal/eslint-config/index.js

Centralized reusable ESLint configuration imported by all packages in the monorepo

Start here when modifying shared linting rules that propagate to all packages

Global Type Definitions
types/global.d.ts

Global TypeScript type definitions and module augmentation used across the monorepo

Start here when adding global type declarations or augmenting existing module types

How to Make Changes

Common modification patterns for this codebase

Add a new React component package

1
packages/react/{component-name}/src/{component-name}.tsx

Create the main component source file with the component implementation. Export the component and its props type. Use React.forwardRef for the root element.

Pattern: Follow the structure in any existing component package (e.g., packages/react/accessible-icon or packages/react/avatar) — each has a src/ directory with a single main .tsx file named after the component

2
packages/react/{component-name}/src/index.ts

Create a barrel export file that re-exports everything from the main component file using 'export { ComponentName, type ComponentNameProps } from "./{component-name}"'

Pattern: See how packages/react/accessible-icon/src/index.ts re-exports from its main component file

3
packages/react/{component-name}/package.json

Create package.json with name '@radix-ui/react-{component-name}', define 'source', 'main', 'module', and 'types' fields pointing to src/index.ts and dist/ outputs. Add peerDependencies on react and react-dom. Add dependencies on any @radix-ui primitives used (e.g., @radix-ui/react-primitive, @radix-ui/react-use-controllable-state).

Pattern: Copy the package.json structure from packages/react/avatar/package.json — it shows the standard field layout, scripts, and dependency declarations

4
packages/react/{component-name}/tsconfig.json

Create a tsconfig.json that extends the shared base TypeScript config and sets compilerOptions and include paths for the src directory

Pattern: Copy the tsconfig.json from packages/react/avatar/tsconfig.json which extends the shared config

5
packages/react/radix-ui/src/index.ts

Add a re-export line for the new component: 'export { ComponentName, type ComponentNameProps } from "@radix-ui/react-{component-name}"' so it is available from the umbrella package

Pattern: See the existing re-export lines in packages/react/radix-ui/src/index.ts for every other component

6
packages/react/radix-ui/package.json

Add '@radix-ui/react-{component-name}' as a dependency in the radix-ui umbrella package

Pattern: See how packages/react/radix-ui/package.json lists all @radix-ui/react-* packages as dependencies

Conventions to follow:

Every component uses React.forwardRef and accepts a ref propComponent names are PascalCase, package directory names are kebab-caseProps interfaces are named {ComponentName}Props and exported alongside the componentPrimitive DOM rendering goes through @radix-ui/react-primitive's Primitive component (e.g., <Primitive.div>)All packages use the shared build pipeline via internal/builder/builder.jsEach component package is a standalone npm package under the @radix-ui scope

Add a new shared React hook

1
packages/react/{hook-name}/src/{hook-name}.tsx

Create the hook implementation file. Export the hook as a named export. The hook name should follow the 'use-*' convention (e.g., use-controllable-state).

Pattern: Follow the structure in packages/react/use-controllable-state or packages/react/use-callback-ref — each has a src/ directory with the hook implementation

2
packages/react/{hook-name}/src/index.ts

Create a barrel export that re-exports the hook from the implementation file

Pattern: See how other hook packages like packages/react/use-callback-ref/src/index.ts handle their exports

3
packages/react/{hook-name}/package.json

Create package.json with name '@radix-ui/react-{hook-name}', standard source/main/module/types fields, and peerDependencies on react

Pattern: Copy the package.json structure from packages/react/use-callback-ref/package.json

4
packages/react/{hook-name}/tsconfig.json

Create tsconfig.json extending the shared base config

Pattern: Copy from packages/react/use-callback-ref/tsconfig.json

Conventions to follow:

Hook packages are named 'use-{name}' in kebab-caseHook functions are named 'use{Name}' in camelCaseHooks that wrap browser APIs should handle SSR gracefully (check typeof window)Hook packages have peerDependencies on react only (not react-dom) unless DOM access is needed

Add a core utility function

1
packages/core/{utility-name}/src/{utility-name}.ts

Create the utility implementation file. Export pure functions with TypeScript types. Core utilities must be framework-agnostic (no React imports).

Pattern: Follow the structure in packages/core/number — it exports pure utility functions with full TypeScript typing

2
packages/core/{utility-name}/src/index.ts

Create barrel export re-exporting all public functions and types

Pattern: See packages/core/number/src/index.ts for the re-export pattern

3
packages/core/{utility-name}/package.json

Create package.json with name '@radix-ui/{utility-name}', no react peerDependencies since core packages are framework-agnostic

Pattern: Copy the package.json structure from packages/core/number/package.json

4
packages/core/{utility-name}/tsconfig.json

Create tsconfig.json extending the shared base config

Pattern: Copy from packages/core/number/tsconfig.json

Conventions to follow:

Core packages must NOT import React or any framework-specific codeCore packages use .ts files (not .tsx) since they contain no JSXAll functions should be pure and well-typed with explicit return typesCore packages are published under @radix-ui/ scope without the 'react-' prefix

Add Storybook stories for a component

1
apps/storybook/stories/{component-name}.stories.tsx

Create a new stories file. Import the component from its package. Define a default export with title and component metadata. Create named exports for each story variant (e.g., Default, WithCustomProps, Controlled).

Pattern: Look at existing story files in apps/storybook/stories/ — each follows the CSF (Component Story Format) with a default meta export and named story exports

Conventions to follow:

Story files use the .stories.tsx extensionStory titles follow the pattern 'Components/{ComponentName}'Each story is a named export (e.g., export const Default = {})Import components from their @radix-ui/react-{name} package, not relative pathsInclude stories for all major states: default, controlled, disabled, with custom styling

Add Cypress e2e tests for a component

1
cypress/e2e/{component-name}.test.ts

Create a new Cypress test file. Use describe/it blocks. Visit the component's test page and interact with it using Cypress commands. Assert on DOM state, ARIA attributes, and keyboard interactions.

Pattern: Look at existing test files in cypress/e2e/ — they use cy.visit(), cy.get(), cy.findByRole() and assert on accessibility attributes

2
cypress/support/e2e.js

If the component needs custom Cypress commands (e.g., for complex interactions), add them here using Cypress.Commands.add()

Pattern: See existing custom commands already registered in cypress/support/e2e.js

Conventions to follow:

Test files use the .test.ts extension in the cypress/e2e/ directoryUse cy.findByRole() and cy.findByText() (Testing Library queries) over cy.get() with CSS selectors when possibleTest keyboard navigation thoroughly — Tab, Enter, Space, Arrow keys, EscapeAssert ARIA attributes (aria-expanded, aria-selected, role) for accessibility complianceGroup related tests in describe blocks named after the interaction pattern

Add SSR smoke test for a component

1
apps/ssr-testing/app/{component-name}/page.tsx

Create a new Next.js page that renders the component with typical props. The page should import the component from its @radix-ui package and render it in a basic layout. This verifies the component doesn't crash during server-side rendering.

Pattern: See existing pages in apps/ssr-testing/app/ — each is a simple page.tsx that imports and renders a component with minimal props

2
apps/ssr-testing/app/layout.tsx

Add a navigation link to the new component's SSR test page in the root layout's nav section so it's accessible from the index

Pattern: See how apps/ssr-testing/app/layout.tsx already lists links to existing component test pages

Conventions to follow:

Each component gets its own route directory under apps/ssr-testing/app/Pages should use 'use client' directive only if the component requires client-side interactivityImport components from @radix-ui/react-{name} package namesKeep SSR test pages minimal — the goal is to verify no hydration errors, not test functionality

Modify the build pipeline

1
internal/builder/builder.js

Modify the build orchestration logic. This file handles compilation, bundling, and output generation for all packages. Add new build steps, modify output formats, or change compilation settings here.

Pattern: Read the existing build steps in internal/builder/builder.js to understand the pipeline stages: source reading, TypeScript compilation, bundling, and output writing

2
internal/builder/radix-build.js

If adding new CLI flags or changing the build invocation interface, modify this entry point

Pattern: See how internal/builder/radix-build.js currently parses arguments and calls into builder.js

Conventions to follow:

Build scripts are plain JavaScript (not TypeScript) for zero-compilation bootstrappingError handling should use process.exit(1) with descriptive error messagesSignal handling (SIGINT, SIGTERM) is managed in radix-build.js for clean shutdowns

Add or modify ESLint rules

1
internal/eslint-config/index.js

For rules that should apply to ALL packages in the monorepo, add them to the shared config here. This is the centralized config imported by every package.

Pattern: See how internal/eslint-config/index.js structures its rule definitions and plugin configurations

2
internal/eslint-config/react-package.js

For rules specific to React component packages, add them to this React-specific preset

Pattern: See how internal/eslint-config/react-package.js extends the base config with React-specific rules

3
internal/eslint-config/vite.js

For rules specific to Vite-based projects, modify this config

Pattern: See how internal/eslint-config/vite.js layers Vite-specific rules on top of the base

4
eslint.config.mjs

For root-level overrides or workspace-wide ignores, modify the root ESLint config

Pattern: See how eslint.config.mjs imports from internal/eslint-config and adds project-level overrides

Conventions to follow:

Shared rules go in internal/eslint-config/index.js, not in individual package configsReact-specific rules go in internal/eslint-config/react-package.jsApp-specific overrides go in the app's own eslint.config.js (e.g., apps/storybook/eslint.config.js)Use flat config format (ESLint v9+), not the legacy .eslintrc format

Add a new primitive (low-level building block)

1
packages/react/{primitive-name}/src/{primitive-name}.tsx

Create the primitive implementation. Primitives are low-level building blocks used by multiple higher-level components. They should be minimal, composable, and handle a single concern (e.g., focus management, slot rendering, collection tracking).

Pattern: Study packages/react/primitive or packages/react/slot — primitives export minimal components/hooks that handle one specific DOM or React concern

2
packages/react/{primitive-name}/src/index.ts

Create barrel export for the primitive

Pattern: See packages/react/primitive/src/index.ts

3
packages/react/{primitive-name}/package.json

Create package.json. Primitives typically have minimal dependencies — only react, react-dom as peerDeps and possibly @radix-ui/react-primitive.

Pattern: Copy structure from packages/react/slot/package.json or packages/react/primitive/package.json

4
packages/react/{primitive-name}/tsconfig.json

Create tsconfig.json extending the shared base

Pattern: Copy from packages/react/primitive/tsconfig.json

Conventions to follow:

Primitives should handle exactly one concern (single responsibility)Primitives must be composable — they should work with any parent/child componentUse React context for implicit parent-child communication within a primitivePrimitives should forward refs and spread remaining props onto the DOM elementPrimitives are the foundation layer — they should NOT depend on higher-level component packages

Modify global test configuration

1
scripts/setup-tests.ts

Add new global test utilities, custom matchers, or polyfills. This file runs before all Vitest tests across the monorepo.

Pattern: See how scripts/setup-tests.ts currently sets up matchers and polyfills — add new setup in the same style

2
types/global.d.ts

If the new test utilities introduce new global types or augment existing ones, declare them here

Pattern: See how types/global.d.ts declares existing global type augmentations

Conventions to follow:

Test setup must be synchronous or use top-level awaitGlobal polyfills should check for existing implementations before overridingType augmentations go in types/global.d.ts, not inline in test files

Coding Conventions

Standards and patterns used in this codebase

Build System

Packages are compiled into three output formats (CommonJS, ESM, and TypeScript declarations) using esbuild and tsup via a shared builder utility

internal/builder/builder.js compiles each package to CJS, ESM, and .d.ts outputs; radix-build.js serves as the CLI entry point invoking the builder module
internal/builder/builder.jsinternal/builder/radix-build.js
ESLint Configuration

ESLint configs are centralized in an internal shared package and re-exported, with app-specific configs composing the shared base with additional rules (e.g., Storybook, Cypress)

internal/eslint-config/index.js aggregates JS, TS, and custom overrides; apps/storybook/eslint.config.js extends shared Vite rules with Storybook-specific linting
internal/eslint-config/index.jsinternal/eslint-config/eslint.config.jsapps/storybook/eslint.config.jseslint.config.mjs
Testing Strategy

Testing is layered: Vitest for unit tests with a shared setup file (polyfills and testing-library initialization), Cypress for end-to-end tests of complex interactive components, and a dedicated Next.js SSR app for server-rendering smoke tests

scripts/setup-tests.ts initializes testing libraries and polyfills for Vitest; cypress.config.ts configures E2E viewport and parameters; apps/ssr-testing tests SSR rendering
scripts/setup-tests.tscypress.config.tscypress/support/e2e.jsapps/ssr-testing/next.config.js
Package Architecture

Components follow a layered dependency architecture: core utilities (framework-agnostic) → foundation primitives (hooks, context, refs, slots) → infrastructure (overlay, focus, positioning) → composed components (dialog, menu, popover, form, etc.) → umbrella re-export package

packages/core provides number helpers and rect observation; packages/react builds hooks and primitives on top; higher-level packages like dialog and menu compose overlay infrastructure; radix-ui re-exports everything
packages/react/radix-ui
TypeScript Configuration

Global TypeScript type definitions and module declarations are maintained in a centralized types directory, with shared TS configs managed in the internal infrastructure package

types/global.d.ts provides global type definitions and module declarations used across the monorepo
types/global.d.ts
Monorepo Structure

The repository uses a monorepo layout with apps/ for runnable applications (storybook, ssr-testing), packages/ for publishable libraries (core, react components), internal/ for build tooling and shared configs, and cypress/ for E2E tests

apps/storybook for visual development, apps/ssr-testing for SSR smoke tests, internal/builder for build tooling, internal/eslint-config for shared lint rules
apps/storybook/eslint.config.jsapps/ssr-testing/next.config.jsinternal/builder/builder.jsinternal/eslint-config/index.js
Configuration Pattern

Configuration files use flat ESLint config format (eslint.config.js/mjs) and re-export patterns where wrapper files delegate to index modules for cleaner imports

internal/eslint-config/eslint.config.js re-exports from index.js as default export; root eslint.config.mjs composes shared configs with project-specific Cypress rules
internal/eslint-config/eslint.config.jsinternal/eslint-config/index.jseslint.config.mjs