Material 3 Expressive × Bootstrap 5.3
Bootstrap markup, Material 3 Expressive design.
Keep writing standard Bootstrap HTML. These docs show what each component looks like after the restyle, the extra M3 classes the framework adds, the HTML to copy, and the optional scripts that bring in springs, ripples and the components CSS can't do on its own.
Getting started
Introduction
Booterial is a stylesheet and a few optional scripts that restyle Bootstrap 5.3 to Google's Material 3 Expressive design. Every value is checked against Jetpack Compose's material3 source, Google's reference implementation.
There's no build step: plain CSS custom properties and vanilla JavaScript, so it works from a file on disk, a CDN or any CMS template. The theme comes from one seed color. Every color role is generated in HCT (Material's color space) and each pairing the framework draws is contrast-checked against WCAG before it's written out.
- Bootstrap markup keeps working.
.btn,.form-control,.dropdown,.cardand the rest pick up M3 colors, shapes, type and motion automatically. - M3-only components get their own classes, such as
.btn-fab,.chip,.nav-rail,.loading-indicatorand.slider, all documented below. - Motion uses springs. The theme solves M3's spring constants into CSS
linear()easings, so overshoot and settle come from the physics. - The scripts are optional. Everything renders without them; they add what CSS can't do (ripples, sliding indicators, spring momentum, snackbars and so on).
Quick start
Load Bootstrap, then the framework's CSS and your theme. Add the scripts you want after Bootstrap's bundle.
Starter template
Load order matters: m3-bootstrap.css after Bootstrap, then your generated m3-theme.css. In scripts, m3-color.js must come before m3-theme.js, which must come before m3-customizer.js.
Production sites only need m3-bootstrap.css, a theme file and whichever optional scripts you use. The color science, theme generator and customizer (m3-color.js, m3-theme.js, m3-customizer.js) are for building the theme. Export a theme.css from the customizer and ship that.
Files
What each file does and whether you need it.
css/m3-bootstrap.cssRequired. Maps M3 tokens onto Bootstrap and adds the M3 components.
css/m3-theme.cssRequired. Your theme: color, shape, type and motion tokens. Generate your own with the customizer.
css/m3-shapes.cssOptional. The 35 Material shapes as .md-shape-* clip classes.
js/m3-components.jsOptional. Ripples, sliders, tab indicators, menus, snackbars, toolbars, app-bar collapse and more.
js/m3-pickers.jsOptional. Date, time and color pickers for fields marked data-m3-datepicker, data-m3-daterange, data-m3-timepicker or data-m3-colorpicker.
js/m3-springs.jsOptional. Real spring physics for switches and progress, so interrupted motion keeps its momentum.
js/m3-color.jsTheme building. HCT color science (a port of material-color-utilities).
js/m3-theme.jsTheme building. Turns a config into color schemes, a contrast report and theme CSS.
js/m3-customizer.js + css/m3-customizer.cssTheme building. The live customizer panel on this page.
assets/shapes/*.svgOptional. Exact SVG outlines of the 35 Material shapes.
The material icon font (Material Symbols Rounded) is loaded from Google Fonts in the starter template. Any icon set works; the framework sizes .material-symbols-rounded and .material-symbols-outlined automatically.
Theming & customizer
A theme is a small config (seed color, scheme variant, contrast level, shape, type and motion) turned into CSS custom properties. Use the customizer on this page to build one, then export it.
Using the customizer
- Open it with the Customize button in the top bar. Every change applies live to this whole page.
- Pick a seed color, a scheme variant and a contrast level. Turn on Match exact brand color if the brand hex must appear as-is; it's used wherever it can still pass contrast.
- Adjust shape (corner scale and per-component corners), fonts and elevation.
- Export theme.css and include it after
m3-bootstrap.css. The config is embedded in the file, so you can import it back later to keep editing.
Add the customizer to any page that loads the framework by including m3-color.js, m3-theme.js and m3-customizer.js (plus css/m3-customizer.css), then open it with:
Contrast guarantees
Each contrast level sets the minimum ratio for text and for UI parts. When a generated color misses the target against the surface it's drawn on, the generator shifts its tone until it passes, and the customizer's report lists any pair it had to adjust.
| Level | Text | UI (borders, icons, indicators) |
|---|---|---|
| Standard | 4.5:1 (WCAG AA) | 3:1 |
| Medium | 7:1 (WCAG AAA) | 4.5:1 |
| High | 11:1 | 7:1 |
Theme tokens
Everything the theme controls is a custom property, so you can read them in your own CSS:
| Token family | Example |
|---|---|
| Color roles | var(--md-sys-color-primary), var(--md-sys-color-on-surface-variant) |
| Type scale | var(--md-sys-typescale-title-medium-size), -weight, -line-height, -font |
| Shape | var(--md-sys-shape-corner-large) |
| Elevation | var(--md-sys-elevation-level2) |
| Motion | var(--md-motion-fast-spatial) (duration + spring easing, ready for transition) |
| State layers | var(--md-sys-state-hover-state-layer-opacity) (8%), -pressed- (10%) |
Light & dark mode
The theme file contains both schemes. Bootstrap's data-bs-theme attribute picks one.
Set data-bs-theme="light" or "dark" on <html> (or any container, to theme just that part). With the customizer loaded, buttons carrying data-m3-mode switch modes and remember the choice:
To avoid a flash of the wrong mode on load, set the attribute before the page paints. This is the snippet this page uses in its <head>:
Bootstrap classes
Bootstrap's own classes keep working. Component modifiers are translated into M3; utility classes always win.
Component sizes
| Bootstrap class | Becomes |
|---|---|
.btn-sm, .btn-group-sm | M3 extra-small button (32px) |
.btn | M3 small button (40px), the default |
.btn-lg, .btn-group-lg | M3 medium button (56px) |
| (no Bootstrap equivalent) | .btn-size-l (96px) and .btn-size-xl (136px) |
Utilities
Bootstrap utilities are !important, so they override the framework every time. That's fine for layout, but on components it can switch off M3 behavior:
| Utility | Effect on M3 components |
|---|---|
m-*, w-*, d-*, flex-*, gap-* | Safe. Nothing the framework animates is touched. |
px-* | Safe on buttons. Replaces the side padding (24px by default). |
py-* | Avoid on buttons: they grow past their M3 height. Use a size class. |
rounded-* | Mapped to M3 corners, but freezes the shape: no press morph, no toggle shape change. |
shadow-sm / shadow / shadow-lg | Mapped to elevation levels 1 / 2 / 3, but block hover lift on elevated buttons and FABs. |
bg-*, text-* | M3 colors, but they override the hover and pressed tint. On buttons use the button variants instead. |
Extra utilities
The framework adds M3-aware utilities alongside Bootstrap's:
| Class | What it does |
|---|---|
.bg-surface, .bg-surface-dim, .bg-surface-bright | Surface background with its on-color text. |
.bg-surface-container-lowest … -highest | The five surface containers, with on-surface text. |
.bg-primary-container, .bg-secondary-container, .bg-tertiary-container, .bg-error-container | Container colors with their on-container text. |
.bg-surface-variant, .bg-inverse-surface, .bg-tertiary | Other roles, each with its paired text color. |
.text-on-surface, .text-on-surface-variant, .text-outline, .text-tertiary | Text color roles. |
.elevation-0 … .elevation-5 | M3 elevation levels. |
.rounded-m3-xs, -sm, -md, -lg, -xl, -full | M3 corner sizes. |
.md-display-large … .md-label-small | Type scale roles (see Typography). |
Foundations
Color
M3 color roles, generated from your seed and contrast-checked. Each tile shows a role with the "on" color meant to sit on it. The hex values update as you change the theme.
Using color roles
Roles come in pairs: a background role and its on role for text and icons on top of it (primary / on-primary, surface-container / on-surface). Pair them as designed and contrast is guaranteed. Bootstrap's theme colors are mapped onto the roles, so .btn-primary, .text-bg-primary and .bg-primary all use your M3 primary.
In your own CSS, use the custom properties directly:
Semantic colors
M3 has only an error role. The framework adds success, warning and info (each with container and on-colors) so Bootstrap's status classes keep working. By default they're harmonized, nudged slightly toward your seed hue, and they get the same contrast checks.
Typography
The M3 type scale: 15 roles, each also available in its Expressive emphasized weight. Fonts, scale and heading weight come from the theme.
Type classes
Apply a role with .md-<role>. Add .md-emphasized for the Expressive emphasized weight.
Headline small
Body large, for long-form reading.
Label large, emphasized
Bootstrap headings
Plain headings and Bootstrap's display classes are mapped onto the scale:
| Element / class | M3 role |
|---|---|
h1, .h1 | display-small |
h2 | headline-large |
h3 | headline-medium |
h4 | headline-small |
h5 | title-large |
h6 | title-medium |
.display-2 / -3 / -4 | display-large / medium / small |
.lead | title-large in the regular weight |
small, .small | body-small |
Shape
The M3 corner scale, scaled by the theme's corner setting. Components pick their corners from it (buttons full, cards medium, dialogs extra-large and so on).
Corner utilities
Bootstrap's .rounded, .rounded-1 … .rounded-pill map onto the same scale.
Shape library
The 35 M3 Expressive shapes, generated from Compose's MaterialShapes. Load css/m3-shapes.css and add .md-shape-<name> to clip any square element. The exact SVG outlines are in assets/shapes/.
Shapes are drawn in a square. Give the element a 1:1 box (width and height, or aspect-ratio: 1) unless you want it stretched.
Names: circle square slanted arch fan arrow semi-circle oval pill triangle diamond clam-shell pentagon gem sunny very-sunny cookie-4 cookie-6 cookie-7 cookie-9 cookie-12 ghostish clover-4 clover-8 burst soft-burst boom soft-boom flower puffy puffy-diamond pixel-circle pixel-triangle bun heart.
Elevation
Six M3 elevation levels. Components use them automatically (elevated card 1, menu 2, FAB and snackbar 3); use .elevation-0 … .elevation-5 elsewhere.
Bootstrap's .shadow-sm, .shadow and .shadow-lg map to levels 1, 2 and 3. The theme's elevation style (M3, soft or none) changes every level at once.
Motion
M3 motion is defined as springs (damping and stiffness), not curves. The theme solves each spring and writes it as a CSS linear() easing, so overshoot and settle come from the physics and nothing needs JavaScript at runtime.
Spring tokens
Spatial springs move position, size and shape, and may overshoot. Effects springs drive color and opacity, and never overshoot. Click a card to play it; all plots share one time axis.
Using motion tokens
Each token is a ready-made transition value (duration and spring easing). Use spatial for anything that moves or changes size, effects for color and opacity:
| Token | Use for |
|---|---|
--md-motion-fast-spatial | Small, quick moves: switch handles, toggles, button shape morphs, menus opening. |
--md-motion-default-spatial | Most component motion: tab indicators, sheets, toolbars. |
--md-motion-slow-spatial | Large, full-screen moves. |
--md-motion-fast-effects / -default-effects / -slow-effects | Color, opacity and anything that mustn't overshoot. |
Reduced motion and slow motion
Every duration is multiplied by --md-motion-scale. It's 0 when the visitor prefers reduced motion, so everything snaps. Set it higher to slow the page down while you study a transition (the playback speed buttons above do exactly that):
Components in motion
Stock markup, animated by the theme's springs.
Switch
The handle slides on fast-spatial and grows 16→24px (28px while pressed).
Shape morph on press
Hold a button down: its corners tighten, then spring back on release.
Progress values on a spring
Value changes ride default-spatial. Jump between values to see the spring.
Spring playground
Dial in any spring. The ball, switch and button in this panel use it in place of fast-spatial, so you can feel a value before it becomes a token, then copy the CSS.
- Settles in
- Overshoot
Icons
The framework is built around Material Symbols (Rounded or Outlined). Load the font, then use a span with the icon's name.
- Inside buttons, chips, menus, nav items and fields, icons are sized and spaced for you; a button's first icon is treated as a leading icon.
- Mark decorative icons
aria-hidden="true". On icon-only buttons put the label on the button (aria-label). - Selected toggle icon buttons switch to the filled style automatically.
Actions
Buttons
Buttons let people take an action with one tap. Their emphasis tells people which action matters most.
Use it for
- The most important action gets a filled button; the others step down to tonal, outlined or text
- Short verb labels: "Save", "Send", "Add to cart"
- One filled button per view where possible
Use something else
- A screen's single main action: a FAB
- Compact actions in toolbars, app bars or rows: icon buttons
- Picking between options: toggle buttons or a connected group
From Bootstrap: Bootstrap's variants keep working and map to M3 emphasis levels. Buttons become pill-shaped, sized from M3's five sizes, and tighten their corners when pressed.
Emphasis
Pick by how important the action is: filled for the main action, tonal for a secondary one, elevated when it has to stand off a busy background, outlined for medium emphasis, text for the least.
| Class | M3 button |
|---|---|
.btn-primary | Filled: primary / on-primary. |
.btn-tonal | Filled tonal: secondary-container. |
.btn-elevated | Elevated: surface-container-low, level 1 shadow, primary label. |
.btn-outlined (also .btn-outline-primary, .btn-outline-secondary) | Outlined: outline-variant border, on-surface-variant label. |
.btn-text (also .btn-link) | Text: primary label, no container. |
Bootstrap variants
The other Bootstrap variants map to M3 roles. .btn-tertiary is added for the tertiary role. The outlined status variants keep their color, since M3 has no equivalent.
Sizes
Five Expressive sizes. Bootstrap's .btn-sm and .btn-lg map to extra-small and medium.
| Class | Height | Side padding | Icon | Label |
|---|---|---|---|---|
.btn-size-xs / .btn-sm | 32px | 16px | 20px | label-large |
| default | 40px | 24px | 20px | label-large |
.btn-size-m / .btn-lg | 56px | 24px | 24px | title-medium |
.btn-size-l | 96px | 48px | 32px | headline-small |
.btn-size-xl | 136px | 64px | 40px | headline-large |
Square shape
Add .btn-square for the Expressive square button (12–28px corners depending on size).
Icons
Put a Material Symbols span first for a leading icon; the button tightens its padding on that side automatically. For a trailing icon, put it last and add .btn-icon-end.
Disabled
Use the disabled attribute (or .disabled with aria-disabled="true" on links). Disabled buttons use M3's on-surface 12% container and 38% content.
Press shape-morph and ripples
Pressed buttons tighten their corners and spring back on release (fast-spatial). With m3-components.js loaded they also get Compose's ripple. To opt an element out add data-m3-no-ripple; to give any element a ripple add data-m3-ripple.
Toggle buttons
Toggle buttons turn an option on or off, such as Bold or Favorite. The selected state changes color and shape.
Use it for
- An option that applies right away, like Bold, Mute or Bookmark
- Pair each with a
.btn-checkinput so the state is a real checkbox or radio
Use something else
- A setting in a list or form: a switch
- One choice from a set of options: a connected group
From Bootstrap: Bootstrap's .btn-check markup drives it. Selected buttons switch color and morph from round to square, as in M3 Expressive.
Use Bootstrap's toggle pattern: a hidden .btn-check input with a label.btn, or aria-pressed="true" / .active on a button you manage yourself.
| Base | Unselected | Selected |
|---|---|---|
.btn-primary | surface-container / on-surface-variant | primary / on-primary |
.btn-tonal | secondary-container | secondary / on-secondary |
.btn-elevated | surface-container-low / primary | primary / on-primary |
.btn-outlined | transparent / on-surface-variant | inverse-surface |
Icon buttons
Icon buttons take a supplementary action with a single icon, in less space than a labeled button.
Use it for
- Compact actions in toolbars, app bars, cards and list rows
- Icons whose meaning is obvious; always add an
aria-label
From Bootstrap: Bootstrap has no icon button. .btn-icon turns any .btn variant into one.
Sizes and widths
The five size classes apply. Each size also has narrow and wide forms: .btn-icon-narrow and .btn-icon-wide.
| Size | Icon | Narrow / default / wide width |
|---|---|---|
.btn-size-xs | 20px | 28 / 32 / 40px |
| default | 24px | 32 / 40 / 52px |
.btn-size-m | 24px | 48 / 56 / 72px |
.btn-size-l | 32px | 64 / 96 / 128px |
.btn-size-xl | 40px | 104 / 136 / 184px |
Toggle icon buttons
Add .btn-toggle. Selected icons switch to the filled style.
FABs
The floating action button holds the most important action on a screen.
Use it for
- One primary, constructive action per screen, such as Compose or Create
- An extended FAB when a label makes the action clearer
- A FAB menu for two to six related actions
Sizes: .btn-fab-sm (40px), default (56px), .btn-fab-md (80px) and .btn-fab-lg (96px). Colors: default primary-container, .btn-fab-secondary, .btn-fab-tertiary, solid .btn-fab-primary and .btn-fab-surface.
Extended
Add .btn-fab-extended and a label. It works with every size and color. .btn-fab-collapsed shrinks it back to an icon (the label stays as the accessible name); press Collapse to see the width spring.
With m3-components.js, data-m3-fab-collapse collapses it while the page (or its scrolling container) scrolls down and expands it on the way back up. See it in the app bar demo.
Menu
A FAB that opens related actions. The FAB morphs into a close button and the items slide out one after another, their labels riding the growing edge. m3-components.js opens it from the toggle and closes it on Escape, an outside click or a choice.
Align the items to the FAB's position with .fab-menu-start or .fab-menu-center, or open it downward with .fab-menu-down for a FAB near the top:
| Class | What it does |
|---|---|
.btn-fab | 56px FAB, 16px corners, primary-container. |
.btn-fab-sm / -md / -lg | 40px (12px corners) / 80px (20px, 28px icon) / 96px (28px, 32px icon). |
.btn-fab-secondary / -tertiary / -primary / -surface | Secondary or tertiary container; solid primary; surface-container-high with a primary icon. |
.btn-fab-extended | Adds a label. |
.btn-fab-collapsed / data-m3-fab-collapse | Collapse an extended FAB to its icon / do it on scroll (script). |
.fab-menu + .fab-menu-items + .fab-menu-toggle | FAB menu; the script toggles .open. |
.fab-menu-start / -center / -down | Items left-aligned / centered / opening downward (not in Compose). |
Button groups
Button groups gather related buttons. In M3 Expressive they react to each other: a pressed button grows and its neighbors make room.
Use it for
- Related actions side by side: the standard group
- Picking one or more options from a set: the connected group, which replaces M3's segmented buttons
From Bootstrap: A plain .btn-group of .btn-check toggles renders as an outlined segmented button.
Standard group
.btn-group-standard spaces buttons 12px apart. A pressed button grows about 15% and its neighbors give way, on the fast spring.
Connected group
.btn-group-connected replaces M3's segmented buttons: 2px gaps, round outer corners, inner corners that tighten when pressed and go fully round when selected. Usually holds .btn-toggle buttons.
Add .w-100 to stretch the buttons evenly across the row.
Segmented buttons (Bootstrap button group)
A Bootstrap .btn-group of .btn-check toggles renders as an outlined segmented button, with a check icon on the selected segment.
Split button
A split button pairs a main action with a menu of related ones.
Use it for
- A clear default with occasional alternatives, like Save and Save as
Use something else
- Equal choices with no default: a menu or a button group
From Bootstrap: Like Bootstrap's split dropdown, with M3 shapes: the inner corners round on hover and press, and the chevron flips while the menu is open.
It uses Bootstrap's dropdown plugin, so the menu needs Bootstrap's JS bundle. Any button variant and size works on both halves (keep them the same).
Communication
Snackbars
Snackbars give a brief update about something the app did or will do, at the bottom of the window. People can keep working without touching them.
Use it for
- Low-priority confirmations: "Message archived", "Photo saved"
- At most one action, such as Undo or Retry
- Short text, one line where possible; no icons or links
Use something else
- Something that needs a decision: a dialog
- A status that should stay put: an alert
- Several ongoing uploads or downloads: the transfer panel
From Bootstrap: Bootstrap toasts are placed and stacked by hand. Snackbars queue one at a time from a single call, pause while hovered or focused, and report whether the action was used. (Bootstrap's .toast also picks up the snackbar look.)
On the web, a snackbar that disappears on its own can be missed by people with low vision or who need more time. M3 asks for one of two fixes: also show the message near what triggered it (the Save button turns to "Saved"), or give the snackbar an action so it stays until used.
Each one scales in from 80% and fades in; the next waits until the current one is dismissed (the queue button shows this). On screens 600px and wider they sit at the bottom-left corner (bottom-right in right-to-left languages), sized to their message, as in Google's web apps; on phones they're centered. Short snackbars leave after 4 seconds; ones with an action stay until you act or close them, as in Compose. Hovering or focusing a snackbar pauses its timer.
From JavaScript
| Attribute (on any clickable element) | Option |
|---|---|
data-m3-snackbar="…" | The message; clicking shows the snackbar. |
data-m3-snackbar-action="…" | Action button label. |
data-m3-snackbar-duration | short, long, indefinite or milliseconds. |
data-m3-snackbar-dismissible | Add a close button. |
data-m3-snackbar-stacked | Action on its own line. |
data-m3-snackbar-align="center" | Keep it centered on wide screens too. |
Tooltips
Tooltips show a brief label or explanation when people hover, focus or long-press an element.
Use it for
- Plain tooltips to name an icon-only button
- Rich tooltips for a short explanation, with an optional action
Use something else
- Information people need to get their task done: show it on the page
- News about something that just happened: a snackbar
From Bootstrap: Tooltips become M3 plain tooltips and popovers become rich tooltips. No caret by default, and rich tooltips close on an outside click.
Placement
Use Bootstrap's data-bs-placement. Like Compose, tips have no caret, sit 4px from their anchor and grow out from the side that faces it.
Optional caret
M3 leaves the caret off by default (Compose's caretShape is null). To point at the anchor, add data-bs-custom-class="tooltip-caret". It works on tooltips and popovers, and the caret is 16 × 8, Compose's TooltipDefaults.caretSize.
Rich tooltip with an action
Put an action in .popover-actions with data-bs-html="true". Bootstrap's sanitizer keeps links, so use an <a class="btn btn-text">.
Dismissal and motion
With m3-components.js loaded, a rich tooltip opened by click stays open until you click outside it or press Escape, as Compose's persistent tooltips do. Clicks inside it, on its action for example, keep it open. Tips scale from 0.8 to 1 on the fast-spatial spring while fading on fast-effects, and sit 4px from their anchor. The script sets this up through bootstrap.Tooltip.Default, so load it before you create your tooltips.
As in Bootstrap, tooltips and popovers are opt-in; initialize them in your script:
| Part | M3 token |
|---|---|
.tooltip | inverse-surface container, inverse-on-surface body-small text, extra-small corners, 200px max width. |
.popover | surface-container, medium corners, level-2 shadow (caret included), 320px max width. |
.popover-header | Subhead: title-small, on-surface-variant. |
.popover-body | Supporting text: body-medium, on-surface-variant. |
.popover-actions | Action row; put .btn-text buttons in it (primary label). |
.tooltip-caret | Via data-bs-custom-class: shows the optional 16 × 8 caret. |
Badges
Badges show counts or status on icons and navigation items.
Use it for
- Unread counts on navigation items and icon buttons
- A small dot for something new
Use something else
- A label inside text, like "New": a word badge, below
- A message about what happened: a snackbar
From Bootstrap: .badge stays as a word badge; .badge-count and .badge-dot are M3's.
On icons
M3 badges always sit on something. Wrap the icon and its badge in .badge-anchor: a count badge (.badge.badge-count, 16px, label-small) or a 6px .badge-dot. They're placed as Compose's BadgedBox places them and use the error color by default. Nav bar and rail items do this on their own.
The badge is visual only, so put the count in the button's aria-label ("Mail, 3 unread"), as above.
Word badges
Bootstrap's .badge is a label for text: label-medium in a 20px pill, colored with .text-bg-*. M3 has no word badge; these are kept from Bootstrap for inline use.
Release notes New
| Class | What it does |
|---|---|
.badge-anchor | Wraps an icon and places its badge on the top end corner (Compose BadgedBox). |
.badge.badge-count | M3 large badge: 16px, label-small, error color. |
.badge-dot | M3 small badge: a 6px error dot. |
.badge | Bootstrap word badge: label-medium in a 20px pill; color it with .text-bg-*. |
Progress indicators
Progress indicators show that a process is running and, when known, how far along it is.
Use it for
- Determinate when you know the percentage; indeterminate when you don't
- Linear for pages and sections; circular inline or in small spaces
Use something else
- A short wait of up to a few seconds: the loading indicator
- Several transfers at once: the transfer panel
From Bootstrap: .progress becomes M3's flat linear indicator, with a gap and a stop dot. The wavy and circular indicators are new.
Flat linear
A 4px gap separates the indicator from the track, which ends in a stop dot.
Wavy
Set the value with --md-progress-value (a percentage) and keep aria-valuenow in step. The wave flattens near 0% and 100%, as in Compose.
Indeterminate
| Class | What it does |
|---|---|
.progress-wavy | Wavy linear indicator. |
.progress-circular (+ -sm, -lg) | Wavy circular indicator: 32 / 48 / 72px. |
.progress-indeterminate | Looping version of either. |
Value changes animate on a spring (see Motion). With m3-components.js the wave is drawn as SVG each frame, giving round caps and smooth amplitude changes.
Loading indicator
The loading indicator shows that something is loading, for short waits.
Use it for
- Waits from about 200ms to a few seconds, such as refreshing a feed
- In place of a circular indeterminate progress indicator
Use something else
- Longer or measurable work: a progress indicator
- Under about 200ms: show nothing
From Bootstrap: It replaces Bootstrap's spinners, which still work (see the end of this section).
Determinate
Morphs from a circle toward a soft burst, turning half a revolution, as --md-loading-progress goes from 0 to 1. It only moves when the progress changes: set the property (and aria-valuenow) from your loading code. With m3-components.js each change glides on Compose's progress spring, so uneven updates still morph smoothly; without it the shape steps. Drag the slider or simulate a load:
Bootstrap spinners
Bootstrap's .spinner-border and .spinner-grow still work and take the primary color. They're plain Bootstrap, not M3; prefer the loading indicator above for short waits and a circular progress indicator when you know how far along a task is.
| Class | What it does |
|---|---|
.loading-indicator | 48px indeterminate indicator. |
.loading-indicator-contained | On a primary-container circle. |
.loading-indicator-sm / -lg | 32px / 72px. |
.loading-indicator-determinate | Follows --md-loading-progress (0–1). |
Color it with --md-li-color (and --md-li-container for the contained one). It stops when the visitor prefers reduced motion.
Alerts
Alerts show a message in the page that stays until it's dismissed or resolved.
Use it for
- A status that should stay visible, like a failed sync or an expiring trial
From Bootstrap: Same markup on M3 container colors. M3's closest equivalents are banners and cards.
Primary alert
On-primary-container text on primary-container.Transfer panel
A Drive-style panel that tracks several uploads or downloads at once, in the corner of the window.
Use it for
- Several longer transfers that people may want to watch or cancel
Use something else
- One quick confirmation: a snackbar
- Progress on a single element: a progress indicator
From Bootstrap: Built on a Bootstrap .toast, which M3 doesn't stack, so this is our own addition.
It's not an M3 component. Unlike a snackbar it stays up and holds several rows, so it follows the theme like a dialog or menu: surface-container-high, level-3 shadow and large corners, as Google Drive's upload panel does. The header counts what's in flight ("Uploading 3 items", "2 items complete · 1 failed") and has collapse and close buttons. Drive it from your upload code:
Containment
Cards
Cards hold content and actions about a single subject.
Use it for
- Grouping related information and actions
- Elevated, filled or outlined, depending on how much a card should stand out
From Bootstrap: .card is the outlined card; .card-elevated and .card-filled add the other two.
Filled
surface-container-highest
For grouping without lifting content off the page.
Outlined
Bootstrap's default .card
An outline-variant border on the surface color.
Clickable cards (an a.card, or a .card with .card-action) lift on hover as in Compose.
Carousel
Carousels show a collection of items that can be scrolled on and off the screen.
Use it for
- Browsing a collection of visual items: photos, album art, products, places
- A featured item or two with a peek at what's next (hero)
From Bootstrap: Bootstrap's .carousel is a slideshow, one slide at a time. M3's carousel is a browsing strip whose items grow and shrink as they scroll, so it uses its own .m3-carousel class and both can sit on the same page.
Each item is a window onto its image with extra-large corners. As an item heads for the edge, its window narrows while the image stays centered, which gives the carousel its parallax. The layout is a port of Compose's: a set of keylines with large, medium and small slots sized to fit the width exactly. Items resize between them as they scroll, and the slots shift at either end so the first and last items can be large. The browser does the scrolling itself, so touch, trackpads and momentum behave natively. A mouse can drag and throw it: the release carries its speed into Compose's motion, a spring that settles on an item without bouncing, or, for uncontained, a glide to a stop.
Multi-browse
Many items at once for quick browsing: large, medium and small slots. data-item-width is the width large items aim for (186 by default); the carousel adjusts it so the slots fit the width exactly. It moves one item per swipe, however hard the throw (Compose's single-advance fling). Add data-fling="free" to let a hard throw carry past several items before it settles.
Carousel items aren't cards: they're mostly picture, with optional brief text. Put a .media-text in an item and it behaves as in Material's own carousel: the text and its scrim follow the left edge of the item's visible window, and only fade in once the window is wide enough for all of the text (and out again before any of it would be cut off). There are no ellipses. M3 also asks for a way to see every item without scrolling sideways: a header arrow, as here, or a Show all button under the carousel that opens a page of all the items.
Places
arrow_forwardLake Braies
Moraine Lake
Isle of Skye
Tulum
Black Forest
Yosemite
Tomorrowland
Berghain
Red Rocks
The Fillmore
Uncontained
Items keep their width (data-item-width), and the last one on screen is cut off at the edge. It scrolls freely, without snapping.
Hero
One large item centered between two small ones, for featuring content. data-item-width is optional: leave it off and the large item takes all the space the small ones don't. Text on an item stays inside its window and fades out as it narrows.
Lake Braies
Moraine Lake
Isle of Skye
Tulum
Black Forest
Yosemite
Banner with autoplay
M3 doesn't cover autoplay; its carousel is for browsing at your own pace. But a homepage banner that moves on by itself, with a call to action on each slide, is what many sites expect, and the hero layout suits it. Add data-autoplay (milliseconds, 6000 by default; it moves one item and wraps to the start). Moving content needs a way to stop it (WCAG 2.2.2), so there's always a pause button: yours, marked data-m3-carousel-play="#id", or one the script adds under the carousel. It also holds while the carousel is hovered or focused or the tab is in the background, starts its count again after any interaction, and stays off for people who ask for reduced motion.
M3 rules out arrows on or beside the carousel, but allows controls above or below it. data-m3-carousel-prev="#id" and data-m3-carousel-next="#id" turn any button into one, and they switch off at the ends.
Sizing and spacing
Set the height with --m3-carousel-height (221px by default, as in M3's examples). data-spacing sets the gap between items (8px), and data-padding adds space before the first item and after the last. That padding is handled the Compose way: the end items shift in from it as you scroll, instead of the whole strip being inset. Small items stay between data-min-small and data-max-small (40 and 56px). M3 asks that items never get so small that you can't tell what's in them.
Keyboard, as M3 sets out: Tab lands on the first item (not the carousel itself), Left and Right move between items (Home and End jump to the ends), Up and Down leave the carousel, and Space or Enter opens the item's link. An item holding a link or button shows a hover layer and a ripple, its corners tighten a little while it's pressed, and a click anywhere on it follows its link. With reduced motion turned on, there's no parallax and no resizing: every item is the large size and the strip runs to the edge. Without the script the items show as a plain row that scrolls and snaps.
| Attribute or class | What it does |
|---|---|
.m3-carousel + data-m3-carousel | The carousel: "multi-browse" (default), "uncontained" or "hero". Give it an aria-label. |
.m3-carousel-item | An item, usually an <img>; add a .media-text for brief text that fades as the item narrows. |
data-item-width | Large item width to aim for (multi-browse, hero) or the item width (uncontained). |
data-spacing / data-padding | Gap between items (8) / space before the first and after the last (0). |
data-fling="free" | Multi-browse or hero: a throw can pass several items (Compose's multi-browse fling) instead of one. |
data-autoplay="6000" | Move on by itself every 6 seconds, wrapping to the start (not an M3 feature; comes with a pause button). |
data-m3-carousel-prev / -next / -play="#id" | Buttons above or below the carousel: previous, next, and pause/play for autoplay. |
.m3-carousel-controls | A row for those buttons, under the carousel. |
data-min-small / data-max-small | Small item width range (40–56). |
--m3-carousel-height / --m3-carousel-shape | Height (221px) / item corners (extra-large). |
Imagery
In M3, images live inside shaped containers, and any text on them sits on a scrim so it stays readable.
M3 has no imagery component. Its guidance is spread across cards, carousels, color and accessibility, and these classes gather it: .media crops an image into its container, .media-text puts protected text on it, .media-hero and .media-parallax make header images, and data-m3-image-color takes colors from the image itself.
Images in cards
The card's shape clips the image: no border, radius or shadow on the image itself. Wrap it in .media with one of Bootstrap's .ratio classes for the crop, and move the focal point with object-position when the subject isn't centered. An image can top a card, or fill it with the text on the image.
Image on top
A 16:9 crop at the top of an outlined card. The text stays on the card's surface.
Focal point
object-position: 50% 70% keeps the valley in frame instead of the sky.
Image fills the card
Text on the image gets a scrim.
Text on images
Never put text straight on a photo: what's readable on one image disappears on the next. .media-text lays a scrim behind the text. It's black at 60% under the text itself, which keeps white text above 4.5:1 even on a pure white image, then fades out over 96px on an eased curve, so there's no hard edge. That follows Material's text-protection guidance: a long gradient with most of its change near the dark end. .media-text-top puts the text at the top, .media-text-fill dims the whole image for centered text, and .media-text-tinted uses a dark shade of the image's own color instead of black (see color from images).
Bottom
On the brightest part of the photo
Top
Over white smoke
Fill
For centered text
Header images and parallax
.media-hero is a tall header image (240–440px, following the window height) with Expressive extra-large corners; add .rounded-0 for full bleed. Add .media-parallax and the image drifts more slowly than the page as it scrolls by.
Does M3 still use parallax? Yes, but in a narrower way than the old full-page background effect. In M3's carousel, each item is a window onto its image; as an item shrinks toward the edge, its image moves the other way, so it looks pressed against the edge rather than cut off. Android's collapsing image headers do the same. A page background fixed behind everything isn't an M3 pattern. .media-parallax is the header version: CSS scroll-driven animation, no JavaScript. Browsers without it, and people who ask for reduced motion, get a still image.
Destinations
The Canadian Rockies
Ten lakes, three passes, one very long drive.
Color from images
M3 calls this content-based color: colors drawn from the content on screen rather than the app theme. Spotify's album pages are the familiar case, with the cover's color washing down into the page. Add data-m3-image-color to a container and the script picks the image's most telling vivid color, using the idea behind Material's color scoring: how much of the image is near that hue, and how colorful it is. It sets --md-image-color, and the color fades in when the image loads. Then:
.image-gradient: the color at 35% over the surface, fading down into the plain surface (the album-page look)..image-tint: a quiet 16% tint of surface-container, for cards..media-text-tinted: the scrim in a dark shade of the color.
The text stays in the theme's own on-surface color. Mixing at most 35% of any color into the surface keeps that text above 4.5:1, light or dark. The image must be on your own site or sent with CORS (crossorigin="anonymous"); otherwise the browser won't let the script read it, and the colors stay neutral. You can also set --md-image-color yourself, from the server, and skip the script.
Album
Surface Tension
The Material Ensemble · 2026 · 9 songs, 38 min
Each card below takes its own tint from its cover:
Festival Mix
Big rooms, bigger drops
Late Night
Low light, deep cuts
Golden Hour
Warm and loud
Riff Raff
Guitars, mostly
Carousels
M3's carousel is the main place it moves images around: items in a row that shrink as they reach the edges, with the parallax described above. It comes in multi-browse, uncontained, hero and full-screen layouts. M3 warns against shrinking items so far that you can't tell what's in the picture. The carousel just above has the multi-browse, uncontained and hero layouts.
Guidelines
Do
- Give meaningful images real
alttext; usealt=""when the image is decoration - Set
widthandheight(or a.ratio) andloading="lazy", so the page doesn't jump as images arrive - Keep the subject in frame on every screen size with
object-position
Don't
- Put text on an image without a scrim, or rely on the photo being dark enough
- Add borders, shadows or separate corner rounding to images inside cards
- Use a fixed full-page background image as a parallax backdrop
| Attribute or class | What it does |
|---|---|
.media + .media-img | An image cropped to fill its container (object-fit: cover). Pair with .ratio-* or a height. |
.media-text | Text on the image with a scrim: 60% black under the text, then an eased 96px fade. |
.media-text-top / -fill / -tinted | Text at the top / the whole image dimmed / the scrim in the image's own dark color. |
.media-hero | A 240–440px header image with extra-large corners. |
.media-parallax | The image drifts more slowly than the page (CSS scroll-driven; still if unsupported or reduced motion). |
data-m3-image-color | Sets --md-image-color from an image inside (or the one the value selects). Needs m3-components.js. |
.image-gradient / .image-tint | The image color washed into the surface / a light tint of surface-container. Text keeps 4.5:1. |
.card-img-overlay | Bootstrap's overlay, bottom-aligned with a scrim. |
Media viewer
The media viewer shows images, video and other content full screen, one at a time, with the rest of the gallery a swipe away.
Use it for
- Photo galleries, product photos, before and afters
- Videos, YouTube or Vimeo, and documents opened from a page without leaving it
From Bootstrap: Bootstrap has no lightbox, and M3 has no media viewer component. This one works like Fancybox (data attributes on the links you already have, grouped into galleries) but is built from M3 parts: a full-screen black stage, icon buttons, M3's container transform to open and close, shared axis between items and a carousel of the gallery. No other library is needed.
Gallery
Put data-m3-viewer on a link to the full-size image, around its thumbnail. Links with the same value make one gallery, in page order. A click opens the picture straight out of its thumbnail (the rounded crop grows into the whole image) with the rest of the gallery in a strip below it; closing shrinks it back into the thumbnail it's showing. data-caption goes under the picture (or use a <figure> and its <figcaption>). Double-click, double-tap, + or the zoom button zooms to the image's own size; drag to look around.
Slideshow and links
The round play button in the top bar runs the gallery as a slideshow: a thin bar over the strip fills over each item's time, then the next comes in, looping at the end. A video plays to its end first; YouTube, Vimeo and pages wait for you to move on. It holds while you zoom or drag, and the button pauses it (WCAG asks for a way to stop anything that moves on its own). data-slideshow="4000" sets the time per item (5 seconds otherwise) and data-autoplay starts it on opening; put them on any item of the gallery, as on the first one above.
While a named gallery is open, the address shows which item (#places-3): copy it to share that picture, and the page opens the gallery there. Back closes it. data-hash="false" leaves the address alone. Try #places-4.
Video, embeds and HTML
The viewer works out what each link is from its address: a video file (.mp4, .webm), a YouTube or Vimeo page (played with YouTube's no-cookie player), a .pdf, or #id for something on the page, shown on an M3 surface card; anything else is an image. data-type says so outright (image, video, youtube, vimeo, iframe, pdf, html), and data-width / data-height size a video or frame (16:9 otherwise). Videos start playing and stop when you move on.
Opening hours
Any element on the page can be shown. It moves into the viewer while it's open and back afterwards, so keep it in a hidden wrapper, not hidden itself.
A button insideFrom code
M3Components.viewer(items, index) opens it with any list; each item takes the same things as the attributes. It takes { thumbs, slideshow, autoplay, hash } as a third argument and returns an object with el, go(i), next(), prev(), play(), pause() and close(), and the viewer sends m3:viewer-open, m3:viewer-change and m3:viewer-close on the document (detail: index, item, items).
Keyboard: ← → move, Home / End go to the first or last, + / - zoom, Esc closes; Tab reaches the buttons and the strip, whose items take the arrow keys and Enter. It's a native <dialog>, so focus stays inside, the page behind is inert, and focus goes back to the link afterwards. A link with a modifier key held (Ctrl, Cmd, Shift) opens normally. With reduced motion nothing moves.
| Attribute | What it does |
|---|---|
data-m3-viewer | On a link or button: opens the viewer. The same value makes a gallery; empty shows it alone. (Anything else needs tabindex="0"; it gets Enter and Space.) |
href / data-src | What's shown (the full-size image, the video, the page, #id). |
data-type | image, video, youtube, vimeo, iframe, pdf or html; read from the address otherwise. |
data-caption | Text under the item (or the figure's <figcaption>). |
data-title | Text in the top bar, next to the count. |
data-width / data-height | A video's or frame's size (its shape is what counts; 16:9 otherwise). |
data-thumb | The picture for it in the strip (the <img> inside otherwise). |
data-download | Adds a download button; the value names the file. |
data-thumbs="false" | No strip. |
data-slideshow / data-autoplay | The slideshow's time per item in ms (5000 otherwise) / start it on opening. On any item of the gallery. |
data-hash="false" | Don't show #name-3 in the address (and don't open from it). |
Dialogs
Dialogs ask for a decision or show critical information, blocking the page until the person responds. They are interruptive on purpose, so use them sparingly.
Use it for
- Confirming a high-risk action, like discarding work or deleting
- A single, short task that needs an answer before going on
- A clear question for a headline, never "Are you sure?"; put the confirming action last
From Bootstrap: Same .modal markup and JavaScript. Dialogs are always centered and fit their content between 280 and 560px instead of a fixed 500px; the headline, text and actions follow M3; icon, scrolling and full-screen variants are added.
Basic
With an icon
Put a .modal-icon before the title. The icon (secondary, 24px) and the headline center above the text.
Scrolling content
Add Bootstrap's .modal-dialog-scrollable. The headline and actions stay put, and dividers mark where the content scrolls under them.
Form
A dialog holding fields should use its full 560px. Add .modal-dialog-wide (Bootstrap's .modal-sm, -lg and -xl also set a fixed width). Fields inside take the dialog's container color.
Full-screen
M3's full-screen dialog is Bootstrap's .modal-fullscreen: surface color, no corners, and a top bar with a close icon button, a title-large headline and a text-button action. Any .modal-header that starts with an icon button gets the bar layout. M3 recommends full-screen dialogs only on compact screens, so .modal-fullscreen-sm-down is usually the right choice. The example uses .modal-fullscreen so you can see it here.
Requires an answer
With Bootstrap's data-bs-backdrop="static" and data-bs-keyboard="false", clicking the scrim or pressing Escape doesn't close the dialog, so the person has to pick an action.
| Class | What it does |
|---|---|
.modal / .modal-dialog / .modal-content | Basic dialog: centered, fits its content between 280 and 560px, surface-container-high, 28px corners. |
.modal-title | Headline: headline-small, on-surface. |
.modal-body | Supporting text: body-medium, on-surface-variant. |
.modal-footer | Actions, right-aligned, 8px apart; use .btn-text. |
.modal-icon | Hero icon above a centered headline. |
.modal-dialog-scrollable | Body scrolls between dividers. |
.modal-dialog-wide | Takes the full 560px (forms). |
.modal-fullscreen / -sm-down … | Full-screen dialog: surface, no corners, top bar header. |
Sheets
Sheets show supporting content anchored to an edge of the window: the bottom on phones, the side on larger screens.
Use it for
- Secondary content or actions on a phone: a bottom sheet
- Filters or details beside the main content on larger screens: a side sheet
Use something else
- A decision that must be answered: a dialog
- Navigation between destinations: a navigation drawer
From Bootstrap: .offcanvas-bottom, -end and -start become M3 bottom and side sheets, sliding on a spring.
Bottom sheet
Bootstrap's .offcanvas-bottom becomes the M3 bottom sheet: a drag handle, 28px top corners, up to 640px wide, sliding on a spring. Its height follows its content.
Side sheet
Secondary content anchored to the side of the window. A modal side sheet is Bootstrap's .offcanvas-end (or .offcanvas-start), sliding over the page with a scrim. A standard side sheet sits in the layout beside the content.
Modal
Up to 400px wide on surface-container-low, with large corners on the inner edge and a level-1 shadow. The header holds an optional back button, the title-large headline and a close button. The optional .offcanvas-footer keeps its actions at the bottom; add .border-top for the divider.
Filters
Narrow the results by type and date.
Detached
Add .offcanvas-detached to float the sheet 16px from the window edges, with all four corners rounded.
Filters
Narrow the results by type and date.
Standard
A standard side sheet is part of the layout, not an overlay: .side-sheet on surface, flat, with an optional .border-start divider. Put it in a flex row next to your content. To open and close it, wrap it in Bootstrap's .collapse.collapse-horizontal. The sheet keeps its full width and slides out past the window edge as the content takes its space, so nothing inside it reflows. Its width eases without overshoot, so the content beside it never wobbles. For a sheet on the start side, add .side-sheet-start and use .border-end.
Page content. It takes whatever width the sheet leaves.
| Class | What it does |
|---|---|
.offcanvas-end / .offcanvas-start | Modal side sheet: surface-container-low, level 1, large inner corners, max 400px. |
.offcanvas-detached | Floats the modal sheet 16px from the edges, all corners rounded. |
.offcanvas-header | 72px header: optional leading .btn-icon (back), .offcanvas-title (title-large, on-surface-variant), .btn-close. |
.offcanvas-body | Content, 24px side padding, on-surface-variant. |
.offcanvas-footer | 72px action row at the bottom, left-aligned; add .border-top for the divider. |
.side-sheet | Standard side sheet: in-page, surface, flat. Add .border-start / .border-end for the divider. |
.side-sheet-start | A standard sheet on the start side, so it slides out toward that edge. |
Lists
Lists are continuous, vertical groups of text or images that are easy to scan.
Use it for
- Items of the same kind, in one, two or three lines
From Bootstrap: .list-group with M3 item heights (56, 72 or 88px) and state layers. The segmented list is new.
Segmented list
Add .list-group-segmented: items 2px apart whose corners round off on hover and when pressed or selected.
| Class | What it does |
|---|---|
.list-group-item-two-line / -three-line | 72px / 88px items. |
.list-group-segmented | Expressive segmented list. Item color via --md-list-item-bg. |
Accordion
Accordions stack collapsible sections, so long content can be scanned by heading.
Use it for
- FAQs and long pages split into sections people open one at a time
Use something else
- Switching between views of equal weight: tabs
- A few short sections: just show them
From Bootstrap: Same markup. M3 colors and shape; the panel opens on a no-bounce spring, so nothing below it moves past its final position, and the chevron turns on the bouncy one.
Tables
Tables show data in rows and columns, so values are easy to compare.
Use it for
- Comparing several values across many items
- Sorting, selecting and paging through records
From Bootstrap: Every Bootstrap table class works. Headers use M3 title-small, rows sit on the surface, and hover and selection use M3 state layers.
Basic
| Client | Plan | Status | MRR |
|---|---|---|---|
| Northwind Studio | Studio | Active | $240 |
| Acme Co. | Starter | Trial | $0 |
| Globex | Agency | Active | $480 |
| Initech | Studio | Past due | $240 |
Striped and hoverable
Add .table-striped for alternating rows and .table-hover for an M3 hover state layer.
| Client | Plan | Status | MRR |
|---|---|---|---|
| Northwind Studio | Studio | Active | $240 |
| Acme Co. | Starter | Trial | $0 |
| Globex | Agency | Active | $480 |
| Initech | Studio | Past due | $240 |
Bordered, borderless and small
| Client | Plan | Status | MRR |
|---|---|---|---|
| Northwind Studio | Studio | Active | $240 |
| Acme Co. | Starter | Trial | $0 |
| Globex | Agency | Active | $480 |
| Initech | Studio | Past due | $240 |
| Client | Plan | Status | MRR |
|---|---|---|---|
| Northwind Studio | Studio | Active | $240 |
| Acme Co. | Starter | Trial | $0 |
| Globex | Agency | Active | $480 |
| Initech | Studio | Past due | $240 |
| Client | Plan | Status | MRR |
|---|---|---|---|
| Northwind Studio | Studio | Active | $240 |
| Acme Co. | Starter | Trial | $0 |
| Globex | Agency | Active | $480 |
| Initech | Studio | Past due | $240 |
Row colors
Bootstrap's contextual classes use the M3 container colors, so text keeps its contrast: .table-primary, -secondary, -success, -warning, -danger, -info, -light and -dark. They work on a whole table, a row or a cell.
| Client | Plan | Status | MRR |
|---|---|---|---|
| Northwind Studio | Studio | Active | $240 |
| Acme Co. | Starter | Trial | $0 |
| Globex | Agency | Active | $480 |
| Initech | Studio | Past due | $240 |
Data table
A fuller pattern in a card: a toolbar with a title and actions, selectable rows, a sortable column and a footer with rows per page and paging. Selected rows use .table-active (secondary-container). For sorting, put a .table-sort button in the header cell and set aria-sort on the th. The selecting and sorting in this demo come from the page's demo script; wire them to your own data.
| Ava Martinez | Design | ava@example.com | Oct 2, 2026 | 12 | |
| Ben Okafor | Engineering | ben@example.com | Sep 18, 2026 | 31 | |
| Chloe Nguyen | Marketing | chloe@example.com | Oct 9, 2026 | 7 | |
| Diego Alvarez | Engineering | diego@example.com | Aug 27, 2026 | 24 | |
| Emma Schultz | Support | emma@example.com | Oct 14, 2026 | 3 |
Wide tables
Wrap a table in .table-responsive and it scrolls sideways on narrow screens instead of widening the page. Add a <caption> to describe it (.caption-top puts it above).
| Site | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Total |
|---|---|---|---|---|---|---|---|---|---|
| zephyrworks.com | 1,204 | 1,322 | 1,410 | 1,388 | 1,502 | 1,611 | 1,590 | 1,702 | 11,729 |
| 4beam.com | 412 | 398 | 455 | 470 | 502 | 488 | 530 | 561 | 3,816 |
| vicariouspr.com | 220 | 241 | 236 | 259 | 270 | 266 | 281 | 302 | 2,075 |
| Class | What it does |
|---|---|
.table | Title-small headers (on-surface-variant), 12px × 16px cells, outline-variant dividers. |
.table-striped / .table-hover | Alternating rows / hover state layer. |
.table-bordered / .table-borderless / .table-sm | All borders / none / compact 6px × 12px cells. |
.table-primary … .table-dark | Row, cell or table colors from the M3 container roles. |
.table-active | Selected row: secondary-container. |
.table-sort + th[aria-sort] | Sortable header button; the arrow shows and turns with aria-sort. |
.table-toolbar / .table-toolbar-title / .table-footer | Toolbar above and footer below a table in a card. .is-selecting tints the toolbar while rows are selected. |
Navigation
App bars
Top app bars show a page's title and its main actions at the top of the screen.
Use it for
- Small bars for most pages; medium or large flexible bars for a prominent title that collapses on scroll
- Two or three actions; put the rest in a menu
Use something else
- Many actions: a toolbar
- A marketing site header: Bootstrap's
.navbar
From Bootstrap: Bootstrap's .navbar is a site header. App bars are app-style page headers with M3 sizes and scroll behavior.
Small
Medium and large flexible
The title moves into .app-bar-headline below the row. .app-bar-scrolled switches the container to surface-container (the "content is scrolled under me" color).
| Class / attribute | What it does |
|---|---|
.app-bar | Small bar: 64px row, title-large title. |
.app-bar-medium | 112px (136 with subtitle), headline-medium title. |
.app-bar-large | 120px (152 with subtitle), display-small title. |
.app-bar-row / .app-bar-actions / .app-bar-headline | Top row, trailing actions, title block. |
.app-bar-title / .app-bar-subtitle | Title and subtitle. |
.app-bar-scrolled | Scrolled container color. |
data-m3-app-bar | Script adds .app-bar-scrolled once the page scrolls. |
data-m3-app-bar="collapse" | Script collapses a medium or large bar as you scroll (below). |
Collapse on scroll
With data-m3-app-bar="collapse" on a medium or large bar, the headline slides up under the top row as you scroll and the small title fades in; stop halfway and it snaps open or shut. Add a small .app-bar-title to the row for the collapsed state. It works in the page or in any scrolling container. Scroll inside the frame:
The FAB in the frame uses data-m3-fab-collapse (see Extended FAB).
Toolbars
Toolbars hold actions for the current page. A docked toolbar spans the window for actions that stay the same across pages; a floating toolbar hovers over the content for actions about what's on screen.
Use it for
- Actions about the current page or selection
- Standard colors to keep attention on the content;
.toolbar-vibrantto emphasize the controls or signal a mode, such as editing - More actions than fit: open the rest from a menu
Use something else
- Moving between top-level pages: a navigation bar. Never show it together with a docked toolbar
- One main action for the screen: a FAB
- A title with a few actions at the top: an app bar
From Bootstrap: Bootstrap's .btn-toolbar only spaces button groups. These are M3 surfaces with their own color, shape and motion.
Docked
.toolbar-docked is flat and full width: 64px on surface-container with square corners, pinned to the bottom of the screen or a scrolling area. Pin it with Bootstrap's .fixed-bottom for the page, or .sticky-bottom inside a scroller as here. As in Compose, items spread edge to edge inside 16px end padding, never closer than 4px. Add .toolbar-docked-fixed to center them with 32px between instead.
Add data-m3-toolbar="exit-always" to have it slide away as the content scrolls down and come back as it scrolls up. This is Compose's exitAlwaysScrollBehavior: the bar follows the scroll 1:1, and if you stop halfway it snaps shown or hidden on fast-spatial. Tabbing into it always brings it back. Scroll inside the frame:
Add .toolbar-vibrant for the primary-container version:
Floating
Expanding
A floating toolbar that springs out of a FAB, or opens out both sides of a middle toggle. The icons slide out on the moving edge, start bunched up and spread to their spacing. The script opens them and keeps the hidden buttons out of the Tab order until then.
Beside a FAB
Press the FAB. The toolbar opens to the left (right to left). The FAB's icon turns into a close icon; give it both icons as shown. Hidden toolbar buttons can't be reached by Tab until it opens.
Left to right and vertical
Both sides of a toggle
.toolbar-expandable keeps a middle toggle visible and opens a group on each side, so the toolbar grows out from its center.
| Class | What it does |
|---|---|
.toolbar-docked | Full-width 64px bar on surface-container; items spread edge to edge. |
.toolbar-docked-fixed | Centers the items with 32px between them. |
data-m3-toolbar="exit-always" | On .toolbar-docked: hides while scrolling down, returns while scrolling up (needs m3-components.js). |
.toolbar-floating | 64px pill, 8px padding, level-3 shadow. |
.toolbar-vibrant | Primary-container colors. |
.toolbar-vertical | A floating toolbar stacked vertically. |
.toolbar-fab | Wrapper for a .toolbar-floating plus its FAB toggle. Opens right to left. |
.toolbar-fab-start | FAB on the left; opens left to right. |
.toolbar-fab-vertical | FAB at the bottom; a vertical toolbar opens upward. |
.toolbar-fab-toggle | The FAB that toggles it (needs aria-controls). |
.toolbar-expandable | On a .toolbar-floating: two .toolbar-groups open around a middle toggle. |
.toolbar-expand-toggle | The middle toggle button. |
.toolbar-toggle-icon / -close | The toggle's two icons; they turn and cross-fade. |
.open | The open state (set by the script). |
Tabs
Tabs organize related content into views at the same level, so people can switch without leaving the page.
Use it for
- Primary tabs for the main views of a page; secondary tabs for views within one of them
- Short labels, and no more tabs than fit comfortably
Use something else
- Moving between sections of an app: a navigation bar
- Filtering one list: filter chips or a connected group
From Bootstrap: .nav-tabs becomes M3 primary tabs. With m3-components.js one indicator slides between tabs on a spring.
Primary
Secondary
Add .nav-tabs-secondary: a 2px indicator spanning the whole tab, for tabs within a section.
The indicator follows the .active link. Bootstrap's tab plugin (data-bs-toggle="tab") moves it for you; the demo tabs here move it on click.
Pills
Bootstrap's .nav-pills render as M3-colored pills (secondary-container when active).
Search
Search lets people type a query and see suggestions or results as they go.
Use it for
- A prominent way to find content in an app or a large site
- Recent searches or suggestions as soon as the field has focus
Use something else
- Narrowing a list that's already visible: filter chips
Filling the results from JavaScript
The component does the layout and the open and close motion in CSS; your code only decides what goes in the list. How it fits together:
- Opening is automatic. The view opens while the search has focus and its
.search-results-listhas something in it. An empty list keeps the view closed and the bar round, so you can clear it while you wait for results. - On every
inputevent, work out the matches and replace the list's children with.search-resultlinks: an icon, then the text. Show recent searches when the box is empty, and a.search-results-emptyrow when nothing matches. - Results are ordinary links, so Tab and Enter work, and clicking one keeps focus inside the search, so the view stays open until the person moves on.
- Announce the count in a visually hidden
role="status"element, so screen readers hear "5 results" as the list changes.
This one searches the sections of this page. Type "but", "sheet" or "progress":
For results from a server, fetch inside the handler. Wait about 200ms after the last keystroke and cancel the previous request, so a slow early reply can't overwrite a newer one:
| Class | What it does |
|---|---|
.search / .search-bar / .search-bar-input | The 56px search bar (surface-container-high, full corners). |
.search-results / .search-results-list | The docked search view: opens below the bar while the search has focus and the list isn't empty. |
.search-result | A 56px result row: icon, then text. A <mark> inside is bolded. |
.search-results-heading | Optional group heading in the list (title-small). |
.search-results-empty | The "no results" message. |
Breadcrumbs
Breadcrumbs show where a page sits in a deep hierarchy and let people step back up.
Use it for
- Sites or apps with three or more levels of nesting
Use something else
- Moving between top-level sections: a navigation bar or drawer
From Bootstrap: Same markup in body-medium: links in primary, the current page and separators in on-surface-variant. Long trails stay on one line and scroll sideways instead of wrapping.
Chevron divider and icons
Change the separator with Bootstrap's --bs-breadcrumb-divider, and put an icon in an item if it helps.
Long trails
A trail too long for its space stays on one line and scrolls sideways, starting at the first item so it still reads as a breadcrumb. With m3-components.js the cut-off side fades out and gets a round arrow. Each press reveals the next hidden page, one at a time, so a long title can never carry a middle page past unseen. No extra markup: it applies to every .breadcrumb, and short trails look the same as before. Add .breadcrumb-wrap to keep Bootstrap's wrapping instead.
| Class | What it does |
|---|---|
.breadcrumb / .breadcrumb-item | One line; scrolls sideways when too long. |
.breadcrumb-wrap | On the .breadcrumb: wrap onto more lines instead (Bootstrap's behavior). |
.breadcrumb-scroll | Added by the script around a trail: edge fades and arrow buttons. Tune the fade width with --md-crumbs-fade. |
Pagination
Pagination splits a long set of results into pages.
Use it for
- Search results and tables where people jump to a specific page
Use something else
- Feeds people read top to bottom: load more as they scroll
- A handful of items: show them all
From Bootstrap: Same markup, as M3 buttons: 40px round targets, a hover state layer and the current page on secondary-container.
Many pages
Skip a range with an ellipsis item: .page-item-ellipsis keeps it from looking clickable.
Sizes and alignment
.pagination-sm (32px) and .pagination-lg (48px); align with Bootstrap's flex utilities such as .justify-content-center.
| Class | What it does |
|---|---|
.pagination / .page-item / .page-link | 40px round page buttons, 4px apart. |
.page-item.active | Current page: secondary-container. |
.page-item.disabled | Disabled content color. |
.page-item-ellipsis | A non-interactive … gap. |
.pagination-sm / .pagination-lg | 32px / 48px buttons. |
Selection
Checkboxes & radios
Checkboxes let people pick any number of options; radio buttons pick exactly one.
Use it for
- Checkboxes for several selections, or to agree to something
- Radio buttons for one choice from a short, visible list
Use something else
- A single setting that takes effect immediately: a switch
- A long list of options: a select field
From Bootstrap: Same .form-check markup. M3 sizes, a checkmark that draws in, a springing radio dot and state layers on hover, focus and press.
The indeterminate state is set from JavaScript (input.indeterminate = true), as on any checkbox. The demo uses data-indeterminate to do that for you; it's not a framework attribute.
Switches
Switches turn a single setting on or off, and the change takes effect immediately.
Use it for
- Settings that apply at once, like Wi-Fi or dark mode
- A label that says what the switch turns on
Use something else
- Choices saved later with a submit button: a checkbox
- An option inside a toolbar: a toggle button
From Bootstrap: Same .form-switch markup, as the M3 52×32 switch: the handle slides on a spring, grows when on and while pressed, and can carry icons.
With icons
Add .form-switch-icons to the wrapper: the handle carries a check when on and a close icon when off.
With m3-springs.js loaded, call M3Springs.refresh(input) after changing .checked from code.
Sliders
Sliders let people pick a value, or a range, along a scale.
Use it for
- Settings where the exact number matters less than the feel, like volume or brightness
- Ticks for a few discrete steps; a value indicator when precision matters
Use something else
- Exact numbers: a text field
- Two or three options: a connected group
From Bootstrap: A plain .form-range gets the M3 Expressive track; the .slider wrapper adds sizes, ranges, ticks, icons and vertical sliders.
Basic
A Bootstrap range on its own: 16px track, a 4px bar handle that narrows while held, a gap around the handle and a stop dot. Chrome needs m3-components.js to draw the filled part; Firefox fills it either way.
Slider component
Wrap the range in .slider. The script draws the track from real geometry; the input stays underneath for keyboard, forms and screen readers. Without the script it falls back to the basic slider.
Range
Two inputs make a range slider. The handles can't cross.
Sizes and icons
An inset icon (sizes M and up) rides the start of the active track and hops past the handle when the active part gets too short. .slider-icon-start and .slider-icon-end pin icons to the track's ends.
Vertical
Add .slider-vertical; it runs bottom to top. Give it a height (240px by default).
Class / attribute on .slider | What it does |
|---|---|
.slider-s / -m / -l / -xl | Expressive sizes: track 24 / 40 / 56 / 96px (default 16px). |
.slider-vertical | Vertical, bottom to top. |
.slider-labeled | Value indicator above the handle while dragging or keyboard-focused. |
data-m3-suffix="%" | Text appended to the value indicator. |
.slider-ticks | A dot per step (needs step on the input). |
.slider-icon (child) | Inset icon, sizes M and up. |
.slider-icon-start / -end (children) | Icons fixed at the track's ends. |
If you set an input's value from code, call M3Components.refresh(input) afterwards; native inputs don't fire an event for that.
Chips
Chips help people enter information, make selections, filter content or trigger actions, in a compact form.
Use it for
- Assist chips for smart, contextual actions
- Filter chips to narrow down content
- Input chips for values people entered, such as recipients
Assist and filter
Assist chips are buttons. Filter chips use Bootstrap's .btn-check toggle pattern with a label.chip.
Shape-morphing
Add .chip-morph: corners go fully round when selected and tighten while pressed.
Input chips
.chip-input holds an entered value, with an optional .chip-avatar and a .chip-remove button. Removing is up to your code (the demo removes the chip on click).
Dual list
A dual list picks several items out of a long list by moving them from one side to the other, so what's chosen is always in plain view.
Use it for
- Choosing many from dozens or hundreds: team members, permissions, categories, columns to export
- When people need to see and review everything they've chosen
Use something else
- A few options: checkboxes or filter chips
- One choice: a menu or radio buttons
From Bootstrap: Bootstrap's multiple select is a list box you Ctrl-click in, where it's easy to lose a choice. M3 has no dual list, so this one is built from M3 parts: two lists on surface containers, a search field and tonal icon buttons. It stays a <select multiple> underneath, so the form posts it as always.
Choose from a list
Put data-m3-dual-list on a <select multiple>. Its unselected options are listed under Available, its selected ones under Selected. Click items to pick them (Shift-click for a range), then use the arrows, or double-click an item to move it straight across. Moved items fly to the other list on the spatial spring while both lists close up and make room. A search field filters each list once there are 8 or more options.
Groups
<optgroup>s become subheaders in both lists, and items keep to their group. Disabled options are shown but can't be moved.
In order
With data-reorder, the Selected list can be put in order: drag an item by its handle (the others spring out of its way), or pick items and use the move up and move down buttons, or press Alt+↑ / ↓. The options are reordered in the select, so the form posts them in that order. New arrivals go to the end, and an item taken out goes back to its place in Available. Groups only show in Available, since an order cuts across them.
Posts:
Keyboard: each list is a listbox. ↑ ↓ Home End move, Space picks, Shift+arrows pick a range, Ctrl+A picks all, and Enter moves the picked items (or the one you're on) to the other list. Moves are announced. required works: the Selected list is outlined in the error color and takes focus. Below 576px the lists stack and the arrows point down and up.
| Attribute or class | What it does |
|---|---|
data-m3-dual-list | On a <select multiple>: shows it as a dual list. The select stays the form control and gets change events. |
data-available-label / data-selected-label | The lists' titles (Available / Selected). |
data-search | "true" or "false": always or never show the search fields (shown from 8 options otherwise). |
data-reorder | The Selected list can be put in order (drag handle, Alt+↑/↓, move up / down); the form posts it in that order. |
--dual-list-height | The lists' height (360px). |
M3Components.dualList(select) | Returns refresh() and move(values, toSelected); call refresh() after changing the options from code. |
Date pickers
Date pickers let people pick a date, or a range of dates, from a calendar or by typing it.
Use it for
- Picking a date that's likely near today, such as an appointment or a deadline
- Dates where the day of the week matters
Use something else
- A date people know by heart, like a birthday: a plain text field (typing is faster than paging back decades)
- A time of day: a time picker
From Bootstrap: Bootstrap has no date picker, and a native type="date" field looks different in every browser. These are text fields with the M3 docked, modal and range calendars attached. The modal and range pickers use Bootstrap's modal.
Docked, or a dialog on phones
Add data-m3-datepicker to a text field. The script adds a calendar button and masks typing as MM/DD/YYYY. From 600px wide the calendar docks beneath the field (above it when there's no room); on a phone the button opens the dialog below instead, as M3 recommends. Add ="docked" to keep it docked everywhere. Choose a month or year from the menus in the header, or page with the arrows; OK fills the field, Cancel, Escape or a click outside leave it as it was.
Limits and a starting value
Set data-min and data-max as YYYY-MM-DD. Dates outside them are greyed out, the arrows stop at the edges, and a typed date outside the range fails validation. A value already in the field opens the calendar on that date.
Modal
On phones, or everywhere with data-m3-datepicker="modal", the calendar opens in a dialog: the chosen date as a large headline, a month row whose October 2026 button opens a grid of years, the days, and Cancel / OK. The pencil switches to typing the date instead (M3's modal date input); the field slides up as the calendar fades out.
Date range
Put two fields in a data-m3-daterange wrapper, marked data-m3-range="start" and "end". The calendar button opens M3's range picker: full screen on phones, a 568px dialog elsewhere, with every month in one scrolling list. Tap the start date, then the end; the days between get a secondary-container band. Save fills both fields. The pencil switches to typing both dates.
Reading the value
Keyboard: arrows move by day and week, Page Up and Page Down by month (with Shift, by year), Home and End to the start and end of the week, Enter picks, Escape closes. Alt+Down in the field opens the calendar.
| Attribute or class | What it does |
|---|---|
data-m3-datepicker | On a text field: attach the date picker, docked from 600px wide and a dialog below. |
data-m3-datepicker="docked" / "modal" | Docked on every screen size / the dialog on every screen size. |
data-m3-daterange + data-m3-range="start" / "end" | A start and an end field sharing the range picker. |
data-min / data-max | Earliest and latest dates, as YYYY-MM-DD. |
input.m3Date | The chosen Date, or null. |
.datepicker | The calendar: 360×460, surface-container-high, large corners, level 3. |
.datepicker-day | A 40px day; today has a primary outline, the selected day is filled primary. |
Time pickers
Time pickers help people select and set a specific time.
Use it for
- Setting a time of day, such as an appointment, an alarm or a reminder
Use something else
- A date: a date picker
- A length of time, like 45 minutes: a text field or a slider
From Bootstrap: Bootstrap has no time picker, and a native type="time" field looks different in every browser. This is a text field with M3's time picker dialog attached, using Bootstrap's modal.
Dial
Add data-m3-timepicker to a text field. The clock button opens the dialog: tap or drag around the dial to set the hour, and it moves on to the minutes by itself. Tap the hour or minute above the dial to go back to either. The keyboard button switches to typing the time instead. People can also type straight into the field: 930p, 9:30 pm and 21:30 all become 9:30 PM.
24-hour clock
Add data-24h. The dial gets an inner ring for 12 to 23, the AM/PM toggle goes away, and the field reads 21:30.
Scroll wheels
With data-m3-timepicker="scroll" the dialog shows two endless wheels instead of the dial (M3 Expressive's scroll time picker). Flick or drag them, or use the arrow keys; the number in the middle is the one chosen. Tap a number above or below to roll it into place. The keyboard button switches to typing, the same as on the dial. It works with data-24h too.
Reading the value
m3Time is always 24-hour HH:MM, the same as a native time field's value, so it can go straight to a server. Keyboard on the dial or a wheel: the arrow keys move by one, Page Up and Page Down by five. On the dial, Enter goes from the hour to the minutes, then confirms; on a wheel it confirms. Alt+Down in the field opens the dialog.
| Attribute or class | What it does |
|---|---|
data-m3-timepicker | On a text field: attach the time picker. |
data-m3-timepicker="scroll" | Scroll wheels instead of the dial. |
data-24h | Use a 24-hour clock. |
input.m3Time | The chosen time as "HH:MM", or null. |
.tp-field | The 100×120 hour and minute fields: surface-container-lowest, large corners, display-large. The active one is primary with a 2px outline. |
.tp-period-btn | AM and PM: pills that square off to 12px corners in primary-container when chosen. |
.tp-dial | The 256px dial, with a primary hand and 48px handle. |
.tp-wheel | A 100×120 scroll wheel of 66.7px rows. The middle number grows to display-large, emphasized, in on-surface; the rest are display-medium in outline. |
Color pickers
Color pickers let people choose a color by eye, by its code, or from a set palette.
Use it for
- Brand or theme colors, where people often paste a hex code
- Labeling things by color from a fixed set, with a palette-only picker
Use something else
- Two or three choices: filter chips or radio buttons with the color named
From Bootstrap: Bootstrap's .form-control-color is the browser's own color input, which looks different everywhere and can't take a pasted code. Material has no color picker, so this one is built from M3 parts: the date picker's docked surface, the Expressive slider for hue, outlined fields, and swatches that change shape when chosen.
Any color
Add data-m3-colorpicker to a text field. The swatch button opens the picker: drag in the square for saturation and brightness, slide the hue, or type the color as hex or in RGB, HSL, HSB or OKLCH (the button group switches the three number fields). Changes show in the field as you go. From 600px wide the picker docks under the field: Escape puts the color back, and a click outside keeps it. On phones it opens as a dialog with Cancel and OK instead. Where the browser has one, the eyedropper button picks a color from anywhere on screen.
Paste works in any form: #6750a4, 6750A4, #abc, rgb(103, 80, 164), 103, 80, 164, hsl(...), oklch(...) or a name like rebeccapurple. It turns into #RRGGBB straight away.
With a palette
Add data-palette with hex codes, separated by commas, to show them as swatches above the picker. Each is a circle that squares off with a check when it's the current color.
Palette only
With data-m3-colorpicker="palette" people can only choose from the palette: the picker is just the swatches (choosing one closes it; on phones, choose and then OK), and anything typed that isn't in the palette fails validation. Swatches keep their 8px spacing and wrap onto more rows, six to a row. Without data-palette it uses a built-in set of twelve.
With transparency
Colors are opaque unless you add data-alpha, which suits brand and theme colors. With it, the picker gets an opacity slider on a checkerboard and an A field (percent), and see-through colors are written as #RRGGBBAA, or with / 0.5 in the other formats. Pasted rgba(), #RGBA and / alpha values keep their opacity. Fully opaque colors stay six-digit.
Other color formats
The field holds hex by default. Set data-format to rgb, hsl or oklch to write the color that way instead; the picker then opens on that color space's fields. Out-of-range OKLCH colors keep their lightness and hue and lose chroma until they fit sRGB.
Reading the value
input fires as the color changes, change once it's settled (when the picker closes, or on blur after typing). Keyboard: arrows move the square's marker (Shift for bigger steps) and the hue; arrows move through swatches; Enter closes; Escape cancels. Alt+Down in the field opens the picker.
| Attribute or class | What it does |
|---|---|
data-m3-colorpicker | On a text field: attach the color picker. |
data-palette="#hex, #hex" | Swatches to offer. |
data-m3-colorpicker="palette" | Only the palette's colors are allowed. |
data-format="rgb" / "hsl" / "oklch" | Write the field in that format instead of hex. |
data-alpha | Allow transparency: an opacity slider, an A field, #RRGGBBAA. |
input.m3Color | The color as "#RRGGBB" ("#RRGGBBAA" when see-through with data-alpha), or null. |
.colorpicker | The 360px panel: surface-container-high, large corners, level 3. Below 600px it's inside a dialog. |
.cp-modes | A connected button group switching the number fields between RGB, HSL, HSB and OKLCH. |
.cp-hue | The hue slider, built like M3's Expressive slider: a 16px track and a 4px handle with gaps either side. |
.cp-swatch | A 40px swatch, 8px apart; circular, then a 12px rounded square with a check when chosen. |
Text inputs
Text fields
Text fields let people enter and edit text, in forms and dialogs.
Use it for
- Outlined fields for forms with many fields; filled fields to draw more attention
- A visible label on every field; supporting text for hints, counts and errors
Use something else
- Choosing from a short list: radio buttons or chips
- Searching content: the search bar
From Bootstrap: .form-control, .form-select and .form-floating keep their markup and render as M3 outlined fields, with a notched floating label and M3 error colors.
Outlined
Floating labels
Bootstrap's .form-floating gives the M3 label that rests inside the field and floats onto the outline when focused or filled. It springs into place (fast-spatial, a slight overshoot).
With m3-components.js, the outline gets a true notch behind the floated label, so the field works on any background. Without the script, the label hides the outline with its own background, which must match the surface behind the field. Cards, sheets and dialogs set this for you; elsewhere set --md-field-container to your background color.
Filled
Add .form-control-filled or .form-select-filled to the field, and .form-floating-filled to a floating wrapper.
Validation
Bootstrap's validation classes work as usual (.is-invalid, .is-valid, or .was-validated on the form). Errors use the M3 error color on the outline, label and supporting text, with no status icons. Submit this form empty to see it:
Character counter
data-m3-counter adds M3's character count to the end of the supporting text line, with any helper text in a following .form-text kept on the left. With maxlength it's a hard limit: typing stops there. With data-limit it's a soft one, for drafting: you can keep going, the count and the field turn to the error color, and the form won't submit until it's back under. The count is part of the field's description for screen readers, and getting near the limit, reaching it and going over are announced. data-autosize lets a textarea grow with its text (up to 320px, then it scrolls).
Setting a field's value from code doesn't fire input; call M3Components.refresh(field) afterwards to update its count. A browser sends a line break in a textarea as two characters (CR LF), but counts it as one, so allow for that on the server when checking length.
Input groups
Bootstrap input groups work and pick up the M3 field styling. M3 has no input group of its own.
Phone numbers
With m3-components.js, every <input type="tel"> formats itself as (XXX) XXX-XXXX while people type, paste or autofill. No extra markup. The cursor stays put while editing mid-number, and backspacing over (, ), the space or - removes the digit beside it. Pasted numbers like +1 915-555-1234 or 915.555.1234 are cleaned up, and a number starting with + and another country code is left as typed. The field gets a pattern (unless it has one), so validation flags an incomplete number.
The formatted value is what the form submits. For the digits only, use input.value.replace(/\D/g, ''). Add data-m3-mask="none" to a field to leave it unformatted.
Multi-select
A multi-select field holds several choices as input chips, picked from a menu you can search by typing.
Use it for
- Several choices from a long list, in the space of one field: skills, tags, recipients, categories
- Free-form tags, with suggestions
Use something else
- A handful of options, all visible: filter chips or checkboxes
- Choosing dozens and reviewing them side by side: a dual list
From Bootstrap: Bootstrap's multiple select is a box you Ctrl-click in. M3 has no multi-select, so this one (in the spirit of Select2) is built from M3 parts: the text field, input chips and the Expressive menu. It stays a <select multiple> underneath, so the form posts it as always and required works.
Search and pick
Put data-m3-multi-select on a <select multiple> with a <label>. What's chosen shows as input chips; type to filter the menu (the match is bolded), click or press Enter to pick, and the menu stays open for the next one. <optgroup>s are the menu's groups. Remove a chip with its , or press Backspace in an empty field.
Filled, with pictures and a limit
data-variant="filled" uses the filled text field. data-avatar on an option puts a picture in its chip and menu item, and data-max caps how many can be chosen (the menu says so when the cap is reached).
Tags, in order
data-create turns what's typed into a new option (Enter, or the "Add" item), like Select2's tags. data-reorder lets the chips be dragged into order (or moved with Alt+← / →); the options are reordered in the select, so the form posts them in that order.
Posts:
Without search
data-search="false" makes it a plain picker: the field opens the menu, and Space or Enter picks.
Keyboard: it follows the combobox pattern. ↓ / ↑ open the menu and move through it, Enter picks, Esc closes (and then clears the search). ← at the start of the field moves into the chips, ← / → move between them, and Delete removes one. Additions, removals and the number of matches are announced. The menu opens below the field, or above it when there isn't room.
| Attribute or class | What it does |
|---|---|
data-m3-multi-select | On a <select multiple>: shows it as a chip field over a menu. Its <label> (or data-label) is the floating label. |
data-variant="filled" | The filled text field (outlined otherwise). |
data-placeholder | Shown in the empty field once it has focus. |
data-search="false" | No typing: a plain picker. |
data-max | The most that can be chosen. |
data-create | Typed values become new options. |
data-reorder | Chips can be put in order; the form posts that order. |
data-avatar | On an <option>: a picture for its chip and menu item. |
M3Components.multiSelect(select) | Returns refresh(), open(), close(), add(value) and remove(value). |
Location picker
A location picker finds an address as you type, shows it on a map with a pin, and fills in the address and coordinates for the form.
Use it for
- Venues, offices, listings and anything else that's saved with its map position
- Addresses where getting the coordinates right matters
Use something else
- A mailing address with no need for a map: plain text fields
From Bootstrap: Neither Bootstrap nor M3 has one. This is built from M3 text fields, the Expressive menu for suggestions and a Google map tinted from the theme.
Address, map and fields
Put data-m3-location on an empty element, with your key in data-key and a form name in data-name. Type at least three characters and pick a suggestion: the fields fill in and the pin drops there. Drag the pin or click the map to fine-tune it, which updates the coordinates and leaves the address alone (data-reverse looks the address up as well). Typing coordinates moves the pin. data-value takes saved values as JSON, and the fields post as venue_data[address], venue_data[latitude] and so on. Without a key, the fields still work and the map says what's missing.
Only what you need
data-fields picks the fields, in order, from street_number, street_name, city, state, zip_code, country, latitude, longitude and building. This one is filled, has just the coordinates and is held above them with .location-picker-stacked.
Try these with your key. It's saved in this browser only. Restrict a real key to your sites in Google's console, since it's visible in the page. Google refuses a key restricted to websites on a page opened from a file, so serve the page to test.
In Google's console, enable Maps JavaScript API and Places API (New), plus Geocoding API for data-reverse. Keys on older projects that only have the legacy Places API work too: it falls back to that by itself. Maps loads only when the picker nears the screen or the address field gets focus, so a page that never shows it doesn't pay for a map load.
Keyboard: ↓ / ↑ move through the suggestions, Enter picks and Esc closes. The number of suggestions and what's picked are announced. The coordinate fields are the keyboard way to place the pin.
| Attribute | What it does |
|---|---|
data-m3-location | On an empty element: builds the picker. |
data-key | The Google Maps key (or set window.M3_GOOGLE_MAPS_KEY; not needed if the page already loads Maps). |
data-name | The form name: fields post as name[address] etc. |
data-value | Saved values, as JSON. |
data-fields | Which fields, in order (a comma list). |
data-label / data-placeholder | The address field's label / placeholder. |
data-required / data-disabled | The address is required / everything is read-only. |
data-variant="filled" | Filled text fields. |
data-center / data-zoom | Where the map starts with no location ("lat,lng", El Paso otherwise) / its zoom. |
data-types / data-country | Limit suggestions by Google place type / country code (comma lists). |
data-reverse | Moving the pin looks up the address too. |
data-map-id / data-map-style="default" | Your Google cloud map style / Google's own look, instead of the theme's tint. |
--location-map-height | The map's height (334px). |
m3:location | Event when the location changes; detail holds the values. |
M3Components.location(el) | Returns value(), set(values), map() and marker(). |
File upload
File upload lets people add files by dropping them in, browsing for them, or pasting them.
Use it for
- Attachments on a form: documents, photos, a résumé
- Uploads that start straight away, with progress, including very large files sent in chunks
Use something else
- One small image where the form looks plain: a regular form control with
type="file"
From Bootstrap: Bootstrap styles <input type="file"> as a text field with a browse button, and it can't take a drop or show what's chosen. Material has no file upload component, so this is Dropzone (v6) in M3 parts: a drop zone, Expressive segmented list rows and the linear progress indicator, in place of Dropzone's own stylesheet.
Dropzone does the work: checking files, thumbnails, uploads, chunking big files and retrying chunks, resizing images before they're sent. The framework adds what it doesn't do: a zone you can reach and use from the keyboard, pasting a file, the transfer panel, a retry button for a failed upload, screen-reader announcements, and a plain-form mode. Without Dropzone on the page, the field stays an ordinary file input.
Attach to a form
Wrap a file input in .file-upload with data-m3-file-upload. Drop files on the zone, click it to browse, or paste one (a screenshot, say) while it has focus. Each file is listed with a thumbnail or type icon, its size, and a remove button. accept, data-max-size ("10MB") and data-max-files on the input become Dropzone's checks: anything that doesn't fit is listed in red with the reason. With no upload URL, the input always holds the accepted files, so the form posts them as normal, to PHP's $_FILES or anything else, and required works too.
One file
Without multiple, a new file replaces the one before.
Upload as you go
With data-upload-url, each file is sent as soon as it's added, three at a time, the way Stanton Admin's form builder works: the form itself then posts without files, and the server links what was uploaded to the submission. Progress shows on the file and in the transfer panel (data-transfer-panel="false" leaves the panel out). Removing a file cancels its upload, and a failed one gets a retry button. This demo fakes the server; files with "fail" in the name fail.
For everything else, pass Dropzone's own options to M3Components.fileUpload; it rebuilds the uploader with them on top of the M3 defaults and returns the Dropzone. Large files in 1 MB chunks with retries, a CSRF token on every request, and files already on the server shown again after a failed submit:
Tiles
data-m3-file-upload="grid" shows the files as square tiles instead of rows, for photos and anything else people pick by how it looks. The zone spans the grid until a file is added, then becomes an "Add" tile after the last one, so the place to drop more stays beside the files. Uploads show a circular indicator over the tile; remove sits in the corner, always visible so it works on touch.
Long names are cut in the middle, the way Finder does it (Quarterly-repo…final-v3.pdf), so the end that tells versions and types apart always shows. The full name is the tooltip and is what screen readers read.
Color from the image. An image file lends its row or tile its own color, the content-based color M3 uses for media: its most vivid color fades into the surface behind the file, at most 30% of the mix, held where the text keeps its contrast on any image. Other files stay neutral. Tiles set the image into the card the way music apps do, with a bolder name (title medium, emphasized). data-image-color="false" on .file-upload turns it off.
Previews
Images and PDFs open from their row or tile: the whole file is the target, with a hover layer (and on a tile, a round primary button that rises into the image's corner, the way music apps do it, so the image itself is never covered), and a click or tap anywhere on it except its own buttons opens it. From the keyboard it's a button on the thumbnail (Enter or Space). An image opens in the media viewer, growing out of its thumbnail, with the uploader's other images in its strip; focus goes back to the thumbnail afterwards. A PDF opens in a new tab in the browser's own viewer. Word files, spreadsheets and the like can't be shown in a browser, so they have no preview.
Compact
data-m3-file-upload="button" swaps the zone for a tonal button, for tight spaces like a message composer. Dropping files anywhere on it and pasting still work.
Dropzone's events all work on the returned instance (dz.on('success', …), 'error', 'queuecomplete', and so on). Keyboard: the zone is a button (Enter or Space browses), and each file's remove button is in the tab order. Changes are announced to screen readers.
| Attribute or class | What it does |
|---|---|
.file-upload + data-m3-file-upload | Around an <input type="file">: the drop zone and file list (a Dropzone). ="button" for the compact version, ="grid" for tiles. |
accept, multiple, capture, required | On the input, as usual; they become Dropzone's checks. |
data-max-size / data-max-files | On the input: largest file ("10MB") / most files. |
data-upload-url | Upload each file as it's added, as the input's name. Without it, the form posts the files. |
data-transfer-panel="false" | Don't show uploads in the transfer panel. |
M3Components.fileUpload(el, options) | The Dropzone; with options, rebuilt with any Dropzone options on top. |
.file-upload-zone | The zone: surface-container-low, large corners, a dashed edge that turns primary while files are over it. |
data-m3-file-upload="grid" | Files as tiles (.file-upload-tiles, .file-tile); the zone becomes an "Add" tile once there are files. |
.fu-name-start / .fu-name-end | The two halves of a middle-truncated name (the full name is the tooltip). |
.file-upload-open | The preview button over an image's or PDF's thumbnail (.is-previewable on the file). |
M3Components.imageViewer(items, index) | The full-screen image viewer (.m3-viewer, a <dialog>). Items: { src, alt, title, width, height, thumb, opener }; returns { el, go(i), close() }. |
data-image-color="false" | On .file-upload: don't tint files with their thumbnail's color. |
.has-image-color | Set on a file once its color is read; --md-image-color holds it, as with imagery. |
.file-upload-item | A file row (Dropzone's preview template, a segmented list item), with Dropzone's dz-processing, dz-success and dz-error states. |
JavaScript
m3-components.js
Optional helpers for what CSS can't do on its own. Load it after Bootstrap's bundle; it has no other dependencies and scans the page (including content added later) automatically.
| Feature | What it adds |
|---|---|
| Ripples | Compose's press ripple on buttons, chips, nav items, list and menu items, tabs. |
| Sliders | The filled track on .form-range; the whole .slider component. |
| Tabs | One sliding indicator per .nav-tabs. |
| Floating labels | A true notch in the outlined field's border. |
| Menus | Close animation; cascading submenus with hover intent and keyboard support. |
| Navigation rail | Expand and collapse with data-m3-rail-toggle. |
| FAB menu, expanding toolbars | Open and close, Escape and outside click, focus handling. |
| App bars, extended FAB | Scrolled color, collapse on scroll, FAB collapse on scroll. |
| Wavy progress | SVG redraw each frame (round caps, eased amplitude). |
| Snackbars, transfer panel | M3Components.snackbar() and M3Components.transfer(). |
API
Data attributes
Everything the scripts respond to in HTML.
| Attribute | On | What it does |
|---|---|---|
data-m3-mode="light|dark|auto" | button | Switches color mode (customizer). |
data-m3-rail-toggle | button in .nav-rail | Expands / collapses the rail. |
data-m3-app-bar | .app-bar | Scrolled color once the page scrolls. |
data-m3-app-bar="collapse" | medium / large .app-bar | Collapses on scroll. |
data-m3-fab-collapse | .btn-fab-extended | Collapses scrolling down, expands scrolling up. |
data-m3-snackbar (+ -action, -duration, -dismissible, -stacked) | any clickable element | Shows a snackbar. |
data-m3-suffix | .slider | Text after the value indicator's number. |
data-m3-ripple / data-m3-no-ripple | any element | Opt in to / out of the ripple. |
m3-springs.js
CSS transitions restart from zero speed when interrupted. Compose's springs keep their momentum, so a switch flipped back mid-slide curves smoothly into the new direction. This optional script integrates real springs for the switch handle and progress values to match.
Spring constants come from the theme tokens, so the theme's springs and --md-motion-scale apply. Without the script everything still animates through CSS.
Theme API
The theme generator and customizer can be scripted, for example to build a client's theme from their brand color in your own tooling.
M3Theme (m3-theme.js)
M3Customizer (m3-customizer.js)
Generators
Node scripts used to build parts of the framework from Compose's source. You only need them to change those parts; the outputs are already in the repo.
| Script | Builds |
|---|---|
tools/shapes-gen.js | The loading indicator's shape morphs (tools/shapes.json), ported from graphics-shapes. Run node tools/shapes-gen.js 160 tools/shapes.json. |
tools/li-css.js | The loading indicator CSS block in m3-bootstrap.css. Run node tools/li-css.js . |
tools/material-shapes.js | css/m3-shapes.css and assets/shapes/*.svg. Run node tools/material-shapes.js. |
