Thank you for your interest in contributing! This guide will help you get started.
Maintainers with push access to
salesforce/design-system-reactshould clone it directly and push branches there instead of forking, so Chromatic visual review runs on your PR.Chromatic does not run automatically for PRs from forks. A maintainer who can edit labels (triage access or higher) can run it after reviewing the changes by adding the
run-chromaticlabel. The label is removed whenever new commits are pushed, so it must be re-applied for each update.
-
Fork this repository (button in upper right)
-
Clone your fork locally
git clone git@github.com:[YOUR-USER]/design-system-react.git cd design-system-react -
Install dependencies (Node >= 24 required;
nvm usepicks up.nvmrc)npm install npx playwright install chromium # browser for the Vitest `browser` project -
Start Storybook
npm run storybook
View components at http://localhost:6007
-
Read the Codebase Overview to understand conventions and best practices. If you work with an AI coding agent, it should read AGENTS.md.
| Command | Description |
|---|---|
npm run storybook |
Start Storybook dev server |
npm run dev |
Start Vite dev server |
npm test -- --run |
Run all tests once (bare npm test starts watch mode) |
npm run test:ui |
Run tests with Vitest UI (needs @vitest/ui installed) |
npm run lint |
Lint all files |
npm run lint:fix |
Fix lint issues and format with Prettier |
npm run typecheck |
TypeScript type checking |
npm run build |
Build the library |
npm run test:ci |
Run everything CI runs (typecheck, test, build, lint) |
git checkout -b feature/my-feature-nameCreate a GitHub issue with:
- Component name and description
- List of proposed props with types
- Link to SLDS documentation
components/
└── my-component/
├── index.tsx # Main component export
├── my-component.tsx # Component implementation
├── types.ts # TypeScript interfaces
├── private/ # Internal sub-components
│ └── sub-component.tsx
├── __docs__/
│ └── MyComponent.stories.tsx # Storybook stories (.jsx or .tsx)
└── __tests__/
└── my-component.test.tsx
- Use functional components with hooks
- Use TypeScript for all new components
- Add forwardRef for ref handling
- Add displayName for debugging
- Export from
components/index.js(runtime) andcomponents/index.d.ts(types)
Stories should be in CSF (Component Story Format):
import type { Meta, StoryObj } from '@storybook/react';
import MyComponent from '../';
const meta: Meta<typeof MyComponent> = {
title: 'Components/MyComponent',
component: MyComponent,
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
export const Default: Story = {
args: {
// props
},
};Stories are picked up automatically from components/**/__docs__/*.stories.{jsx,tsx}, and each one (except portal-rendered stories, which can't be server-rendered) is also rendered into an HTML snapshot (components/__tests__/__snapshots__/story-snapshots.test.jsx.snap). After adding or changing stories, regenerate and commit the snapshots:
npm test -- --run -u# Run all tests once
npm test -- --run
# Run tests with UI
npm run test:ui
# Run tests for a specific component (does NOT include story snapshots)
npm test -- --run components/button
# Regenerate story HTML snapshots after intentional markup changes
npm test -- --run -uTests use Vitest and React Testing Library:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import MyComponent from '../';
describe('MyComponent', () => {
it('renders correctly', () => {
render(<MyComponent label="Test" />);
expect(screen.getByText('Test')).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const onClick = vi.fn();
render(<MyComponent label="Click me" onClick={onClick} />);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalled();
});
});- Test all props
- Test user interactions (click, keyboard, focus)
- Test accessibility (ARIA attributes)
- Aim for 90%+ coverage
- Story HTML snapshots updated and reviewed whenever stories or rendered markup change
This project uses Prettier and ESLint:
# Fix all formatting issues
npm run lint:fix- Use explicit types for props
- Avoid
any- useunknownor proper types - Export types from
types.tsfiles
| Type | Convention | Example |
|---|---|---|
| Component files | kebab-case | my-component.tsx |
| Story files | PascalCase | MyComponent.stories.jsx / .tsx |
| Type files | lowercase | types.ts |
| Components | PascalCase | MyComponent |
| Props interfaces | PascalCase + Props | MyComponentProps |
Run npm run test:ci to do steps 1–4 in one go. A pre-push hook also checks story snapshots.
- Run linting:
npm run lint - Run type check:
npm run typecheck - Run tests:
npm test -- --run(unfiltered, so story snapshots are checked) - Run build:
npm run build - Test in Storybook:
npm run storybook - Rebase on
masterand resolve any conflicts
- Descriptive title and summary
- Link to related issue
- Screenshots for visual changes
- All CI checks passing
Use conventional commits:
feat: Add new Button variant
fix: Correct Modal focus trap
docs: Update README examples
chore: Update dependencies
Keep lint/format-only changes in a separate style: commit from functional changes so the diff is easy to review.
- Maintainers will review your PR
- Address any feedback
- Once approved, your PR will be merged
All components must be accessible:
- Keyboard navigable
- Proper ARIA attributes
- Screen reader compatible
- WCAG 2.1 AA compliant
- Issues: GitHub Issues
- Stack Overflow: Tag with
design-system-react
Thank you for contributing!