Use with shadcn/ui
Install the md-gb elements as React components with the shadcn CLI, themed by your shadcn variables.
What you get
Link to “What you get”This site is a shadcn registry at
/3.0/r/. npx shadcn@latest add copies a file into your project, adds the
npm dependencies, and merges the theme:
- A React wrapper per component family, such as
md-buttonormd-menu(a menu with its group and item). The file iscomponents/ui/md-<name>.tsx. It registers themd-gb-*elements and wraps each one with@lit/react: typed props from the element class, and the events asonChange,onCloseand so on. md-theme, installed with every item: it loads the M3 styles and maps your shadcn variables to the M3 color and shape roles.md-theme-m3, optional: the reverse, the M3 baseline as shadcn variables, so shadcn components match the md-gb elements.
The registry lists every item in
registry.json.
Add a component
Link to “Add a component”The plain URL form needs no setup:
npx shadcn@latest add https://material-web.dev/3.0/r/md-button.json
To use the short form, add the registry to components.json once. The keys of
registries start with @, and {name} is the item:
{
"registries": {
"@material-web": "https://material-web.dev/3.0/r/{name}.json"
}
}
npx shadcn@latest add @material-web/md-button @material-web/md-menu
The command installs @material/web and @lit/react with your package
manager. Each component page has this command after its first example.
Use the component like any other. The JSON and TSX blocks on this page are plain blocks and not live demos: the TSX needs React, which this page does not load, and the JSON is configuration with no visible effect.
import {MdButton} from '@/components/ui/md-button';
import {MdMenu, MdMenuItem} from '@/components/ui/md-menu';
export function Actions() {
return (
<>
<MdButton color="filled" popoverTarget="file-menu">
File
</MdButton>
<MdMenu id="file-menu">
<MdMenuItem onChange={(event) => console.log(event.type)}>Open</MdMenuItem>
</MdMenu>
</>
);
}
The theme bridge
Link to “The theme bridge”md-theme adds @import "@material/web/labs/gb/styles/m3.css" to your global
CSS, then declares the --md-sys-* variables in :root and in .dark from
your shadcn variables. The M3 styles are in a cascade layer and the bridge is
not, so the bridge wins regardless of the order of the imports. Change your
shadcn theme, or toggle .dark, and the elements follow. It also sets
color-scheme for :root and .dark.
Containers stay with their on- colors: each pair maps to a pair that shadcn
already keeps readable together. The *-fixed roles, scrim and shadow keep
the M3 baseline.
| M3 role | shadcn value | Why |
|---|---|---|
--md-sys-color-primary |
var(--primary) |
Same role. |
--md-sys-color-on-primary |
var(--primary-foreground) |
Same pair. |
--md-sys-color-primary-container |
var(--accent) |
shadcn has no primary container; accent is its tinted fill. |
--md-sys-color-on-primary-container |
var(--accent-foreground) |
Pairs with accent. |
--md-sys-color-secondary |
var(--muted-foreground) |
The mid-tone solid; text on it is --background. |
--md-sys-color-on-secondary |
var(--background) |
Pairs with the solid above. |
--md-sys-color-secondary-container |
var(--secondary) |
shadcn secondary is a tonal fill, like an M3 container. |
--md-sys-color-on-secondary-container |
var(--secondary-foreground) |
Pairs with secondary. |
--md-sys-color-tertiary |
var(--primary) |
shadcn has no tertiary; it follows primary. |
--md-sys-color-on-tertiary |
var(--primary-foreground) |
Pairs with primary. |
--md-sys-color-tertiary-container |
var(--accent) |
Follows the primary container. |
--md-sys-color-on-tertiary-container |
var(--accent-foreground) |
Pairs with accent. |
--md-sys-color-error |
var(--destructive) |
Same role. |
--md-sys-color-on-error |
var(--destructive-foreground, oklch(1 0 0)) |
shadcn v4 has no destructive-foreground; white. |
--md-sys-color-error-container |
color-mix(in oklab, var(--destructive) 14%, var(--background)) |
A tint of destructive on the background. |
--md-sys-color-on-error-container |
var(--destructive) |
Destructive on its tint. |
--md-sys-color-background |
var(--background) |
Same role. |
--md-sys-color-on-background |
var(--foreground) |
Same pair. |
--md-sys-color-surface |
var(--background) |
The page surface. |
--md-sys-color-on-surface |
var(--foreground) |
Same pair. |
--md-sys-color-surface-variant |
var(--muted) |
Muted fill. |
--md-sys-color-on-surface-variant |
var(--muted-foreground) |
Muted text. |
--md-sys-color-surface-tint |
var(--primary) |
Same as primary. |
--md-sys-color-surface-dim |
var(--muted) |
Muted fill. |
--md-sys-color-surface-bright |
var(--background) |
The page surface. |
--md-sys-color-surface-container-lowest |
var(--background) |
Lowest tone: the page. |
--md-sys-color-surface-container-low |
var(--card) |
Cards. |
--md-sys-color-surface-container |
var(--popover) |
Menus and popovers. |
--md-sys-color-surface-container-high |
var(--muted) |
Muted fill. |
--md-sys-color-surface-container-highest |
var(--accent) |
Accent fill (the highest tone in both schemes). |
--md-sys-color-outline |
var(--muted-foreground) |
M3 needs 3:1 against the surface; shadcn --border is too faint for field and button outlines. |
--md-sys-color-outline-variant |
var(--border) |
Dividers and decorative borders. |
--md-sys-color-inverse-surface |
var(--foreground) |
Inverse of the page. |
--md-sys-color-inverse-on-surface |
var(--background) |
Pairs with the inverse surface. |
--md-sys-color-inverse-primary |
var(--primary-foreground) |
Primary-side color that reads on the inverse surface. |
Corners follow --radius (the px values are the M3 ones at shadcn's default 0.625rem). --md-sys-shape-corner-value-* get the same values.
| M3 corner | shadcn value |
|---|---|
--md-sys-shape-corner-xs |
calc(var(--radius) * 0.4) |
--md-sys-shape-corner-sm |
calc(var(--radius) * 0.8) |
--md-sys-shape-corner-md |
calc(var(--radius) * 1.2) |
--md-sys-shape-corner-lg |
calc(var(--radius) * 1.6) |
--md-sys-shape-corner-lg-increased |
calc(var(--radius) * 2) |
--md-sys-shape-corner-xl |
calc(var(--radius) * 2.8) |
--md-sys-shape-corner-xl-increased |
calc(var(--radius) * 3.2) |
--md-sys-shape-corner-xxl |
calc(var(--radius) * 4.8) |
shadcn has no tertiary role, so it follows primary. outline is the muted
foreground and not --border, because M3 needs 3:1 against the surface for the
outline of a field or an outlined button; --border is the outline-variant
(dividers).
Match shadcn to the M3 baseline
Link to “Match shadcn to the M3 baseline”md-theme-m3 goes the other way. It writes the baseline colors of M3 (light
and dark) into the shadcn variables:
npx shadcn@latest add https://material-web.dev/3.0/r/md-theme-m3.json
| shadcn variable | M3 baseline role |
|---|---|
--background |
--md-sys-color-surface |
--foreground |
--md-sys-color-on-surface |
--card |
--md-sys-color-surface-container-low |
--card-foreground |
--md-sys-color-on-surface |
--popover |
--md-sys-color-surface-container |
--popover-foreground |
--md-sys-color-on-surface |
--primary |
--md-sys-color-primary |
--primary-foreground |
--md-sys-color-on-primary |
--secondary |
--md-sys-color-secondary-container |
--secondary-foreground |
--md-sys-color-on-secondary-container |
--muted |
--md-sys-color-surface-container-high |
--muted-foreground |
--md-sys-color-on-surface-variant |
--accent |
--md-sys-color-surface-container-highest |
--accent-foreground |
--md-sys-color-on-surface |
--destructive |
--md-sys-color-error |
--border |
--md-sys-color-outline-variant |
--input |
--md-sys-color-outline |
--ring |
--md-sys-color-primary |
--chart-1 |
--md-sys-color-primary |
--chart-2 |
--md-sys-color-tertiary |
--chart-3 |
--md-sys-color-secondary |
--chart-4 |
--md-sys-color-primary-fixed-dim |
--chart-5 |
--md-sys-color-tertiary-fixed-dim |
--sidebar |
--md-sys-color-surface-container-low |
--sidebar-foreground |
--md-sys-color-on-surface |
--sidebar-primary |
--md-sys-color-primary |
--sidebar-primary-foreground |
--md-sys-color-on-primary |
--sidebar-accent |
--md-sys-color-secondary-container |
--sidebar-accent-foreground |
--md-sys-color-on-secondary-container |
--sidebar-border |
--md-sys-color-outline-variant |
--sidebar-ring |
--md-sys-color-primary |
--radius is 0.75rem, the M3 medium corner.
It is independent of md-theme. With both installed, the bridge points the M3
roles at shadcn variables that now hold the M3 colors, so the two agree.
Bundler setup
Link to “Bundler setup”The elements import their styles as CSS module scripts
(import styles from './menu.css' with {type: 'css'}). Browsers support that,
but Vite, webpack and Node do not without help, and the build fails with
"default" is not exported by "…/focus-ring.css". Every stylesheet ships next
to a Lit CSSResult module (menu.cssresult.js), so a one-time plugin that
points the imports there is enough. For Vite (a plain block; it is
configuration with no visible effect):
// vite.config.ts
import {defineConfig, type Plugin} from 'vite';
function mdGbCssModules(): Plugin {
return {
name: 'md-gb-css-modules',
enforce: 'pre',
transform(code, id) {
if (!/[\\/]@material[\\/]web[\\/]/.test(id) || !id.endsWith('.js')) return;
return code.replace(
/import\s+(\w+)\s+from\s+'(\.[^']*)\.css'\s+with\s*\{\s*type:\s*'css'\s*\};?/g,
"import {styles as $1} from '$2.cssresult.js';",
);
},
};
}
export default defineConfig({
plugins: [mdGbCssModules() /* , react(), tailwindcss() */],
optimizeDeps: {exclude: ['@material/web']},
});
Limits
Link to “Limits”- Tailwind classes style only the host. The elements use shadow DOM, so
className="bg-red-500"colors the<md-gb-button>box, not its inside. Customize with the--md-*custom properties and::part()selectors listed in each component's API reference. They work in a plain CSS file or in Tailwind's arbitrary properties, such as[--container-color:red]. - Client only. The wrappers start with
'use client', and the elements cannot be imported in Node (they use CSS module scripts,customElementsanddocument). In Next.js the server renders client components too, so import the file withnext/dynamic(() => import('...'), {ssr: false})or render it after mount, unless you set up Lit SSR. - Labs. The elements come from
@material/web/labs/gb/. The registry keeps@material/webas the dependency, and the path needs a version that shipslabs/gb. - Typography, motion and icons are not bridged. Load the Material Symbols font as in the quick start.
- Local testing. When this site runs from the dev server the registry
files point to
http://localhost:8000. SetMWC_REGISTRY_URLto build them for another origin.
The registry files are generated by scripts/m3/gb-shadcn.mjs from the element
sources, so the props and events match the API reference. For agents, the same
information is in llms.txt and
custom-elements.json.