@stackific/md3
v2.0.0
Published
Material Design 3 framework — SCSS + plain-JS. M3 tokens, palette utilities, dialogs, menus, sliders, snackbars; optional runtime theme generation when the consumer brings material-dynamic-colors.
Downloads
295
Maintainers
Readme
@stackific/md3
Material Design 3 framework. Compiled CSS + plain-JS runtime. Drop into any HTML page; no framework required.
- 100% class-driven — every component is a class (
button,card,field,nav…). md3 never styles bare HTML tags, so your own<button>,<h1>, or<nav>are never restyled — no fighting the framework, and no!importantarms race. - Light, dark, and OS-following
automodes — CSS-driven, no JS needed. - 18 baked brand themes, swap with one attribute.
- Optional runtime theme generation from a hex / image / file (via
material-dynamic-colors). - Material Symbols icon fonts ship inside the CSS — no Google Fonts import.
Table of contents
- Install
- Quick start
- Bootstrap —
data-theme,data-mode,data-ui - Runtime API —
ui(...) - Main layout
- Components
- Helpers
- Design tokens
- Themes
- Theme & Mode selector
- Breakpoints
- Repository
Install
CDN — paste into any HTML page:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@stackific/md3/dist/md3.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@stackific/md3/dist/md3.js"></script>npm / pnpm / yarn:
npm install @stackific/md3
# or: pnpm add @stackific/md3
# or: yarn add @stackific/md3import "@stackific/md3"; // runtime, registers globalThis.ui
import "@stackific/md3/style"; // compiled CSSLocal files — download dist/md3.css, dist/md3.js, and the dist/assets/* they reference from the published package, then serve them from your own host:
<link rel="stylesheet" href="/md3/md3.css">
<script type="module" src="/md3/md3.js"></script>For Vite users, build with assetsInlineLimit: 0 to keep the bundled CSS at its original size (otherwise the font files get inlined as data URIs).
Optional — runtime theme generation from a hex / image / file:
npm install material-dynamic-colorsimport "material-dynamic-colors"; // once, at app entry
await ui("theme", "#1447E6");Without it, only baked-theme swaps and precomputed { light, dark } objects work via ui("theme", …).
Quick start
<!doctype html>
<html data-theme="stackific" data-mode="auto">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@stackific/md3/dist/md3.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@stackific/md3/dist/md3.js"></script>
<title>Hello md3</title>
</head>
<body>
<main class="main responsive">
<h1 class="h1">Hello, md3</h1>
<nav class="nav">
<button class="button">Primary</button>
<button class="button border">Outlined</button>
<button class="button transparent circle"><i class="icon">search</i></button>
</nav>
<article class="card">
<h5 class="h5">Card</h5>
<p class="p">Add md3 component classes to semantic markup — your own tags stay untouched.</p>
</article>
</main>
</body>
</html>That's it. You should see an md3-styled page that picks light or dark automatically from your OS preference.
Bootstrap
<html data-theme="stackific" data-mode="auto">| Attribute | Values | Notes |
| ------------ | ---------------------------- | ------------------------------------------------------ |
| data-theme | any baked theme name | Pure attribute swap; no JS required for static pages. Omit to use the default (stackific). |
| data-mode | auto | light | dark | auto follows prefers-color-scheme in CSS (no JS). |
| data-ui | #some-id | On any element, clicking delegates to ui("#some-id"). Anchors without href also respond to Enter. |
Mode is persisted to localStorage["md3:mode"], theme to localStorage["md3:theme"]. The stored value beats the hardcoded data-mode / data-theme on <html>.
Runtime API
A single global function ui(...) (also import { ui } from "@stackific/md3"). The runtime auto-rescans the DOM via a MutationObserver, so calling ui() after injecting markup is usually optional.
| Call | Effect |
| ----------------------------------- | --------------------------------------------------------------- |
| ui() | Re-scan the DOM (fields, sliders, ripples, progress, triggers). |
| ui("guid") | Returns a UUIDv4 string. |
| ui("mode") | Returns current mode: "auto" | "light" | "dark". |
| ui("mode", "dark") | Set mode. Persists to localStorage["md3:mode"]. |
| ui("themes") | Returns array of baked theme names. |
| ui("theme", "stackific") | Swap baked theme. Persists to localStorage["md3:theme"]. |
| await ui("theme", "#1447E6") | Generate theme from hex. Requires material-dynamic-colors. |
| await ui("theme", url \| File \| Blob) | Generate theme from image / blob. Requires material-dynamic-colors. |
| ui("theme", { light, dark }) | Apply precomputed CSS-string per mode. |
| await ui("theme") | Returns the currently-applied { light, dark } token strings. |
| ui("#my-dialog") | Toggle a <dialog>. |
| ui("#my-menu") | Toggle a <menu>. |
| ui("#my-snackbar") | Show snackbar for 6000 ms. |
| ui("#my-snackbar", 3000) | Show snackbar for N ms. |
| ui("#my-snackbar", -1) | Show until clicked. |
| ui("#my-page") | Activate a .page (siblings deactivate). |
| ui("#anything-else") | Toggle .active on the target. |
ui("#id") toggles: calling it on an open element closes it, calling it on a closed one opens it.
The JS file is almost optional. Theme/mode swapping, dialogs, menus, snackbars, overlays, pages, and tabs all work through the CSS .active class plus data-ui="#id" clicks the browser handles natively. The runtime is required only for: slider value rendering, textarea auto-resize (on browsers without field-sizing), the file/color/password field wiring, and await ui("theme", …) MDC generation.
Main layout
Any element that contains <main class="main"> becomes a grid with slots for edge .navs, .header, .main, and .footer. You don't need to use every slot.
<nav class="nav left">...</nav>
<nav class="nav right">...</nav>
<nav class="nav top">...</nav>
<nav class="nav bottom">...</nav>
<header class="header responsive | fixed">...</header>
<main class="main responsive">...</main>
<footer class="footer responsive | fixed">...</footer>Grid composition:
nav.left | nav.top | nav.right
nav.left | header | nav.right
nav.left | main | nav.right
nav.left | footer | nav.right
nav.left | nav.bottom | nav.right.responsive caps content at 75 rem and centers it. .fixed makes header / footer sticky. For RTL languages set dir="rtl" on <body> or any ancestor.
Compact (mobile)
<nav class="nav bottom">
<a><i class="icon">home</i><span>Home</span></a>
<a><i class="icon">search</i><span>Search</span></a>
<a><i class="icon">share</i><span>Share</span></a>
</nav>
<main class="main responsive">
<h3 class="h3">Compact</h3>
</main>Medium (tablet rail)
<nav class="nav left">
<a><i class="icon">home</i><span>Home</span></a>
<a><i class="icon">search</i><span>Search</span></a>
<a><i class="icon">share</i><span>Share</span></a>
</nav>
<main class="main responsive">
<h3 class="h3">Medium</h3>
</main>Expanded (desktop drawer)
<nav class="nav left max">
<a><i class="icon">home</i><span>Home</span></a>
<a><i class="icon">search</i><span>Search</span></a>
<a><i class="icon">share</i><span>Share</span></a>
</nav>
<main class="main responsive">
<h3 class="h3">Expanded</h3>
</main>Multi-pane
<nav class="nav left">…</nav>
<main class="main responsive">
<div class="grid">
<div class="s12 m6 l6"><h3 class="h3">Pane 1</h3></div>
<div class="s12 m6 l6"><h3 class="h3">Pane 2</h3></div>
</div>
</main>Empty state
<div class="fill medium-height middle-align center-align">
<div class="center-align">
<i class="icon extra">mail</i>
<h5 class="h5">You have no new messages</h5>
<p class="p">Click the button to start a conversation</p>
<div class="space"></div>
<nav class="nav center-align">
<button class="button round">Send a message</button>
</nav>
</div>
</div>Components
Badge
Corner indicators placed inside another element. Default error-colored.
<button class="button circle">
<i class="icon">mail</i>
<div class="badge">3</div>
</button>
<button class="button chip circle">
<i class="icon">mail</i>
<div class="badge">3</div>
</button>| Class | Result |
| -------------------------------------------------------------------- | ------------------------------------- |
| (none) | Top-right of parent, error colors. |
| .top / .bottom / .left / .right | Corner override. |
| .fill / .primary / .secondary / .tertiary | Role-colored background. |
| .border | Outlined on surface background. |
| .circle / .square / .round / .no-round / corner-round classes | Shape variants. |
| .min | Dot only (no text). |
| .none | Inline (not absolutely positioned). |
Button
<button class="button">Button</button>
<a class="button">Link button</a>
<button class="button">
<i class="icon">home</i>
<span>Button</span>
</button><button class="button"> and <a class="button"> are interchangeable. Variants combine on the same element.
| Class | Result |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| (none) | Filled with primary, 2.5 rem high, fully-rounded. |
| .small / .large / .extra | 2 / 3 / 3.5 rem high. |
| .border | Outlined; primary-text on transparent background. |
| .fill | secondary-container background. |
| .primary / .secondary / .tertiary | Role-colored background. |
| .transparent | Drops background and box-shadow; for icon buttons. |
| .square / .circle | No padding; equal block/inline size. |
| .round / .no-round / .small-round / .large-round | Corner-radius scale. |
| .left-round / .right-round / .top-round / .bottom-round | Per-corner radius. |
| .responsive | Fills the container's inline-size. |
| .horizontal / .vertical | Row vs column flow of children. |
| .extend | Collapses to icon; expands label on hover or .active. |
| .active | primary-container background. |
| [disabled] | 50% opacity, no pointer-events. |
Responsive button
Stretches to fill its container.
<button class="button responsive">Button</button>
<button class="button responsive">
<i class="icon">home</i>
<span>Button</span>
</button>FAB
Floating Action Button — the primary action of a screen.
<button class="button circle extra">
<i class="icon">add</i>
</button>
<button class="button square extra">
<i class="icon">add</i>
</button>
<button class="button circle extra left-round top-round">
<i class="icon">add</i>
</button>Extended FAB
Wider FAB with a text label.
<button class="button extend circle">
<i class="icon">add</i>
<span>Create</span>
</button>
<button class="button extend square">
<i class="icon">add</i>
<span>Create</span>
</button>FAB menu
A FAB that opens a menu of related actions.
<div>
<button class="button circle extra">
<i class="icon">more_horiz</i>
</button>
<menu class="menu group no-wrap small-space top">
<li><button class="button fill"><i class="icon">search</i><span>Search</span></button></li>
<li><button class="button fill"><i class="icon">home</i><span>Home</span></button></li>
<li><button class="button fill"><i class="icon">more_vert</i><span>About</span></button></li>
</menu>
</div>Icon button
A transparent button shaped only on hover/press.
<button class="button transparent circle">
<i class="icon">search</i>
</button>
<button class="button transparent circle">
<img class="responsive" src="/avatar.png">
</button>Button groups
Standard groups, connected groups, and split buttons. Children should use directional corner rounds.
<nav class="nav group">
<button class="button left-round">Left</button>
<button class="button no-round">Center</button>
<button class="button right-round">Right</button>
</nav>
<nav class="nav group connected">
<button class="button left-round">Left</button>
<button class="button no-round">Center</button>
<button class="button right-round">Right</button>
</nav>
<nav class="nav group split">
<button class="button left-round">
<i class="icon">add_circle</i>
<span>Action</span>
</button>
<button class="button right-round square">
<i class="icon">keyboard_arrow_down</i>
</button>
</nav>Add .secondary / .tertiary on nav.group.split to colorize the group.
Card
<article class="card">
<h5 class="h5">Title</h5>
<p class="p">Description</p>
<nav class="nav">
<button class="button">Action</button>
</nav>
</article>| Class | Result |
| ------------------------------------------------------------------------------------------- | ----------------------------------- |
| (none) | Elevated, surface-container-low. |
| .border | No shadow, outline border. |
| .fill / .primary-container / .secondary-container / .tertiary-container | Role-colored background. |
| .small / .medium / .large | 12 / 20 / 32 rem block-size. |
| .round / .no-round / .left-round / .right-round / .top-round / .bottom-round | Corner radius. |
| .padding / .no-padding / .tiny-padding / .small-padding / .medium-padding / .large-padding | Inner padding override. |
| .elevate / .no-elevate / .small-elevate / .medium-elevate / .large-elevate | Shadow override. |
Card with leading media
<article class="card">
<div class="row">
<img class="circle large" src="/avatar.png">
<div class="max">
<h5 class="h5">Title</h5>
<div>Supporting text</div>
</div>
</div>
<nav class="nav">
<button class="button">Action</button>
</nav>
</article>Card with hero media
<article class="card no-padding">
<img class="responsive medium" src="/hero.jpg">
<div class="row absolute bottom left right padding bottom-shadow">
<h5 class="h5">Title</h5>
<div class="max"></div>
<button class="button circle transparent"><i class="icon">more_vert</i></button>
</div>
</article>Side-by-side card
<article class="card no-padding">
<div class="grid no-space">
<div class="s6"><img class="responsive" src="/photo.jpg"></div>
<div class="s6">
<div class="padding">
<h5 class="h5">Title</h5>
<p class="p">Description</p>
<nav class="nav"><button class="button border round">Action</button></nav>
</div>
</div>
</div>
</article>Checkbox
<label class="checkbox">
<input type="checkbox">
<span></span>
</label>
<label class="checkbox">
<input type="checkbox">
<span>Click here</span>
</label>Sizes: .small, (default), .large, .extra.
With icons
<label class="checkbox icons">
<input type="checkbox">
<span>
<i class="icon">close</i>
<i class="icon">done</i>
</span>
</label>Inside a field
<div class="field middle-align">
<nav class="nav">
<label class="checkbox"><input type="checkbox"><span>Item 1</span></label>
<label class="checkbox"><input type="checkbox"><span>Item 2</span></label>
<label class="checkbox"><input type="checkbox"><span>Item 3</span></label>
</nav>
<output>Helper text</output>
</div>Indeterminate state
Set via JavaScript — there is no HTML attribute for this:
document.getElementById("my-checkbox").indeterminate = true;Chip
<button class="button chip">Chip</button>
<button class="button chip">
<i class="icon">home</i>
<span>Chip</span>
</button>
<a class="chip">Link chip</a><button class="chip"> and <a class="chip"> are interchangeable.
| Class | Result |
| ------------------------------------------------------------------------ | ------------------------------- |
| (none) | 2 rem, outlined. |
| .small / .medium / .large | 2 / 2.5 / 3 rem. |
| .fill | Filled (no border). |
| .primary / .secondary / .tertiary | Role-colored background. |
| .border / .no-border | Border control. |
| .circle / .square | Equal block/inline size. |
| .round / .no-round / corner-round classes | Corner radius. |
| .horizontal / .vertical | Child flow. |
Canonical chip patterns:
<!-- Suggestion -->
<button class="button chip">Suggestion</button>
<!-- Input (removable) -->
<button class="button chip">
<span>Input</span>
<i class="icon">close</i>
</button>
<!-- Filter -->
<button class="button chip">
<i class="icon">done</i>
<span>Filter</span>
</button>
<!-- Assist with leading icon -->
<button class="button chip">
<i class="icon primary-text">today</i>
<span>Assist</span>
</button>
<!-- With image -->
<button class="button chip">
<img src="/favicon.png">
<span>Image</span>
</button>Container
The main content of a page.
<main class="main responsive">…</main>.responsive caps inline-size at 75 rem and centers; .max removes the cap.
Dialog
<dialog class="dialog">
<h5 class="h5">Title</h5>
<p class="p">Content of dialog</p>
<nav class="nav right-align no-space">
<button class="button transparent link">Cancel</button>
<button class="button transparent link">Confirm</button>
</nav>
</dialog>| Class | Result |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| (none) | Centered modal, 80% max block-size. |
| .small / .medium / .large | 25% / 50% / 75%. |
| .left / .right | Slides in from the side, full height. With .small 20 rem / .medium 32 rem / .large 44 rem. |
| .top / .bottom | Slides in vertically, full width. With .small 16 rem / .medium 24 rem / .large 32 rem. |
| .max | Full-screen. |
| .modal | Opens via native showModal() (native backdrop, Escape, scroll lock). |
| .fill / .primary-container / .secondary-container / .tertiary-container | Role-colored background. |
| .border / .round / .no-round / corner-round classes | Border and corner radius. |
| .padding / .no-padding / .tiny-padding / .small-padding / .medium-padding / .large-padding | Padding override. |
| .elevate / .no-elevate / .small-elevate / .medium-elevate / .large-elevate | Shadow override. |
| .active | Open state (also [open] from native API). |
Opening and closing — 5 methods
1. Add/remove active class
<dialog class="dialog active">…</dialog>2. Native HTML <dialog> API
document.querySelector("#dialog").show(); // non-modal
document.querySelector("#dialog").showModal(); // modal
document.querySelector("#dialog").close();3. data-ui attribute
<button class="button" data-ui="#dialog">Open</button>
<dialog class="dialog" id="dialog">
<h5 class="h5">Title</h5>
<nav class="nav right-align no-space">
<button class="button" data-ui="#dialog">Cancel</button>
<button class="button" data-ui="#dialog">Confirm</button>
</nav>
</dialog>4. ui() call
ui("#dialog");5. Popover API
<button class="button" popovertarget="dialog">Open</button>
<dialog class="dialog" id="dialog" popover>
<h5 class="h5">Title</h5>
</dialog>document.querySelector("#dialog").showPopover();
document.querySelector("#dialog").hidePopover();
document.querySelector("#dialog").togglePopover();Custom overlay
Place an .overlay sibling immediately before the dialog; modifier classes on the overlay are honored:
<div class="overlay blur"></div>
<dialog class="dialog" id="dialog">…</dialog>Navigation drawer
<div class="overlay"></div>
<dialog id="drawer" class="dialog left">
<header class="header">
<nav class="nav">
<img class="circle large" src="/avatar.png">
<h6 class="h6 max">Title</h6>
<button class="button transparent circle large" data-ui="#drawer">
<i class="icon">close</i>
</button>
</nav>
</header>
<ul class="list">
<li class="wave round"><i class="icon">inbox</i><span class="max">Inbox</span><b>24</b></li>
<li class="wave round"><i class="icon">send</i><span>Outbox</span></li>
<li class="wave round"><i class="icon">favorite</i><span>Favorites</span></li>
<li class="wave round"><i class="icon">delete</i><span>Trash</span></li>
</ul>
</dialog>Divider
<hr class="divider">
<hr class="divider small">
<hr class="divider medium">
<hr class="divider large">
<div class="divider"></div>
<div class="divider vertical"></div>.small / .medium / .large control vertical margin (0.5 / 1 / 1.5 rem). .vertical produces a vertical rule.
Expansion
Use native <details> with <summary class="summary">.
<details>
<summary class="summary">Header</summary>
<p class="p">Body content</p>
</details>Multi-level:
<details>
<summary class="summary">Level 1</summary>
<details>
<summary class="summary">Level 2</summary>
<details>
<summary class="summary">Level 3</summary>
<p class="p">Body</p>
</details>
</details>
</details>Custom summary (hides the marker):
<details>
<summary class="summary none">
<button class="button">Custom trigger</button>
</summary>
<p class="p">Body</p>
</details>Field
Wrapper for inputs, selects, textareas, and composite controls.
<div class="field border">
<input type="text">
</div>.label is optional; with it, the label floats above the input when focused or filled.
<div class="field label border">
<input type="text" placeholder=" ">
<label>Label</label>
</div>| Class | Result |
| ------------------------------------------- | ------------------------------------------------- |
| .label | Adds a floating label child. |
| .border | Outlined. |
| .fill | Filled (surface-variant). |
| .round | Pill shape. |
| .small / (default) / .large / .extra | 2.5 / 3 / 3.5 / 4 rem input height. |
| .prefix | Extra start-padding for a leading icon child. |
| .suffix | Extra end-padding for a trailing icon child. |
| .invalid | Error coloring; shows <output class="invalid">. |
Input
<div class="field border">
<input type="text">
</div>
<div class="field border">
<input type="text">
<output>Helper text</output>
</div>
<div class="field invalid border">
<input type="text">
<output class="invalid">Error text</output>
</div>
<div class="field label border">
<input type="text" placeholder=" ">
<label>Label</label>
</div>
<div class="field label prefix border">
<i class="icon">search</i>
<input type="text">
<label>Label</label>
</div>
<div class="field label suffix border">
<input type="text">
<label>Label</label>
<i class="icon">visibility</i>
</div>
<div class="field label prefix suffix border">
<i class="icon">search</i>
<input type="text">
<label>Label</label>
<i class="icon">tune</i>
</div>Clickable prefix/suffix icons — wrap in <a> or use <i class="front">:
<div class="field label prefix border">
<a><i class="icon">search</i></a>
<input type="text">
<label>Label</label>
</div>
<div class="field label prefix border">
<i class="icon front">search</i>
<input type="text">
<label>Label</label>
</div>Floating-label triggers — either add placeholder=" " (pure CSS) or let the runtime toggle .active:
<!-- CSS-only -->
<div class="field label border">
<input type="text" placeholder=" ">
<label>Label</label>
</div>
<!-- Manual control -->
<div class="field label border">
<input type="text" class="active">
<label class="active">Label</label>
</div>Password fields auto-wire a visibility toggle when a child <i>visibility</i> is present:
<div class="field label suffix border">
<input type="password">
<label>Password</label>
<i class="icon">visibility</i>
</div>Select
<div class="field suffix border">
<select>
<option>Item 1</option>
<option>Item 2</option>
<option>Item 3</option>
</select>
<i class="icon">arrow_drop_down</i>
</div>
<div class="field label suffix border">
<select>
<option>Item 1</option>
<option>Item 2</option>
</select>
<label>Label</label>
<i class="icon">arrow_drop_down</i>
<output>Helper text</output>
</div>Textarea
Auto-resizes unless [rows] is set.
<div class="field border">
<textarea></textarea>
</div>
<div class="field border">
<textarea rows="10"></textarea>
</div>
<div class="field label border">
<textarea placeholder=" "></textarea>
<label>Label</label>
</div>Search
Search field with auto-suggestion menu.
<div class="field large prefix round fill">
<i class="icon front">search</i>
<input>
<menu class="menu min">
<li class="transparent">
<div class="field large prefix">
<i class="icon front">arrow_back</i>
<input>
</div>
</li>
<li><i class="icon">history</i><div>Recent 1</div></li>
<li><i class="icon">history</i><div>Recent 2</div></li>
<li><i class="icon">history</i><div>Recent 3</div></li>
</menu>
</div>FAB-anchored search:
<div>
<button class="button extra circle fill">
<i class="icon">search</i>
</button>
<menu class="menu no-wrap left min">
<li class="transparent">
<div class="field large prefix">
<i class="icon front">arrow_back</i>
<input>
</div>
</li>
<li><i class="icon">history</i><div>Recent 1</div></li>
</menu>
</div>Custom inputs (file, color, date, time)
Wrap a hidden input in a button trigger; the runtime mirrors values back into a sibling text input.
<div>
<button class="button circle"><i class="icon">attach_file</i></button>
<input type="file">
</div>
<div>
<button class="button circle"><i class="icon">palette</i></button>
<input type="color">
</div>
<div>
<button class="button circle"><i class="icon">today</i></button>
<input type="date">
</div>
<div>
<button class="button circle"><i class="icon">schedule</i></button>
<input type="time">
</div>
<!-- With label -->
<div>
<button class="button"><i class="icon">attach_file</i><span>File</span></button>
<input type="file">
</div>Grid
12-column grid with 1 rem gap. Cell wrappers live immediately inside .grid — don't put .grid and .s1–.s12 on the same element.
<div class="grid">
<div class="s12 m6 l3"><h5 class="h5">First</h5></div>
<div class="s12 m6 l3"><h5 class="h5">Second</h5></div>
<div class="s12 m6 l3"><h5 class="h5">Third</h5></div>
<div class="s12 m6 l3"><h5 class="h5">Fourth</h5></div>
</div>| Class | Active |
| ---------------- | ------------------ |
| .s1 … .s12 | Always. |
| .m1 … .m12 | ≥ 601 px. |
| .l1 … .l12 | ≥ 993 px. |
Gap modifiers on .grid: .no-space, .space, .small-space, .medium-space (1.5 rem), .large-space (2 rem).
✅ Do put component elements inside the cell:
<div class="grid">
<div class="s12 m6 l3"><article class="card">…</article></div>
<div class="s12 m6 l3"><div class="field">…</div></div>
</div>🚫 Don't put grid classes directly on the element:
<div class="grid">
<article class="card s12 m6 l3">…</article> <!-- breaks layout -->
</div>Header & Footer
<header class="header">
<nav class="nav">
<button class="button circle transparent"><i class="icon">arrow_back</i></button>
<h5 class="h5 max">Title</h5>
<button class="button circle transparent"><i class="icon">more_vert</i></button>
</nav>
</header>
<footer class="footer">
<nav class="nav">
<button class="button circle transparent"><i class="icon">check_box</i></button>
<button class="button circle transparent"><i class="icon">brush</i></button>
<div class="max"></div>
<button class="button square round extra primary"><i class="icon">add</i></button>
</nav>
</footer>| Class | Result |
| ---------------------------------- | ------------------------------------------------------------- |
| (none) | 4 rem min (header), 5 rem min (footer). |
| .fixed | Sticky inside the container. |
| .responsive | Caps content at 75 rem and centers it. |
| .max | Removes the 75 rem cap. |
When .fixed and a scrollable parent are used together, header/footer stick to the parent's top/bottom edge:
<article class="card small-width small-height scroll">
<header class="header fixed bold">Fixed header</header>
<p class="p">Long body content…</p>
<p class="p">…that scrolls underneath.</p>
<footer class="footer fixed bold">Fixed footer</footer>
</article>Icon
Material Symbols ligature. Four font variants ship inside the CSS bundle.
<i class="icon">home</i>
<i class="icon">
<svg viewBox="0 0 24 24">
<path d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z"></path>
</svg>
</i>
<i class="icon">
<img src="/icon.png">
</i>| Class | Size |
| -------- | -------- |
| .tiny | 1 rem |
| .small | 1.25 rem |
| (none) | 1.5 rem |
| .large | 1.75 rem |
| .extra | 2 rem |
| .fill | Filled (FILL=1) variation. Also auto-applied inside <a class="active"> / <button class="active">. |
Switch font variant via --font-icon:
:root { --font-icon: "Material Symbols Rounded"; }
/* "Material Symbols Outlined" (default), "Material Symbols Rounded",
"Material Symbols Sharp", "Material Symbols Subset" (smaller bundle), or none */Sharing one SVG sprite across multiple icons:
<svg viewBox="0 0 24 24" style="display: none">
<g id="home"><path d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z"/></g>
<g id="star"><path d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/></g>
</svg>
<i class="icon"><svg viewBox="0 0 24 24"><use href="#home"/></svg></i>
<i class="icon"><svg viewBox="0 0 24 24"><use href="#star"/></svg></i>Third-party icon fonts work too (Font Awesome, MDI, …) — load them yourself:
<i class="icon fa-regular fa-clock"></i>
<i class="icon mdi mdi-clock-outline"></i>Layout
Positioning containers — absolute (relative to parent) or fixed (relative to viewport).
<article class="card small">
<div class="absolute left bottom right">
<h5 class="h5">Pinned to container bottom</h5>
</div>
</article>
<div class="fixed left bottom right">
<h5 class="h5">Pinned to viewport bottom</h5>
</div>| Class | Effect |
| -------------------------------------------------------------- | ----------------------------------------------- |
| .absolute / .fixed | Positioning context. |
| .left / .right / .top / .bottom | Pin to that edge. |
| .front / .back | z-index +10 / −10. |
| .center / .middle | Horizontal / vertical centering. |
| .small / .medium / .large | With .left.right: 20 / 28 / 44 rem block-size. With .top.bottom: same inline-size. |
<article class="card small">
<span class="absolute center middle">Centered</span>
</article>
<article class="card small center-align middle-align">
<span>Centered via alignment</span>
</article>List
<ul class="list">
<li>Item</li>
<li>Item</li>
</ul>
<ol class="list">
<li>Item</li>
</ol>| Class | Result |
| --------------------------------------------------------- | ---------------------------- |
| (none) | 3.5 rem rows, no dividers. |
| .border | Bottom divider between rows. |
| .no-space / .space / .small-space | 2.5 / 3 / 3.5 rem rows. |
| .medium-space / .large-space | 4.5 / 5.5 rem rows. |
Nested
<ul class="list">
<li>Item</li>
<li>
<ul class="list">
<li>Nested</li>
<li>Nested</li>
</ul>
</li>
</ul>Expansion list
<ul class="list">
<li>Item</li>
<li>
<details>
<summary class="summary">Header</summary>
<ul class="list">
<li>Inner</li>
</ul>
</details>
</li>
</ul>Headline and supporting text
<ul class="list border">
<li>
<button class="button circle">A</button>
<div class="max">
<h6 class="h6 small">Headline</h6>
<div>Supporting text</div>
</div>
<label>+15 min</label>
</li>
</ul>With leading icon or avatar:
<ul class="list">
<li>
<i class="icon">home</i>
<div class="max">
<h6 class="h6 small">Headline</h6>
<div>Supporting text</div>
</div>
<label>+15 min</label>
</li>
<li>
<img class="round" src="/avatar.png">
<div class="max">
<h6 class="h6 small">Headline</h6>
<div>Supporting text</div>
</div>
</li>
</ul>Media
<img src="/image.png" class="circle extra">
<video class="circle extra">
<source src="/video.mp4" type="video/mp4">
</video>
<svg class="circle extra" viewBox="0 0 24 24">
<path d="…"/>
</svg>| Class | Result |
| ------------------------------------------------------------------ | --------------------------------------------------- |
| .tiny / .small / (default) / .large / .extra | 2 / 2.5 / 3 / 3.5 / 4 rem square. |
| .circle / .round / .square | Border-radius style. |
| .no-round / .left-round / .right-round / .top-round / .bottom-round | Per-corner radius. |
| .responsive | 100% inline-size. |
| .responsive.tiny / .small / .medium / .large / .extra | Fixed block-size 4 / 8 / 12 / 16 / 20 rem. |
| .empty-state | Max 24 rem wide; for empty-state illustrations. |
Menu
<div>
<button class="button">
<span>Menu</span>
<i class="icon">arrow_drop_down</i>
</button>
<menu class="menu">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</menu>
</div>With links:
<div>
<button class="button">
<span>Links</span>
<i class="icon">arrow_drop_down</i>
</button>
<menu class="menu">
<li><a href="#">Item 1</a></li>
<li><a href="#">Item 2</a></li>
</menu>
</div>With divider:
<menu class="menu">
<li>Item 1</li>
<li><hr class="divider"></li>
<li>Item 2</li>
</menu>Selected state:
<menu class="menu">
<li>Item 1</li>
<li class="active">Item 2</li>
<li>Item 3</li>
</menu>| Class | Result |
| --------------------------------------------------------------------------------------- | ----------------------------------------------- |
| (none) | Anchored below trigger, full width. |
| .no-wrap | Width fits content; never wraps. |
| .wrap | Allow item wrap. |
| .min | Compact anchored popover. |
| .max | Full-screen. |
| .top / .bottom / .left / .right | Open direction / alignment. |
| .border | Outlined. |
| .group | Visually merges with parent (no shadow / bg). |
| .no-space / .space / .tiny-space / .small-space / .medium-space / .large-space / .extra-space | Gap scale. |
Submenu
<div>
<button class="button">
<span>Menu</span>
<i class="icon">arrow_drop_down</i>
</button>
<menu class="menu">
<li>
<span>Submenu</span>
<menu class="menu">
<li>Item</li>
<li>Item</li>
</menu>
</li>
</menu>
</div>Grouped menu
.group makes the menu transparent and stacks groups visually.
<div>
<button class="button">
<span>Grouped</span>
<i class="icon">arrow_drop_down</i>
</button>
<menu class="menu group">
<li>
<menu class="menu">
<li>Item</li>
<li>Item</li>
</menu>
</li>
<li>
<menu class="menu">
<li>Item</li>
<li>Item</li>
</menu>
</li>
</menu>
</div>Opening and closing
Three patterns:
- Active class:
<menu class="active">…</menu> data-uiattribute:<div data-ui="#menu"><menu id="menu">…</menu></div>ui()call:ui("#menu")
Clicking outside an open menu closes it automatically.
Navigation
<nav class="nav"> and .row are interchangeable containers — <nav> is semantic, .row is not.
<nav class="nav">
<button class="button">Button</button>
<a class="chip">Chip</a>
<a><img class="circle" src="/avatar.png"></a>
<label class="checkbox"><input type="checkbox"></label>
</nav>
<div class="row">
<div>min</div>
<div class="max">max</div>
<div>min</div>
</div>| Class on <nav> / .row | Result |
| ----------------------------------------------------------------------------------------------- | ------------------------------------- |
| .no-space / .tiny-space / .small-space / .medium-space / .large-space | Gap: 0 / 0.5 / 1 / 1.5 / 2 rem. |
| .left-align / .center-align / .right-align | Justify-content (also text-align). |
| .top-align / .middle-align / .bottom-align | Align-items. |
| .horizontal / .vertical | Flow direction. |
| .wrap / .no-wrap | Flex-wrap behavior. |
| .min | Inline-flex; shrinks to content. |
| .max | flex: 1 (children with .max grow). |
| .border / .round / .no-round / corner-round classes | Border and corner radius. |
| .margin / .no-margin / .tiny-margin / .small-margin / .medium-margin / .large-margin | Outer margin. |
| .fill / .primary-container / .secondary-container / .tertiary-container | Role-colored background. |
| .elevate / .no-elevate / .small-elevate / .medium-elevate / .large-elevate | Shadow. |
Navigation rail (vertical edge)
<nav class="nav left">
<a><i class="icon">home</i><div>Home</div></a>
<a><i class="icon">search</i><div>Search</div></a>
<a><i class="icon">more_vert</i><div>More</div></a>
</nav>
<nav class="nav left max">
<a><i class="icon">home</i><div>Home</div></a>
<a><i class="icon">search</i><div>Search</div></a>
</nav>Navigation bar (horizontal edge)
<nav class="nav bottom">
<a><i class="icon">home</i><div>Home</div></a>
<a><i class="icon">search</i><div>Search</div></a>
<a><i class="icon">more_vert</i><div>More</div></a>
</nav>
<nav class="nav top max">
<a><i class="icon">home</i><div>Home</div></a>
</nav>Tabbed nav
<nav class="nav tabbed">
<a class="active"><i class="icon">info</i><span>Overview</span></a>
<a><i class="icon">style</i><span>Specs</span></a>
<a><i class="icon">design_services</i><span>Guidelines</span></a>
</nav>nav.tabbed: default 4 rem height, .small 3 rem, .large 5 rem.
Toolbar
<nav class="nav toolbar">
<a><i class="icon">videocam_off</i></a>
<a><i class="icon">mic</i></a>
<a class="active"><i class="icon">front_hand</i></a>
<a><i class="icon">more_vert</i></a>
</nav>.fill (primary-container), .vertical, .max (full width) all work on nav.toolbar.
Group / connected / split
<nav class="nav group">
<button class="button left-round">Left</button>
<button class="button no-round">Center</button>
<button class="button right-round">Right</button>
</nav>
<nav class="nav group connected">
<button class="button left-round">Left</button>
<button class="button no-round">Center</button>
<button class="button right-round">Right</button>
</nav>
<nav class="nav group split">
<button class="button left-round"><i class="icon">add_circle</i><span>Action</span></button>
<button class="button right-round square"><i class="icon">keyboard_arrow_down</i></button>
</nav>List form
<nav class="nav">
<ul>
<li><button class="button">Button</button></li>
<li><a class="chip">Chip</a></li>
</ul>
</nav>Overlay
Scrim that blocks the screen. Used with dialogs or as a standalone loading curtain.
<div class="overlay center-align middle-align">
<progress class="progress circle"></progress>
</div>| Class | Result |
| --------------------------------------------------------------------------- | ------------------------------- |
| .active | Shown. |
| .blur / .small-blur / .medium-blur / .large-blur | Backdrop-blur effect. |
| .left-align / .right-align / .center-align / .top-align / .bottom-align / .middle-align | Align inner content. |
Opening and closing — same three patterns:
- Active class:
<div class="overlay active">…</div> data-ui:<button data-ui="#overlay">Show</button>+<div class="overlay" id="overlay">…</div>ui():ui("#overlay")
Page
<div class="page active">
<h5 class="h5">Title</h5>
</div>| Class | Result |
| ---------------------------------------------- | ------------------------------------- |
| .active | Visible. |
| .left / .right / .top / .bottom | Entry transform direction. |
Activating a page — three patterns:
- Active class: add
.activeto one, remove from siblings. data-ui:<a data-ui="#page1">Open</a>— siblings at the same level deactivate.ui():ui("#page1").
Progress
<progress class="progress"></progress> <!-- indeterminate -->
<progress class="progress" value="25" max="100"></progress> <!-- linear -->
<progress class="progress wavy"></progress>
<progress class="progress wavy" value="25" max="100"></progress>
<progress class="progress circle"></progress> <!-- circular indeterminate -->
<progress class="progress circle" value="25" max="100"></progress>
<progress class="progress circle wavy"></progress>
<progress class="progress circle wavy" value="25" max="100"></progress>| Class | Result |
| ---------------- | -------------------------------------------- |
| (none) | Linear, 0.25 rem thick. |
| .small / .medium / .large | 0.25 / 0.35 / 0.45 rem thick. |
| .indeterminate | Forces animated indeterminate state. |
| .wavy | Wavy SVG track (linear or circle). |
| .circle | Circular; .small 1.5 rem / (default) 2.5 / .large 3.5. |
| .max | Absolutely fills the parent (use inside <article>, <button>, …). |
A bare <progress></progress> is auto-promoted to indeterminate at boot.
Radio
<label class="radio">
<input type="radio">
<span></span>
</label>
<label class="radio">
<input type="radio">
<span>Click here</span>
</label>Sizes: .small, (default), .large, .extra.
With icons
<label class="radio icons">
<input type="radio">
<span>
<i class="icon">close</i>
<i class="icon">done</i>
</span>
</label>Group inside a field
<div class="field middle-align">
<nav class="nav">
<label class="radio"><input type="radio" name="g"><span>Item 1</span></label>
<label class="radio"><input type="radio" name="g"><span>Item 2</span></label>
<label class="radio"><input type="radio" name="g"><span>Item 3</span></label>
</nav>
<output>Helper text</output>
</div>Shape
SVG-masked decorative element. Place inside <button>, <div>, or a sized container.
<div class="shape sunny"></div>
<div class="shape sunny">
<img class="responsive" src="/favicon.png">
</div>
<div class="small-width small-height">
<div class="shape sunny max"></div>
</div>| Class | Result |
| ------------------------------------------------------------------------------ | ------------------------------- |
| .tiny / .small / (default) / .medium / .large / .extra | 2.5 / 3 / 3.5 / 4.5 / 5.5 / 6.5 rem. |
| .max | Fills the parent. |
| .space / .no-space / .tiny-space / .small-space / .medium-space / .large-space / .extra-space | Inner mask padding. |
| .rotate / .slow-rotate / .fast-rotate | Rotation animation (12 s / 24 s / 6 s). |
Shape names (one per class):
arch, arrow, boom, bun, burst, circle, clamshell, diamond, fan, flower, gem, ghost-ish, heart, leaf-clover4, leaft-clover8, loading-indicator, oval, pentagon, pill, pixel-circle, pixel-triangle, puffy, puffy-diamond, semicircle, sided-cookie4, sided-cookie6, sided-cookie7, sided-cookie9, sided-cookie12, slanted, soft-boom, soft-burst, square, sunny, triangle, very-sunny, wavy, wavy-circle.
Inside a button:
<button class="button circle extra transparent">
<span class="shape sided-cookie12 max medium-space"></span>
</button>Spinner:
<div class="shape sided-cookie12 transparent rotate">
<button class="button responsive">
<i class="icon">search</i>
</button>
</div>Slider
Default range is 0–100.
<div class="slider">
<input type="range">
<span></span>
</div>
<div class="slider">
<input type="range" min="4" max="8">
<span></span>
</div>| Class | Track size |
| ------------------------------------------------------------------ | ---------- |
| .tiny | 1 rem |
| .small | 1.5 rem |
| .medium | 2.5 rem |
| .large | 3.5 rem |
| .extra | 6 rem |
| .vertical | Rotates 90°. |
| .max | Fills the parent (container slider). |
Dual thumb
<div class="slider">
<input type="range" value="25">
<input type="range" value="50">
<span></span>
</div>Value tooltip
<div class="slider">
<input type="range">
<span></span>
<div class="tooltip"></div>
</div>Inset icon
The icon appears only with .medium, .large, or .extra.
<div class="slider medium">
<input type="range">
<span><i class="icon">sunny</i></span>
</div>Inside a field
<div class="field middle-align">
<div class="slider">
<input type="range">
<span></span>
</div>
<output>Helper</output>
</div>Container slider
<article class="card">
<div class="slider max">
<input type="range">
<span></span>
</div>
</article>Snackbar
<div class="snackbar">
<i class="icon">warning</i>
<span>I'm a snackbar</span>
</div>| Class | Result |
| ----------------------------------------------------------- | --------------------------------------- |
| (none) | Bottom-centered; auto-hides after 6 s. |
| .top / .bottom | Anchor position. |
| .error / .primary / .secondary / .tertiary | Role-colored background. |
| .active | Shown. |
With action
<div class="snackbar">
<div class="max">Item moved to trash</div>
<a class="inverse-primary-text">Undo</a>
</div>Opening — four patterns:
Active class: add
.active.data-ui:<button data-ui="#snack">Show</button>.ui():ui("#snack")— default 6000 ms;ui("#snack", 3000)for a custom timeout;ui("#snack", -1)to keep open until click.Popover API:
<button class="button" popovertarget="snack">Show</button> <div class="snackbar" id="snack" popover>I'm a snackbar</div>document.querySelector("#snack").showPopover(); document.querySelector("#snack").hidePopover(); document.querySelector("#snack").togglePopover();
Only one snackbar shows at a time — opening one dismisses any open siblings.
Switch
<label class="switch">
<input type="checkbox">
<span></span>
</label>
<nav class="nav">
<div class="max">
<h6 class="h6">Title</h6>
<div>Complementary text</div>
</div>
<label class="switch">
<input type="checkbox">
<span></span>
</label>
</nav>Sizes: .small, (default), .large, .extra.
With icons
<label class="switch icons">
<input type="checkbox">
<span><i class="icon">wifi</i></span>
</label>
<label class="switch icons">
<input type="checkbox">
<span>
<i class="icon">close</i>
<i class="icon">done</i>
</span>
</label>Inside a field
<div class="field middle-align">
<nav class="nav">
<div class="max">
<h6 class="h6">Title</h6>
<div>Complementary text</div>
</div>
<label class="switch">
<input type="checkbox">
<span></span>
</label>
</nav>
</div>Table
<table class="table">
<thead>
<tr><th>Header</th><th>Header</th></tr>
</thead>
<tbody>
<tr><td>Cell</td><td>Cell</td></tr>
<tr><td>Cell</td><td>Cell</td></tr>
</tbody>
<tfoot>
<tr><th>Footer</th><th>Footer</th></tr>
</tfoot>
</table>| Class on <table> | Result |
| ---------------------------------------------------------------------------------------- | ----------------- |
| .border | Row dividers. |
| .stripes | Zebra rows. |
| .no-space / .space / .small-space / .medium-space / .large-space | Cell padding. |
| .left-align / .center-align / .right-align | Text alignment. |
<th class="fixed">, <thead class="fixed">, <tfoot class="fixed"> for sticky headers/footers inside a scrolling container. <td class="min"> / <th class="min"> shrinks the cell to its content.
If you want to change the background of the alternate rows of the table:
.my-striped-table {
--active: var(--secondary-container); /* any md3 role token */
}Apply the class on the (or an ancestor).
Scroll
<div class="scroll small-height">
<table class="table">
<thead class="fixed">
<tr><th>Header</th><th>Header</th></tr>
</thead>
<tbody>
<tr><td>Cell</td><td>Cell</td></tr>
</tbody>
</table>
</div>Tabs
<div class="tabs">
<a class="active">Tab 1</a>
<a>Tab 2</a>
<a>Tab 3</a>
</div>
<div class="page padding active"><h5 class="h5">Tab 1</h5></div>
<div class="page padding"><h5 class="h5">Tab 2</h5></div>
<div class="page padding"><h5 class="h5">Tab 3</h5></div>| Class on .tabs | Result |
| --------------------------------------------------------- | --------------------- |
| .small / .large | 2 / 4 rem min height. |
| .min | Shorter active underline. |
| .max | Tabs grow to fill. |
| .left-align / .center-align / .right-align | Justify-content. |
| .horizontal / .vertical | Flow direction. |
Activating tabs — two patterns:
- Active class: add
.activeto one tab<a>and one matching.page, remove from siblings. data-ui:<a data-ui="#page1">Tab 1</a>— the matching.pageactivates, siblings hide.
Tooltip
A child of the trigger element.
<button class="button">
<span>Button</span>
<span class="tooltip">I'm a tooltip</span>
</button>
<div>
<button class="button chip round">Hover me</button>
<div class="tooltip">I'm a tooltip</div>
</div>| Class | Result |
| -------------------------------------------------- | ----------------------------------- |
| (none) | Above the parent. |
| .left / .right / .bottom | Position override. |
| .small / .medium / .large | 8 / 12 / 16 rem wide, multi-line. |
| .max | Block-level 20 rem rich tooltip. |
| .no-space / .medium-space / .large-space | Gap from anchor: 0 / −1 / −1.5 rem. |
Rich tooltip
<div>
<button class="button chip round">Rich tooltip</button>
<div class="tooltip max">
<p class="p">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<nav class="nav">
<button class="button">Action</button>
</nav>
</div>
</div>Typography
<h1 class="h1">Display</h1>
<h2 class="h2">Display</h2>
<h3 class="h3">Display</h3>
<h4 class="h4">Headline</h4>
<h5 class="h5">Headline</h5>
<h6 class="h6">Headline</h6>Heading size modifiers: .small, .large.
Formatting
<a class="link">link</a>
<a class="inverse-link">inverse-link</a>
<p class="p italic">italic</p>
<p class="p bold">bold</p>
<p class="p underline">underline</p>
<p class="p overline">overline</p>
<p class="p upper">upper</p>
<p class="p lower">lower</p>
<p class="p capitalize">capitalize</p>
<p class="p small-text">small (0.75 rem)</p>
<p class="p medium-text">medium (0.875 rem)</p>
<p class="p large-text">large (1 rem)</p>
<p class="p truncate">long single-line text…</p>Line spacing
.no-line, .tiny-line (1.25 rem), .small-line (1.5), .medium-line (1.75), .large-line (2), .extra-line (2.25).
Blockquote, pre, code
<blockquote class="blockquote">Quoted text.</blockquote>
<pre class="pre">Preformatted.</pre>
<pre class="pre">
<code class="code">console.log("hello");</code>
</pre>
<p class="p">
The function <code class="code">console.log()</code> prints a log message.
</p><pre> and <blockquote> modifiers: .border, .no-border, .scroll.
Helpers
Alignments
<div class="center-align middle-align">…</div>.left-align, .right-align, .center-align (justify + text), .top-align, .middle-align, .bottom-align (align-items).
Blurs
Backdrop-blur with a translucent background.
<header class="header blur">…</header>
<article class="card blur">…</article>
<button class="button blur">Bl