m3-expressive-react
v1.3.1
Published
Material Design 3 (Expressive) component library for React — token-first, spec-driven, MUI-idiomatic API
Maintainers
Readme
m3-expressive-react
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.
Install
npm install m3-expressive-reactRequires React 18 or 19.
Quick start
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.
Per-component imports
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.
| You import components from… | Import this CSS once |
| ------------------------------------- | ----------------------------------------------------- |
| m3-expressive-react (root) | m3-expressive-react/styles.css (every component) |
| m3-expressive-react/<Component> | 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.
Components
Buttons & actions — Button, IconButton, ButtonGroup, SplitButton,
SegmentedButton, Fab, FabMenu, Chip / ChipSet
Selection & input — Checkbox, Radio / RadioGroup, Switch, Slider,
TextField, SearchBar, DatePicker, TimePicker
Navigation — AppBar, Toolbar, NavigationBar, NavigationRail,
NavigationDrawer, Tabs, Menu
Containment & communication — Card, List, Carousel, Dialog,
BottomSheet, SideSheet, Snackbar, Tooltip, Badge, Divider,
ProgressIndicator, LoadingIndicator, SwipeToDismiss
See the components page of the docs site for live demos and prop tables.
Theming
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.
Icons
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.
Spec fidelity
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.
Migrating from 0.x
See the migration guide (docs/migration-v1.md) for the complete v1 breaking-change guide with before/after tables and a checklist.
Development
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 develop:
cd site && npm install && npm startSee CONTRIBUTING.md for guidelines. This project uses Conventional Commits.
Tech stack
| 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 |
