Material Design 3 (Expressive) component library for React.
Token-first and spec-driven: every component reads from MD3 design tokens
(--md-sys-* CSS custom properties), color is generated at runtime from a
seed via Dynamic Color, and behavior is built on native elements with
hand-rolled, APG-compliant interaction patterns — no external a11y framework.
The public API is deliberately MUI-idiomatic (one component per MD3
component selected by variant, onChange(event, value),
startIcon/endIcon, controlled/uncontrolled pairs), while appearance,
defaults, and behavior follow the MD3 spec and Jetpack Compose material3.
Documentation: minop1205.github.io/m3-expressive-react — guides, live demos, and generated prop tables.
npm install m3-expressive-reactRequires React 18 or 19.
import { ThemeProvider, Button } from 'm3-expressive-react'
import 'm3-expressive-react/styles.css'
export function App() {
return (
<ThemeProvider seedColor="#6750A4" mode="light">
<Button variant="filled" onClick={() => console.log('clicked')}>
Hello MD3
</Button>
</ThemeProvider>
)
}The
styles.cssimport is required — it carries the design tokens (shape, motion, state, typescale) every component references. Components intentionally ship without per-value fallbacks; colors come fromThemeProvider, which generates the full MD3 color-role map (light/dark) from your seed color.
CSS minifiers: the styles use modern CSS such as
@starting-style(popup entry transitions). cssnano and Lightning CSS handle it; clean-css does not — it drops the rules inside@starting-styleand corrupts the rules that follow. If your build minifies CSS with clean-css (e.g. Docusaurus' default minimizer — setUSE_SIMPLE_CSS_MINIFIER=true), switch to cssnano or Lightning CSS.
Interactive components (and ThemeProvider) ship with a 'use client'
directive, so you can import them straight into Server Components such as
app/layout.tsx / app/page.tsx — no wrapper file needed. The usual React
Server Components rule applies: event handlers (onClick, onChange, …)
can't be passed from a Server Component, so put interactive markup in your
own 'use client' component. Presentational Badge / Divider and the pure
helpers (generateColorScheme, schemeToCssVars, schemeToCssText, motion
tokens) stay server-safe.
// app/layout.tsx (Server Component)
import { ThemeProvider } from 'm3-expressive-react'
import 'm3-expressive-react/styles.css'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ThemeProvider seedColor="#6750A4" mode="light">
{children}
</ThemeProvider>
</body>
</html>
)
}Every component folder is also its own entry point with a default export, so you can import MUI-style and load only the CSS you use:
import 'm3-expressive-react/tokens.css' // once, at the app root
import { ThemeProvider } from 'm3-expressive-react'
import Button from 'm3-expressive-react/Button'
import Radio, { RadioGroup } from 'm3-expressive-react/Radio'
import type { ButtonProps } from 'm3-expressive-react/Button'-
Default export = the component the folder is named after (
Button,Radio,Chip,Card,Menu,Snackbar, …). Two folders have no namesake:AppBar→TopAppBar,ProgressIndicator→LinearProgressIndicator. Every named export and type of the folder (RadioGroup,ChipSet,MenuItem,SnackbarProvider,BottomAppBar,CircularProgressIndicator, …) is available from the same subpath. -
CSS loads automatically: a subpath module imports its own CSS (plus the shared Ripple / FocusRing styles), so your bundler — Vite, webpack 5 with
css-loader, Next.js app and pages router — ships only the styles of the components you import. The design tokens stay global: importtokens.cssonce instead ofstyles.css. -
Root and subpath imports can be mixed — they share the same modules (one
RadioGroupcontext, oneThemeProvider). The root entry is unchanged and still tree-shaken. -
Primitives for your own controls: the
RippleandFocusRingprimitives have subpaths too (m3-expressive-react/Ripple,m3-expressive-react/FocusRing; default + named export), so a custom control built on them gets their CSS intokens.cssmode:import Ripple from 'm3-expressive-react/Ripple' import { FocusRing } from 'm3-expressive-react/FocusRing'
| You import components from… | Import this CSS once |
|---|---|
m3-expressive-react (root) |
m3-expressive-react/styles.css (every component) |
m3-expressive-react/<Component> (or /Ripple, /FocusRing) |
m3-expressive-react/tokens.css (tokens + typescale) |
Notes:
- Don't combine
styles.csswith subpath imports: the subpath CSS would be loaded a second time (bigger CSS, and the repeated rules can reorder the cascade). Pick one setup. - Environments without CSS imports get CSS-free modules:
require()(CJS, Jest) and server-side Nodeimport(thenodeexport condition — e.g. SSR with externalized dependencies) never touch a.cssfile; the client bundle carries the styles. If your bundler cannot import CSS at all, use the root entry withstyles.css. - Types resolve with TypeScript
moduleResolutionbundler,node16,nodenextand legacynode.
Buttons & actions — Button, IconButton, ButtonGroup, SplitButton,
SegmentedButton, Fab, FabMenu, Chip / ChipSet
Selection & input — Checkbox, Radio / RadioGroup, Switch, Slider,
TextField, Select, Autocomplete, SearchBar, DatePicker,
TimePicker, ScrollField
Navigation — AppBar, Toolbar, NavigationBar, NavigationRail,
NavigationDrawer, Tabs, Menu
Containment & communication — Card, List, Carousel, Dialog,
BottomSheet, SideSheet, Snackbar, Tooltip, Badge, Divider,
ProgressIndicator, LoadingIndicator, PullToRefresh, SwipeToDismiss,
DragHandle
See the components page of the docs site for live demos and prop tables.
Reference tokens → System tokens (--md-sys-*) → Component tokens (--_*)
- Static foundations (type scale, shape, elevation, motion, state-layer
opacities) ship in
styles.cssas CSS custom properties. - Color is dynamic:
ThemeProvideruses@material/material-color-utilitiesto derive every--md-sys-color-*role fromseedColor, formode="light" | "dark". - Focus rings and ripples expose small
--md-focus-ring-*/--md-ripple-*contract variables for tuning.
More in the theming guide.
Built-in labels are English and the pickers default to en-US. Wrap the app
in LocaleProvider to set the locale, the 12 / 24-hour default and every
built-in label in one place (component props still override):
import { LocaleProvider } from 'm3-expressive-react'
<LocaleProvider locale="ja-JP" labels={{ Snackbar: { dismissLabel: '閉じる' } }}>
<App />
</LocaleProvider>See the i18n guide.
The library ships no icons: every icon prop (icon, startIcon,
selectedIcon, …) takes a ReactNode, sized and colored by the component.
We recommend Material Symbols as SVG
components (@material-symbols/svg-400 + SVGR):
import Search from '@material-symbols/svg-400/outlined/search.svg?react'
<IconButton icon={<Search />} aria-label="Search" />This needs a bundler plugin and a TypeScript declaration. See the Icons guide for step-by-step Vite and Next.js setup, plus notes on icon fonts and other icon libraries.
Components are implemented against m3.material.io (design source of
truth) and Jetpack Compose androidx.compose.material3 (behavior,
defaults, motion — exact values read from the AndroidX sources), with
adjudicated spec sheets and audit reports in docs/. Interaction
states use the current MD3 values (hover 0.08 / focus 0.10 / pressed 0.10
state layers), and motion approximates Compose's spring specs.
See the migration guide (docs/migration-v1.md) for the complete v1 breaking-change guide with before/after tables and a checklist.
npm install
npm run dev # Storybook
npm test # Vitest (+ Testing Library + axe)
npm run build # library build (dist/)
npm run vrt # visual regression (see CLAUDE.md for baseline rules)The docs site lives in site/ (Docusaurus) and deploys to GitHub
Pages from main — releases, plus docs-only hotfixes (see
CONTRIBUTING.md):
cd site && npm install && npm startSee CONTRIBUTING.md for guidelines. This project uses Conventional Commits.
| Concern | Choice |
|---|---|
| Build | Vite (library mode) + TypeScript |
| Styling | CSS Modules + CSS custom properties (design tokens) |
| A11y | Native elements + hand-rolled APG patterns; axe-tested |
| Color | @material/material-color-utilities (Dynamic Color) |
| Docs/dev | Storybook; Docusaurus docs site |
| Tests | Vitest + Testing Library + axe; Playwright visual regression |