Material 3 web components built with Stencil. More than 70 custom elements — buttons, text fields, dialogs, data tables — that style themselves from CSS custom properties and run in any page, with or without a framework.
Each component bundles its own CSS into its shadow root, so the only stylesheet
you need is a theme file of design tokens. A second, optional one
(material.css) covers what lives outside a shadow root: the two components
that enhance real server-rendered markup, plus the MD3 type scale and a form
grid as plain classes for pages with no build step. There is no runtime
stylesheet fetch, and Tailwind projects can pull the whole token set into their
own build instead.
npm install advanced-material-webOr load it from a CDN with no install:
<script type="module" src="https://unpkg.com/advanced-material-web"></script>A page needs three things: the theme stylesheet, a theme class on <html>, and
the Material Symbols font.
<html lang="en" class="light">
<head>
<link rel="stylesheet" href="https://unpkg.com/advanced-material-web/css/theme.css">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0">
<script type="module" src="https://unpkg.com/advanced-material-web"></script>
</head>
<body>
<material-button variant="filled" label="It works" icon="check"></material-button>
</body>
</html>theme.csscarries the--md-sys-color-*custom properties. They cascade into every shadow tree, so components render themed with no per-component setup.- The
<html>class picks the theme:light,dark, or one of the four contrast variants. No class means no tokens. - The Material Symbols font renders the
icon="..."ligatures. Without it, icons show as their text names.
Want the type scale and grid as ready-made classes, or using
material-data-table / material-breadcrumbs? Add
advanced-material-web/material.css next to theme.css — see
Without Tailwind.
| Method | Source | Use it when |
|---|---|---|
| Single bundle | unpkg.com/advanced-material-web (~590 KB, all elements eager) |
You want one file and use many components. |
| Lazy loader | .../dist/material/material.esm.js (small entry) |
A page uses a few components; chunks load on demand. |
With a bundler:
import { defineCustomElements } from 'advanced-material-web/loader';
import 'advanced-material-web/theme.css';
defineCustomElements();Or import only the elements you use. Each one registers itself:
import 'advanced-material-web/dist/components/material-button.js';The elements work in any framework as-is. The same install also carries typed props, real event bindings, two-way binding and — for React and Vue — server rendering, as subpath imports:
import { MaterialButton } from 'advanced-material-web/react'; // React 18+, Next.js / Remix SSR
import { MaterialButton } from 'advanced-material-web/vue'; // Vue 3.5+, v-model, Nuxt SSR
import { MaterialButton } from 'advanced-material-web/angular'; // Angular 19+, standalone, reactive forms
import { MaterialButton } from 'advanced-material-web/svelte'; // Svelte 5, bind:valueNo separate install and no version to keep in lockstep — each subpath is generated from the same
component sources on every release. See docs/frameworks.md.
The package also ships editor metadata — VS Code custom data, JetBrains web-types, and a Custom
Elements Manifest — so <material-*> tags autocomplete in plain HTML and server templates. Setup is
in docs/frameworks.md.
- Self-contained styles. No stylesheet to fetch, no adopted-stylesheet wiring, no flash of unstyled content. A component renders correctly the moment it upgrades.
- Form association. Put a field in a
<form>with anameand it posts its value, runs constraint validation (required,checkValidity(),reportValidity()), and resets with the form. No hidden inputs. - Server-first. Components enhance server-rendered markup, such as a real
<table>insidematerial-data-tableor a real<form>post, instead of owning a client-side data model. - Plain events. Every interaction is a
CustomEventyou read withaddEventListener(valueChange,materialSort, …). - RTL and i18n. Set
dir="rtl"on any subtree and layout, animation, and keyboard direction follow. Strings and formats come from yourgettextandIntl, English by default. - Accessibility. Keyboard navigation, focus rings, 48dp touch targets,
Windows High Contrast, and
prefers-reduced-motionare built in.
| Group | Elements | Reference |
|---|---|---|
| Actions | button, icon-button, fab, fab-menu, split-button, button-group, chip, chip-set | actions.md |
| Text fields | textfield, textarea, number-field, masked-field, date-field, time-field, datetime-field, date-range-field, file-field, json-field | fields.md |
| Selection | checkbox, radio, switch, slider, select, autocomplete, dropzone | forms.md |
| Navigation | app-bar, toolbar, navigation-bar, navigation-rail, navigation-group, navigation-item, tabs, breadcrumbs, pagination, stepper | navigation.md |
| Overlays | dialog, bottom-sheet, side-sheet, menu, tooltip, snackbar, command-palette | overlays.md |
| Data & display | data-table, list, card, avatar, badge, divider, tree, transfer, calendar, carousel, rich-text, time-picker | display.md, data-table.md, lists.md |
| Progress | linear-progress, circular-progress, loading-indicator, skeleton | progress.md |
| Search | search, search-app-bar | search.md |
Tags and attributes are kebab-case (<material-date-field first-day-of-week="1">).
Boolean attributes follow HTML rules: present means true.
One thing in the package is a function rather than an element — showing a snackbar without putting a host in your markup:
import { snackbar } from 'advanced-material-web';
snackbar({ message: 'Task deleted', actionLabel: 'Undo', onAction: undo });It enqueues into the page's <material-snackbar-host>, creating one on
document.body if there is none — which is also the placement that survives a
transform or contain on an ancestor, since either makes that ancestor the
containing block for fixed positioning and traps a snackbar inside it. Add the
host yourself when you want to set placement or live; the call finds it.
theme.css is two halves in one file:
| Contents | Origin | |
|---|---|---|
tokens.css |
type scale, motion, shape, elevation, focus ring | ours, from the M3 spec |
| the palettes | --md-sys-color-*, six classes |
Material Theme Builder |
The six classes are light, dark, and a medium- and high-contrast variant of
each. Switch themes by changing the <html> class. Those class names are part of
the public contract — tailwind.css keys its dark: variant off them.
With no class anywhere the light palette applies — it sits on :root as well —
so forgetting the class costs you the choice, not the colors. Load the elements
through the CDN bundle or defineCustomElements() and a startup check reports
the setup in the console too: a warning when no tokens are on the page at all, a
note when only the class is missing. It does not run behind the framework
wrappers, which import per-component modules that Stencil doesn't wire the
global script into — there, a forgotten theme.css still shows up as unstyled
components with a silent console.
Build one at Material Theme Builder,
Export → Web → CSS, then load tokens.css and your export instead of
theme.css. No build step, nothing to rebuild:
<link rel="stylesheet" href="https://unpkg.com/advanced-material-web/css/tokens.css">
<link rel="stylesheet" href="/static/my-theme.css">Theme Builder exports colors and nothing else, which is why the split exists:
dropping an export in place of the whole of theme.css would take the type
scale, radii, shadows and motion curves with it. Component code never changes
either way — every element reads the same --md-sys-color-* names. See
theming.md.
Using Tailwind v4? Import the bridge and the utilities you already know are
Material-flavoured — bg-primary, text-on-surface, rounded-lg, shadow-md,
ease-out — with no new class names:
@import "tailwindcss";
@import "advanced-material-web/tailwind.css";<div class="bg-surface-container text-on-surface rounded-lg shadow-md p-4">
<h2 class="text-headline-small">Material type scale</h2>
</div>It bridges the full MD3 token set into Tailwind's theme namespaces — colors
(including the *-fixed pairs), the type scale both as text-xs … text-6xl and
as MD3 roles (text-body-large, text-display-small), --radius-* from the
corner tokens, --shadow-* from the elevation levels plus explicit
shadow-elevation-1 … 5, and the easing and duration curves
(ease-emphasized-decelerate, duration-medium2).
Two things worth knowing:
- Every value is a
var()reference, so a palette swap needs no rebuild — the utilities follow whatever theme class is on<html>. dark:is repointed at that class instead ofprefers-color-scheme, sodark:bg-surfaceand the components can't disagree. Renamed the classes in your own theme? Redeclare@custom-variant darkafter the import.
You still load tokens.css (or theme.css) in the page — the bridge only
declares utilities, it doesn't carry token values.
Not every page has a build step. material.css carries the class-based half of
the design system for the ones that don't — the type scale and a form grid:
<link rel="stylesheet" href="https://unpkg.com/advanced-material-web/css/theme.css">
<link rel="stylesheet" href="https://unpkg.com/advanced-material-web/css/material.css">Type scale — one class per MD3 role, same names as @material/web:
<h1 class="md-typescale-display-small">Invoices</h1>
<p class="md-typescale-body-medium">Body copy.</p>
<span class="md-typescale-label-small">CAPTION</span>Form grid — the usual problem: most fields share a row, the occasional long one takes the whole width, and nobody wants to count columns per breakpoint.
<form class="md-grid">
<material-textfield label="First name" style="--md-span: 3"></material-textfield>
<material-textfield label="Last name" style="--md-span: 3"></material-textfield>
<material-textfield label="Address" style="--md-span: 12"></material-textfield>
</form>12 tracks by default; --md-grid-columns and --md-grid-gap change the
container, --md-span changes one child. Sizing is a container query, not a
media query — the same form stacks by itself inside a side sheet or a narrow
column, with no viewport breakpoints and no per-context overrides. Below 30rem
of container width every child takes its own row; above it each takes the span
it asks for, clamped to the tracks that exist. md-grid-auto is the other
shape: equal tracks that reflow on their own, sized by --md-grid-min, for
tiles where the count doesn't matter.
Both work the same in React (style={{ '--md-span': 3 }}) and Angular
([style.--md-span]="3") — they're classes, so there is nothing to import and
no wrapper component. Live examples:
layout and
typography.
Everything in the file sits in a cascade layer, so your own unlayered CSS
overrides it without !important. On Tailwind? Skip all of this — grid-cols-12
with @container does the same job, and tailwind.css above already
Material-flavours it.
The same file also styles the two components that enhance real server-rendered
markup — material-data-table (a <table>) and material-breadcrumbs (a
<nav>) — instead of owning a shadow root, so their CSS can't be bundled into a
JS chunk the way every other component's is. Skip material.css and the rest of
the library still works; only these two render unstyled. Any future component
built the same server-first way joins this file rather than getting its own.
Each area has a reference under skills/material-web/references/:
- setup.md — loading the library on a page
- theming.md — tokens and palettes
- forms.md / fields.md — form controls and validation
- navigation.md, overlays.md, display.md, lists.md, data-table.md, progress.md, search.md, stepper.md
- i18n.md — catalogs and formats
- integrations.md — what you wire up (endpoints, persistence, file upload)
Framework packages and editor tooling: docs/frameworks.md.
Build from source and run the dev server:
git clone https://github.com/viewflow/material
cd material && npm install
npm start # http://localhost:3333npm start runs three watchers: theme bundling, Tailwind for the demo pages, and
Stencil's dev server. Every component has a demo page at
src/demos/<component>.html, which is where behavior is checked. There are no unit
tests; the components are mostly CSS, so demo pages carry the test cases.
Watch rebuilds skip the framework wrapper codegen (MATERIAL_WRAPPERS=0).
Build the package:
npm run build # the css/ stylesheets + Stencil dist/ + hydrate/ + tooling JSON + CDN bundle
npm run build:all # the above, then the four framework adapters under adapters/npm publish runs npm run build:all first through prepublishOnly. The framework adapters are
subpath exports of this same package, not separate ones — see docs/frameworks.md.
Add a component:
npx stencil generate material-cardKeep each component's styles in its own shadow root and read var(--md-sys-color-*)
directly — src/global/material.css is a Tailwind entry for the demo and
showcase pages, plus the source npm run build:material:pkg reads to produce
the published css/material.css (see Light-DOM components
above). A component's own shadow-DOM styles never depend on it; only the two
light-DOM components it @imports under layer(components) do.
The four published stylesheets under css/ come from src/theme/ and
src/components/, never from hand-editing css/:
| Published | Built from | By |
|---|---|---|
theme.css |
src/theme/theme.css — tokens.css + the six palettes |
build:theme:pkg |
tokens.css |
src/theme/tokens.css — typography + system tokens |
build:tokens:pkg |
tailwind.css |
src/theme/tailwind.css, copied uncompiled |
build:tailwind:pkg |
material.css |
the layer(components) imports in src/global/material.css |
build:material:pkg |
tailwind.css is the odd one: it must reach consumers as source, since their
Tailwind build is what turns its @theme directives into utilities. Nothing
compiles it here, so scripts/build-tailwind-preset.mjs type-checks it by hand
instead — it refuses to publish if the file grows an @source, switches to
@theme inline, hardcodes a value instead of a var(), references a token that
src/theme/ doesn't define, or leaves an --md-sys-color-* without a matching
--color-* utility. Any of those would fail silently in someone else's app.
src/global/material.css imports the same file, so the demo pages exercise
exactly what ships.
advanced-material-web is an Open Source project. It uses the AGPL license,
The GNU Affero General Public License v3.0,
with the additional permissions in LICENSE_EXCEPTION.
The exception permits you to use this package in a project that has a license which is not compatible with the AGPL. A proprietary project is included. Your own code keeps your own license, and you do not release its source. The condition is that you do not change the source code of this package — importing the components, styling them through CSS custom properties, and shipping the bundle as-is all stay within the exception.
If you do change this package, the AGPL applies to your modified version of it.
The license scheme is the same as the license scheme of the GCC Runtime Library. The text above is a summary. Read LICENSE_EXCEPTION for the conditions.
The exception applies to a library that "bears a notice placed by the copyright
holder" naming the AGPL and the exception — so every source file carries that
notice in its header, and npm run license:check fails the build if one drifts.
scripts/license-header.mjs holds the text and stamps it; edit it there, never
file by file.
Minified artifacts (cdn/material.min.js, css/material.css) drop the per-file
copies and carry the notice once, in a banner at the top.
Passing these files to a model — as context, as a prompt, as training data — copies them, and the copy is governed by the AGPL like any other. The copyright holder therefore regards code produced that way as a derived work of this package, whether or not the result is a literal copy.
This is a statement of the copyright holder's position, not an extra condition bolted onto the AGPL: AGPLv3 section 7 lets a recipient remove added conditions outside its own enumerated list, so adding one would achieve nothing except making the license read as something other than AGPL to license scanners.
Rewriting the components with a model in order to avoid the copyleft is the case this is about. If that is what you need, a commercial license removes the copyleft outright and is the cheaper path.