material-web-dx
v0.1.1
Published
A DX library on top of Material Web — functional component API, theme builder, accessibility built-in
Downloads
183
Maintainers
Readme
Material Web DX
A developer experience library on top of Material Web. It provides a default theme, dynamic color generation, dark mode, accessibility helpers, and per-component typed theme directives — so you can use Material Web components with minimal setup.
Install
npm install material-web-dx @material/web lit-html@material/web and lit-html are peer dependencies.
Quick start
1. Import the base tokens and default theme
// Sizing, shape, typography fixes for Material Web
import "material-web-dx/theme/tokens.css";
// Default color palette (seed #5389df, Fidelity variant)
// Includes both light and dark mode
import "material-web-dx/theme/default.css";That's it. No JavaScript required for a working theme. All Material Web components will pick up the colors and sizing automatically.
2. Register the Material Web components you need
import "@material/web/button/filled-button.js";
import "@material/web/button/outlined-button.js";
import "@material/web/textfield/outlined-text-field.js";3. Use them in your HTML
<md-filled-button>Save</md-filled-button>
<md-outlined-text-field placeholder="Email" type="email"></md-outlined-text-field>Dark mode
The default theme CSS supports dark mode out of the box. It activates via:
| Method | How |
|---|---|
| OS preference | prefers-color-scheme: dark media query (automatic) |
| Explicit class | Add class="dark" to <html> |
| Data attribute | Add data-theme="dark" to <html> |
To force light mode even when the OS is set to dark:
<html class="light">or
<html data-theme="light">Programmatic dark mode
import { toggleDarkMode, setDarkMode } from "material-web-dx";
// Toggle between light and dark
toggleDarkMode();
// Set explicitly
setDarkMode(true); // dark
setDarkMode(false); // lightCustom themes
If the default palette doesn't fit your brand, generate a theme from any seed color:
import "material-web-dx/theme/tokens.css";
import { applyTheme } from "material-web-dx";
// Generate and apply a complete M3 color scheme from a single hex color
applyTheme("#6750A4", { variant: "Tonal Spot", dark: false });Scheme variants
| Variant | Description |
|---|---|
| Tonal Spot | Balanced, muted tones (Material default) |
| Fidelity | Colors stay close to the seed |
| Vibrant | Saturated, energetic palette |
| Expressive | Bold, dramatic color shifts |
| Neutral | Minimal color, mostly greys |
| Monochrome | Single-hue greyscale |
| Content | Derived from image content colors |
Generate without applying
import { generateTheme } from "material-web-dx";
const tokens = generateTheme("#6750A4", { dark: true, variant: "Vibrant" });
// tokens is a Record<string, string> of color token names to hex values
// e.g. { "primary": "#d0bcff", "on-primary": "#381e72", ... }Export and use a custom theme file
You can export a generated theme as CSS or SCSS and save it as a static file in your project. This is ideal for production apps where you want zero runtime theme generation — just a plain CSS file checked into your repo.
Step 1: Generate the CSS
import { exportThemeCSS, exportThemeSCSS } from "material-web-dx";
const css = exportThemeCSS("#6750A4", { variant: "Fidelity" });
const scss = exportThemeSCSS("#6750A4", { variant: "Fidelity", dark: true });
// Copy the output and save it as a file
console.log(css);You can also use the interactive Theme Builder on the docs site — pick your seed color and variant, then click "Export CSS" or "Export SCSS" and copy the output.
Step 2: Save as a file
Save the exported output as my-theme.css (or _my-theme.scss) in your project:
/* my-theme.css — exported from Material Web DX */
:root {
--md-sys-color-primary: #6750a4;
--md-sys-color-on-primary: #ffffff;
--md-sys-color-primary-container: #eaddff;
/* ... all 35 color tokens ... */
}Step 3: Import it instead of the default theme
// Base sizing/shape/typography fixes (always needed)
import "material-web-dx/theme/tokens.css";
// Your custom color theme instead of the default
import "./my-theme.css";That's it — no JavaScript, no runtime color generation. The tokens.css file handles sizing and shape, your custom file handles colors. All Material Web components pick up both automatically.
If you need both light and dark variants, export each separately and combine them:
/* my-theme.css */
/* Light */
:root {
--md-sys-color-primary: #6750a4;
/* ... */
}
/* Dark — activates via class, attribute, or OS preference */
:root.dark,
:root[data-theme="dark"] {
--md-sys-color-primary: #d0bcff;
/* ... */
}
@media (prefers-color-scheme: dark) {
:root:not(.light):not([data-theme="light"]) {
--md-sys-color-primary: #d0bcff;
/* ... */
}
}Listen for theme changes
import { onThemeChange } from "material-web-dx";
const unsubscribe = onThemeChange((state) => {
console.log(state.seedHex, state.variant, state.dark, state.tokens);
});
// Later: unsubscribe();Semantic color variables
Access M3 system colors as CSS variable references:
import { colors } from "material-web-dx";
// colors.primary → "var(--md-sys-color-primary)"
// colors.onPrimary → "var(--md-sys-color-on-primary)"
// colors.surface → "var(--md-sys-color-surface)"
// colors.error → "var(--md-sys-color-error)"
// ... all 35 M3 system colorsSupported components
Material Web DX works with all Material Web components. The base token overrides in tokens.css fix sizing and spacing for the following:
Buttons
| Component | Element |
|---|---|
| Filled button | <md-filled-button> |
| Outlined button | <md-outlined-button> |
| Text button | <md-text-button> |
| Filled tonal button | <md-filled-tonal-button> |
<md-filled-button>Save</md-filled-button>
<md-outlined-button>Cancel</md-outlined-button>
<md-text-button>Learn more</md-text-button>
<md-filled-tonal-button>Draft</md-filled-tonal-button>
<!-- With icons -->
<md-filled-button>
<md-icon slot="icon">add</md-icon>
Create
</md-filled-button>Icon button
| Component | Element |
|---|---|
| Icon button | <md-icon-button> |
<md-icon-button aria-label="Settings">
<md-icon>settings</md-icon>
</md-icon-button>Text fields
| Component | Element |
|---|---|
| Filled text field | <md-filled-text-field> |
| Outlined text field | <md-outlined-text-field> |
<md-filled-text-field placeholder="First name"></md-filled-text-field>
<md-outlined-text-field placeholder="Email" type="email">
<md-icon slot="leading-icon">mail</md-icon>
</md-outlined-text-field>
<!-- With validation -->
<md-filled-text-field
placeholder="Required field"
error
error-text="This field is required"
></md-filled-text-field>Select
| Component | Element |
|---|---|
| Filled select | <md-filled-select> |
| Outlined select | <md-outlined-select> |
<md-outlined-select>
<md-select-option selected value="">
<div slot="headline">Choose a country</div>
</md-select-option>
<md-select-option value="us">
<div slot="headline">United States</div>
</md-select-option>
<md-select-option value="uk">
<div slot="headline">United Kingdom</div>
</md-select-option>
</md-outlined-select>Selection controls
| Component | Element |
|---|---|
| Checkbox | <md-checkbox> |
| Switch | <md-switch> |
| Radio | <md-radio> |
| Slider | <md-slider> |
<!-- Checkbox -->
<label><md-checkbox checked touch-target="wrapper"></md-checkbox> Notifications</label>
<!-- Switch -->
<label><md-switch selected></md-switch> Wi-Fi</label>
<!-- Radio group -->
<label><md-radio name="size" value="s" checked></md-radio> Small</label>
<label><md-radio name="size" value="m"></md-radio> Medium</label>
<label><md-radio name="size" value="l"></md-radio> Large</label>
<!-- Slider -->
<md-slider value="40"></md-slider>
<md-slider range value-start="20" value-end="70"></md-slider>Chips
| Component | Element |
|---|---|
| Chip set | <md-chip-set> |
| Filter chip | <md-filter-chip> |
| Assist chip | <md-assist-chip> |
<md-chip-set>
<md-filter-chip label="Running" selected></md-filter-chip>
<md-filter-chip label="Cycling"></md-filter-chip>
</md-chip-set>
<md-chip-set>
<md-assist-chip label="Share">
<md-icon slot="icon">share</md-icon>
</md-assist-chip>
</md-chip-set>Tabs
| Component | Element |
|---|---|
| Tabs container | <md-tabs> |
| Primary tab | <md-primary-tab> |
| Secondary tab | <md-secondary-tab> |
<md-tabs>
<md-primary-tab>
<md-icon slot="icon">flight</md-icon>
Flights
</md-primary-tab>
<md-primary-tab>
<md-icon slot="icon">hotel</md-icon>
Hotels
</md-primary-tab>
</md-tabs>
<md-tabs>
<md-secondary-tab>Overview</md-secondary-tab>
<md-secondary-tab>Specs</md-secondary-tab>
<md-secondary-tab>Reviews</md-secondary-tab>
</md-tabs>List
| Component | Element |
|---|---|
| List | <md-list> |
| List item | <md-list-item> |
| Divider | <md-divider> |
<md-list>
<md-list-item>
<md-icon slot="start">person</md-icon>
<div slot="headline">Alice Johnson</div>
<div slot="supporting-text">Product Designer</div>
<md-icon slot="end">chevron_right</md-icon>
</md-list-item>
<md-divider></md-divider>
<md-list-item>
<md-icon slot="start">person</md-icon>
<div slot="headline">Bob Smith</div>
<div slot="supporting-text">Engineer</div>
<md-icon slot="end">chevron_right</md-icon>
</md-list-item>
</md-list>Dialog
| Component | Element |
|---|---|
| Dialog | <md-dialog> |
<md-dialog id="my-dialog">
<div slot="headline">Save changes?</div>
<form slot="content" method="dialog">
Your unsaved changes will be lost.
</form>
<div slot="actions">
<md-text-button @click=${() => dialog.close()}>Discard</md-text-button>
<md-filled-button @click=${() => dialog.close()}>Save</md-filled-button>
</div>
</md-dialog>FAB (Floating Action Button)
| Component | Element |
|---|---|
| FAB | <md-fab> |
<!-- Icon only -->
<md-fab><md-icon slot="icon">edit</md-icon></md-fab>
<!-- Extended with label -->
<md-fab label="Compose"><md-icon slot="icon">create</md-icon></md-fab>
<!-- Sizes: small, medium (default), large -->
<md-fab size="small"><md-icon slot="icon">add</md-icon></md-fab>
<md-fab size="large"><md-icon slot="icon">add</md-icon></md-fab>
<!-- Color variants: primary (default), secondary, tertiary -->
<md-fab variant="secondary" label="Navigate">
<md-icon slot="icon">navigation</md-icon>
</md-fab>Progress indicators
| Component | Element |
|---|---|
| Linear progress | <md-linear-progress> |
| Circular progress | <md-circular-progress> |
<!-- Determinate -->
<md-linear-progress value="0.6"></md-linear-progress>
<md-circular-progress value="0.7"></md-circular-progress>
<!-- Indeterminate -->
<md-linear-progress indeterminate></md-linear-progress>
<md-circular-progress indeterminate></md-circular-progress>Menu
| Component | Element |
|---|---|
| Menu | <md-menu> |
| Menu item | <md-menu-item> |
<div style="position: relative">
<md-filled-button id="anchor">Open menu</md-filled-button>
<md-menu anchor="anchor">
<md-menu-item>
<div slot="headline">Cut</div>
</md-menu-item>
<md-menu-item>
<div slot="headline">Copy</div>
</md-menu-item>
<md-menu-item>
<div slot="headline">Paste</div>
</md-menu-item>
</md-menu>
</div>Accessibility helpers
Focus trap
Trap keyboard focus inside a container (useful for modals and dialogs):
import { focusTrap } from "material-web-dx";
const release = focusTrap(dialogElement);
// When the dialog closes:
release();Keyboard navigation
Add arrow-key navigation to lists, menus, or tabs:
import { keyboardNav } from "material-web-dx";
const cleanup = keyboardNav(listElement, {
direction: "vertical", // "horizontal" | "vertical" | "both"
wrap: true, // wrap around at edges
selector: "md-list-item" // custom selector for focusable items
});
// Later: cleanup();ARIA helpers
import { uniqueId, spreadAttrs } from "material-web-dx";
const id = uniqueId("dialog"); // "dialog-1", "dialog-2", ...
const attrs = spreadAttrs({
"aria-label": "Close",
"aria-hidden": undefined, // omitted
"aria-expanded": false, // omitted
});
// { "aria-label": "Close" }What's included
| Import path | What it does |
|---|---|
| material-web-dx/theme/tokens.css | Base sizing, shape, typography fixes |
| material-web-dx/theme/default.css | Default color theme (light + dark) |
| material-web-dx | Theme API, directives, components, a11y |
| material-web-dx/directives | Per-component typed theme directives |
| material-web-dx/a11y | Focus trap, keyboard nav, ARIA helpers |
Theme API reference
| Function | Description |
|---|---|
| applyTheme(seed, options?) | Generate and apply a color scheme to the document |
| generateTheme(seed, options?) | Generate a color scheme without applying |
| applyColorTokens(tokenMap) | Apply a custom set of color tokens |
| toggleDarkMode() | Toggle between light and dark mode |
| setDarkMode(isDark) | Set dark mode on or off |
| onThemeChange(listener) | Subscribe to theme state changes; returns unsubscribe fn |
| exportThemeCSS(seed, options?) | Export theme as a CSS string |
| exportThemeSCSS(seed, options?) | Export theme as an SCSS string |
ThemeOptions
interface ThemeOptions {
dark?: boolean; // default: false
variant?: string; // default: "Fidelity"
}License
MIT
