Components
Live previews rendered with the real library. Every section shows props and a copy-paste snippet. Use the adaptive playground at the bottom to flip density and TV focus mode.
Buttons
Five variants, five sizes, icon buttons, FABs, and connected segments.
| Component | Prop | Default | Description |
|---|---|---|---|
Button | 'filled' | 'tonal' | 'elevated' | 'outlined' | 'text' | 'filled' | Variant sets color and elevation emphasis |
Button | 'xs' | 's' | 'm' | 'l' | 'xl' | 's' | Size — Expressive heights 32 / 40 / 56 / 96 / 136px |
Button | href?: string | undefined | Renders an anchor instead of a button |
IconButton | 'standard' | 'filled' | 'tonal' | 'outlined' | 'standard' | Icon-only button; label sets aria-label |
IconButton | toggle?: boolean, selected?: boolean | false | Toggle semantics with aria-pressed |
FAB | extended?: boolean, plain?: boolean | false | Extended shows a label; plain drops elevation |
FAB | 's' | 'm' | 'l' | 'm' | FAB size — 40, 56, or 96px |
ConnectedButtons | connected?: boolean, label?: string | true | Joins child buttons into a segmented group |
Code
<Button variant="filled" size="m">Filled</Button>
<Button variant="tonal">Tonal</Button>
<Button href="/docs/" variant="outlined">Docs</Button>
<IconButton label="Share" variant="filled">
<Icon name="share" />
</IconButton>
<FAB extended label="Compose">
<Icon name="edit" />Compose
</FAB>
<ConnectedButtons label="View mode">
<Button variant="tonal" size="s">Day</Button>
<Button variant="tonal" size="s">Week</Button>
</ConnectedButtons>Cards & List
Surface containers for content, plus two-line list items.
Elevated
Rests on the surface with a subtle shadow.
Filled
Primary container tint, no shadow.
Tonal
Secondary container for supporting content.
Outlined
Quietest option for dense layouts.
Two-line list item Supporting text wraps under the headline and stays readable at every breakpoint.
Second item ListItem keeps 56px rows by default, 48px in compact density.
| Component | Prop | Default | Description |
|---|---|---|---|
Card | 'elevated' | 'filled' | 'outlined' | 'tonal' | 'elevated' | Surface treatment |
Card | onclick?, href?, as? | undefined | Makes the card a button or link target |
ListItem | headline: string | required | Primary line of text |
ListItem | supporting?: string | undefined | Secondary line — renders two-line rows |
Code
<Card variant="elevated" onclick={open}>
<h3>Elevated</h3>
<p>Rests on the surface with a subtle shadow.</p>
</Card>
<Card variant="outlined">
<ListItem
headline="Two-line list item"
supporting="Secondary text stays readable at every breakpoint."
/>
</Card>Forms
Inputs, selection controls, chips, and progress indicators.
TextField outlined We never share it.
TextField filled
TextField error Only letters and numbers.
Multiline
Checkbox & Switch
Radio group
Slider — 40
Select
Chip
Svelte
DateField
CircularProgress determinate
CircularProgress indeterminate
LinearProgress
LoadingIndicator
| Component | Prop | Default | Description |
|---|---|---|---|
TextField | variant?: outlined | filled | 'outlined' | Outline style; invalid shows error color |
TextField | bind:value: string | '' | Bound text; supportingText shows helper line |
Checkbox | bind:checked: boolean, label?: string | false | Custom box with animated check |
Radio | value: string, bind:checked: boolean | required | Pair with name for native group semantics |
Switch | bind:checked: boolean, label?: string | false | role="switch" track control |
Slider | bind:value, min, max, step | 0 / 100 / 1 | Continuous numeric input |
Select | options: { value, label }[], bind:value: string | [] | Dropdown listbox with anchored popup |
Chip | variant?: assist | filter | input | suggested | 'assist' | Chip role; label provides the text |
Chip | bind:selected?: boolean | false | Filter chips toggle selected state |
CircularProgress | value?: number, min?, max? | undefined / 0 / 1 | Determinate within min–max; omitted = indeterminate |
LinearProgress | value?: number, min?, max? | undefined / 0 / 1 | Determinate within min–max; omitted = indeterminate |
DateField | bind:value: string | '' | ISO date text with docked picker; invalid marks errors |
Code
<TextField label="Email" bind:value={email} supportingText="We never share it." />
<TextField label="Username" value="a b" invalid supportingText="Only letters and numbers." />
<Checkbox bind:checked={agree} label="Accept terms" />
<Radio name="drink" value="filter" label="Filter" checked={drink === 'filter'}
onchange={() => (drink = 'filter')} />
<Switch bind:checked={soundOn} label="Sound" />
<Slider bind:value={volume} min={0} max={100} />
<Select label="Topic" options={[{ value: 'motion', label: 'Motion' }]} bind:value={topic} />
<Chip variant="filter" label="Wi-Fi" bind:selected={filterChip} />
<Chip variant="input" label="Svelte" onRemove={removeChip} />
<DateField bind:value={dueDate} aria-label="Due date" />
<CircularProgress value={64} max={100} />
<CircularProgress />
<LinearProgress value={64} max={100} />
<LoadingIndicator />Overlays
Dialog, menu, sheets, and the imperative snackbar.
| Component | Prop | Default | Description |
|---|---|---|---|
Dialog | bind:open: boolean, title?: string | false | Modal dialog with scrim and actions snippet |
Menu | bind:open: boolean, anchorEl?: HTMLElement | false | Popup surface positioned against its trigger |
MenuItem | children, onclick?, onselect?, selected? | undefined | Row inside a menu; closes the menu on click |
BottomSheet | bind:open: boolean, title?: string | false | Modal sheet sliding from the bottom edge |
SideSheet | bind:open: boolean, title?: string, side?, modal? | false / undefined / 'end' / false | Supplementary panel from the inline edge |
snackbar() | (text, { action?, duration? }) => void | 4000ms | Imperative toast; mount <Snackbar /> once |
Code
<script lang="ts">
import { Button, Dialog, Menu, MenuItem, Snackbar, snackbar } from 'm3nerd';
let open = $state(false);
let menuOpen = $state(false);
let anchor = $state<HTMLSpanElement | null>(null);
</script>
<Button onclick={() => (open = true)}>Open dialog</Button>
<Dialog bind:open title="Discard draft?">
<p>Your changes will be lost.</p>
{#snippet actions()}
<Button variant="text" onclick={() => (open = false)}>Cancel</Button>
<Button variant="filled" onclick={() => (open = false)}>Discard</Button>
{/snippet}
</Dialog>
<span bind:this={anchor}>
<Button onclick={() => (menuOpen = true)}>Open menu</Button>
</span>
<Menu bind:open={menuOpen} anchorEl={anchor}>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
</Menu>
<Snackbar />
<button onclick={() => snackbar('Saved')}>Save</button>Feedback
Badges call attention to icons and avatars.
| Component | Prop | Default | Description |
|---|---|---|---|
Badge | value?: string | number, max?: number | undefined / 99 | Badge count; values over max render as max+ |
Badge | dot?: boolean, label?: string | false | Dot variant; label names it for assistive tech |
Badge | children | required | Wraps the icon or avatar being badged |
Code
<Badge value={3}>
<span class="avatar">A</span>
</Badge>
<Badge value={120} max={99}>
<Icon name="notifications" size={28} />
</Badge>
<Badge dot label="Unread items">
<Icon name="campaign" size={28} />
</Badge>Adaptive playground
Flip the document-level attributes and watch tokens respond. The ruler tracks the live viewport width and its breakpoint.
Token sample
0px compact
60084012001600
- compact < 600px
- medium 600–839px
- expanded 840–1199px
- large 1200–1599px
- xlarge ≥ 1600px
| Component | Prop | Default | Description |
|---|---|---|---|
data-density | 'compact' | 'spacious' | default scale | Sets --m3-density-scale on <html> |
data-focus-mode | 'tv' | unset | 64px targets, larger type, glowing focus ring |
window width | matchMedia breakpoints | live | compact | medium | expanded | large | xlarge |
Code
<script lang="ts">
$effect(() => {
const root = document.documentElement;
if (density === 'compact') root.dataset.density = 'compact';
else delete root.dataset.density;
});
$effect(() => {
const update = () => (width = window.innerWidth);
update();
const queries = [600, 840, 1200, 1600].map((w) => {
const mq = matchMedia('(min-width: ' + w + 'px)');
mq.addEventListener('change', update);
return mq;
});
window.addEventListener('resize', update);
return () => {
for (const mq of queries) mq.removeEventListener('change', update);
window.removeEventListener('resize', update);
};
});
</script>