@m3ui-vue/m3ui-vue
170+ tree-shakeable components following Material Design 3 guidelines. Ships with 40 color palettes, dark mode, i18n support, and full TypeScript coverage.
Getting Started
Install
pnpm add @m3ui-vue/m3ui-vue
Fonts
MIcon and every component that renders an icon just apply the material-symbols-outlined class from theme.css — the library ships the CSS rule (sizing, font-feature-settings, etc.) but not the font file itself, so you need to supply Material Symbols Outlined (and Roboto) yourself, via one of two options:
Option A — Google Fonts CDN (default, simplest). Add to your index.html <head>:
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Roboto+Mono:wght@400&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap"
rel="stylesheet"
/>
Option B — self-hosted (offline / no third-party requests). The CDN link needs network access to fonts.googleapis.com, which won't work offline or behind a restrictive firewall. Install material-symbols (ships the .woff2 files locally) and import it from your main stylesheet instead of the CDN link:
pnpm add material-symbols
@import 'material-symbols/outlined.css';
material-symbols is listed as an optional peer dependency for this reason. For the text fonts, @fontsource/roboto and @fontsource/roboto-mono do the same job:
pnpm add @fontsource/roboto @fontsource/roboto-mono
@import '@fontsource/roboto/400.css';
@import '@fontsource/roboto/500.css';
@import '@fontsource/roboto/700.css';
@import '@fontsource/roboto-mono/400.css';
Roboto isn't required — without it, text falls back to system-ui.
Using a different font
Every component reads its typeface from two tokens in theme.css: --font-sans (default 'Roboto', system-ui, …) and --font-mono (default 'Roboto Mono', ui-monospace, …, used by MCodeEditor and MMarkdown code). Override them after importing the theme, and load the font yourself (CDN, @fontsource/*, or your own @font-face):
@import '@m3ui-vue/m3ui-vue/theme';
@theme {
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
}
To switch at runtime, set the variable on <html> — components (including MChart's canvas) pick it up live:
document.documentElement.style.setProperty('--font-sans', "'Inter', system-ui, sans-serif")
CSS
In your main stylesheet (e.g. src/style.css):
@import 'tailwindcss';
@import '@m3ui-vue/m3ui-vue/theme';
@import '@m3ui-vue/m3ui-vue/palettes'; /* 40 color palettes */
@import '@m3ui-vue/m3ui-vue/styles'; /* component transitions & animations */
Plugin (optional)
import { createApp } from 'vue'
import { createM3UI } from '@m3ui-vue/m3ui-vue'
import App from './App.vue'
const app = createApp(App)
app.use(createM3UI({ palette: 'teal' }))
app.mount('#app')
Without the plugin the default purple palette is used.
palette only seeds the first visit — once a user switches palettes (e.g. via useColorPalette().set()), that choice is persisted to localStorage and wins over palette on every later boot. To force a fixed palette that always wins regardless of localStorage, pass persistPalette: false:
app.use(createM3UI({ palette: 'teal', persistPalette: false }))
useColorPalette().set() still works live within the session (e.g. for a per-page/temporary palette), it just stops persisting — nothing is written to localStorage while persistPalette is disabled. Toggle it at runtime with useColorPalette().setPersistPalette(true | false).
Usage
<script setup>
import { MButton, MCard, MTextField, useToast } from '@m3ui-vue/m3ui-vue'
const toast = useToast()
const name = ref('')
</script>
<template>
<MCard>
<MTextField v-model="name" label="Name" />
<MButton icon="save" @click="toast.success('Saved!')">
Save
</MButton>
</MCard>
</template>
Components
Actions
MButton |
Primary, outlined, tonal, text, elevated variants |
MIconButton |
Icon-only actions |
MFab |
Floating action button with optional speed dial |
MFabMenu |
M3 FAB Menu — expands into a labeled action list (built on MFab's #content slot) |
MButtonGroup |
Grouped button options |
MSplitButton |
Button with dropdown actions |
MSegmentedButton |
Toggleable segmented options |
MToggleButton |
M3 Expressive's standalone toggle button — morphs shape (pill ↔ squared) and swaps color on check, filled/tonal/outlined/elevated |
MCopyButton |
Copy-to-clipboard icon button with success feedback |
MExportButton |
Export an array of objects to CSV or JSON |
Inputs
MTextField |
Text input with label, icons, validation |
MNumberField |
Numeric input with stepper |
MMaskField |
Input with mask presets (phone, currency, etc.) |
MPinInput |
OTP/PIN code input with N boxes, optional grouping/separators |
MTagInput |
Tag/chip input |
MSelect |
Single select dropdown |
MMultiSelect |
Multi-select with chips |
MAutocomplete |
Single-value autocomplete |
MMultiAutocomplete |
Multi-value autocomplete |
MCheckbox |
Checkbox |
MSwitch |
Toggle switch |
MRadio / MRadioGroup |
Radio options |
MSlider |
Range slider |
MRating |
Star rating |
MDatePicker |
Inline date picker |
MDatePickerModal |
Modal date picker |
MDateRangePicker |
Inline date range picker |
MDateRangePickerModal |
Modal date range picker |
MTimePicker |
Inline time picker |
MTimePickerModal |
Modal time picker |
MColorPicker |
Inline color picker |
MColorPickerModal |
Modal color picker |
MFileUpload |
Drag & drop file upload |
MPasswordMeter |
Password strength meter (pair with MTextField type="password") |
MSignaturePad |
Canvas signature capture with undo/clear |
Data Display
MCard / MStatCard |
Content cards |
MChip |
Chips / tags |
MBadge |
Notification badges |
MAvatar |
User avatars |
MIcon |
Material Symbols icons |
MTable |
Simple table with sorting, pagination |
MDataTable |
Server-side data table |
MVirtualTable |
Virtualized table for large datasets |
MTreeTable |
Hierarchical table |
MTree |
Tree view |
MList / MListItem / MListSubheader |
Lists |
MTimeline |
Vertical timeline |
MCalendar |
Event calendar |
MCarousel |
The M3 carousel, ported from Compose Material3: multi-browse, uncontained, hero and full-screen layouts, horizontal or vertical. Items keep one size and are masked as they scroll; drag, wheel and keyboard, v-model on the current item, #item slot for custom content |
MSimpleCarousel |
Fixed-width slide carousel (hero/multi-browse*/uncontained layouts) — the old MCarousel, renamed since MCarousel is now the real M3 carousel. *its own multi-browse layout mode is a simpler fixed-width approximation, not the real masking behavior MCarousel implements |
MLightbox |
Beta. Full-screen image viewer: swipe between images, zoom (double tap, pinch, wheel), drag down to close, opens from and closes into the image you tapped; auto-detects <img> elements or takes items |
MJsonViewer |
JSON tree viewer |
MRelativeTime |
Auto-updating "3 minutes ago" / "in 2 hours" text, locale-aware via Intl.RelativeTimeFormat |
Feedback
MAlert |
Inline alerts |
MSnackbar |
Toast notifications: call useToast(), which mounts it for you |
useNotification() |
Rich notifications; mounts its own host, nothing to place |
MDialog / MConfirmDialog |
Modal dialogs |
MBottomSheet |
Bottom sheet |
MBottomSheetScaffold |
Standard (non-modal) bottom sheet over a screen, peeking until dragged up |
MSideSheet |
Side sheet |
MProgressBar |
Linear progress |
MCircleProgressBar |
Circular progress (circle or wavy variant) |
MPageProgress |
Top/bottom page-loading bar, auto-wired to vue-router when present, or driven by hand via usePageProgress() |
MSpinner |
Circular spinner |
MLoadingIndicator |
M3 expressive loading indicator — morphs between rounded shapes (circle, cookie, pentagon, sunny, clover, ellipse); optional contained variant |
MPullToRefresh |
Drag-to-refresh wrapper around its own scrollable container (touch drag on mobile, click-and-drag with mouse/pen); mainly useful for a nested scroll panel or an app-shell context where the browser's own pull-to-refresh doesn't apply |
MLoadingOverlay |
Full-area loading |
MSkeleton |
Content skeleton loader |
MEmptyState |
Empty state placeholder |
MResult |
Success/error/info result pages |
MTooltip |
Tooltips |
Navigation
MTabs |
Tab navigation |
MMenu / MMenuItem / MMenuDivider |
Dropdown menus |
MContextMenu |
Right-click context menu |
MBreadcrumbs |
Breadcrumb trail |
MStepper |
Step-by-step wizard |
MPagination |
Page navigation |
MNavigationBar |
Bottom navigation bar |
MShortNavigationBar |
M3 Expressive's shorter (64dp) navigation bar — equal or centered item arrangement, top or start icon position |
MNavigationRail |
Side navigation rail |
MNavigationDrawer |
Collapsible side drawer |
MTopAppBar / MAppBar |
Top app bar: small, center-aligned, medium, large and the flexible ones, with scroll behaviors |
MBottomAppBar |
Bottom app bar with actions and a FAB, or the Expressive flexible one; can hide on scroll |
MAppBarWithSearch |
Top app bar with a search bar in place of the title (or only the search field) |
MSearchBar |
M3 search bar — docked (dropdown-style), fullscreen or contained fullscreen expansion, drop it anywhere including a MTopAppBar title slot |
MToolbar |
Action toolbar |
Layout
MContainer |
Centered max-width container |
MAppLayout |
Full app layout scaffold |
MNavigationSuite |
Adaptive navigation (M3 NavigationSuiteScaffold): short bar on phones, rail from tablets up, chosen by the component's own size |
MListDetail |
M3 list-detail layout: two panes from 840px, one at a time (with back()) below |
MSupportingPane |
M3 supporting pane layout: beside the main pane from 840px, below it or in a bottom sheet below |
MGrid |
Responsive grid |
MStack / MFlex / MBox |
Flexbox utilities |
MCenter |
Center content |
MSpacer |
Flexible spacer |
MSplitter |
Resizable split panes |
MMasonry |
Masonry grid |
MDivider |
Horizontal/vertical divider |
MExpansionPanel |
Collapsible panel |
MSection |
Semantic section |
MScrollable |
Scrollable area |
MAbsolute / MRelative / MFixed / MSticky |
Positioning |
MFullscreen |
Fullscreen wrapper |
MAspectRatio |
Aspect ratio container |
MShape |
Clips content (or renders a filled shape) into one of M3's 35 expressive shapes, with spring-animated morphing between shapes |
MResponsive |
Responsive breakpoint slots |
MOverlay |
Overlay backdrop |
MWindow |
Window/panel container |
MBar |
Draggable Electron title bar with minimize/maximize/close, for frameless windows |
MFooter |
Page footer |
Typography
MText |
Body text with variants |
MTitle |
Section title |
MSubtitle |
Section subtitle |
MKbd |
Keyboard shortcut hint (e.g. Ctrl+K) |
Emoji
MEmoji |
Render emoji by shortcode |
MEmojiButton |
Emoji picker trigger |
MEmojiSelector |
Emoji picker (Emoji 15.0, Android/Gboard style): categories, recently used, skin tones, search by CLDR name in the app's language and English, keyboard navigation |
Chat
MChatBubble |
Chat message bubble |
Advanced
MKanban |
Kanban board |
MScheduler |
Week/day scheduler |
MCommandPalette |
Command palette (Cmd+K) |
MSpotlightSearch |
Spotlight-style search |
MHotkeys |
Keyboard shortcut handler |
MDragDropList |
Drag & drop sortable list |
MTransferList |
Dual-list transfer |
MInfiniteScroll |
Infinite scroll wrapper |
MTour |
Feature tour / onboarding |
MShader |
Shadertoy-style GLSL/WGSL fragment shader renderer (WebGPU with WebGL2 fallback) |
Separate Entry Points
These components have heavy peer dependencies and are imported from dedicated sub-paths:
import { MRichTextEditor } from '@m3ui-vue/m3ui-vue/rich-text-editor'
import { MCodeEditor, MJsonEditor } from '@m3ui-vue/m3ui-vue/code-editor'
import { MMarkdown } from '@m3ui-vue/m3ui-vue/markdown'
import { MTerminal } from '@m3ui-vue/m3ui-vue/terminal'
import { MChart } from '@m3ui-vue/m3ui-vue/chart'
import { MQRCode } from '@m3ui-vue/m3ui-vue/qrcode'
import { MBarcode } from '@m3ui-vue/m3ui-vue/barcode'
API reference for AI agents
The package ships dist/API.md: every component's props (types, defaults), events, slots, exposed methods and
types, plus the composables, locale keys and theme tokens, in one compact file generated from the source on every
build. Point your coding agent at it, e.g. in your project's CLAUDE.md or AGENTS.md:
UI components come from @m3ui-vue/m3ui-vue; its API is in node_modules/@m3ui-vue/m3ui-vue/dist/API.md.
Composables
useTheme() |
Light / dark / system theme with cycle() |
useColorPalette() |
Switch between 40 palettes at runtime |
useToast() |
Programmatic toast notifications |
useNotification() |
Rich notifications with actions |
useFieldBg() |
Context-aware field background color |
useLocale() |
i18n locale switching |
useDevice() |
Responsive breakpoint detection |
useWindowSizeClass() |
M3 width/height size classes (compact → extra-large) for the window or any element |
usePageProgress() |
Shared state for MPageProgress's start()/set()/done() — drive it by hand or let the component auto-wire vue-router |
useSpring() |
Animate numbers on a real spring with M3's motion specs (motionSprings), like the components do |
Motion tokens: M3's easings are Tailwind ease-* utilities (ease-emphasized, ease-emphasized-decelerate…) and its durations CSS variables (duration-(--duration-medium-2)); in JS, motionSprings (Standard and Expressive), motionEasings, motionDurations and cubicBezier().
Color Palettes
40 built-in palettes with light and dark variants:
purple (default) indigo navy blue cyan teal green lime olive amber sand orange deep-orange brown red coral crimson pink violet slate graphite charcoal steel ash iron fog pewter smoke stone zinc monochrome rust gold emerald turquoise cobalt sapphire amethyst magenta mauve
Contrast levels
Medium and high contrast for every palette (light and dark), as in Android's accessibility settings: import the
optional stylesheet after palettes, then pick a level.
@import '@m3ui-vue/m3ui-vue/palettes-contrast'; /* ~27 KB gzipped */
app.use(createM3UI({ contrast: 'system' })) // 'standard' | 'medium' | 'high' | 'system' (follows prefers-contrast)
useColorPalette().setContrast('high') // at runtime, remembered like the palette
A seed palette is regenerated at the chosen level, with or without the stylesheet.
Dynamic Color (from a seed)
Instead of a named palette, generate the whole scheme (light and dark) from one color with Google's dynamic color algorithm:
app.use(createM3UI({ seed: '#0b57d0', seedOptions: { variant: 'tonal-spot', contrast: 0 } }))
At runtime, useColorPalette().setSeed('#0b57d0'); set(id) switches back to a named palette (the last choice wins). A named palette wins over seed in createM3UI. generateScheme(seed, options) returns the tokens if you want them yourself.
Browser Bar Color on Phones
createM3UI({ themeColor: 'surface' }) keeps <meta name="theme-color"> in sync with a color role of the active palette (palette, seed and dark mode changes included). Off by default. Chrome and Samsung Internet on Android ignore it in a regular tab while the system is in dark mode; installed apps (PWA) still get it.
Switch at Runtime
<script setup>
import { useColorPalette } from '@m3ui-vue/m3ui-vue'
const { palette, palettes, set } = useColorPalette()
</script>
<template>
<select @change="set(($event.target as HTMLSelectElement).value)">
<option v-for="p in palettes" :key="p.id" :value="p.id" :selected="p.id === palette">
{{ p.label }}
</option>
</select>
</template>
Selection is persisted to localStorage, unless persistence was disabled via createM3UI({ persistPalette: false }) or useColorPalette().setPersistPalette(false) — set() still applies the palette live either way.
Custom Palettes
Define the tokens under your own ID in CSS ([data-palette='brand'] { --color-primary: ...; } plus a [data-palette='brand'].dark { ... } block, overriding the same tokens as palettes.css), then activate it with set('brand'). To list it in pickers built from palettes, register it in the plugin; an entry with a built-in ID replaces that palette:
app.use(createM3UI({
customPalettes: [{ id: 'brand', label: 'Brand', seed: '#0B6E4F' }],
}))
Dark Mode
<script setup>
import { useTheme } from '@m3ui-vue/m3ui-vue'
const { theme, cycle } = useTheme()
</script>
<template>
<MButton @click="cycle">Theme: {{ theme }}</MButton>
</template>
Supports light, dark, and system. Persisted to localStorage and respects prefers-color-scheme.
Peer Dependencies
Install only the packages you need:
| Component | Packages |
|---|---|
MRichTextEditor |
@tiptap/vue-3 @tiptap/starter-kit @tiptap/extension-placeholder @tiptap/extension-underline @tiptap/extension-text-align @tiptap/extension-link @tiptap/extension-image @tiptap/extension-color @tiptap/extension-text-style @tiptap/extension-highlight |
MCodeEditor MJsonEditor |
codemirror @codemirror/view @codemirror/state @codemirror/commands @codemirror/language @codemirror/lang-javascript @codemirror/lang-json @codemirror/lang-html @codemirror/lang-css @codemirror/lang-python @codemirror/theme-one-dark |
MTerminal |
@xterm/xterm @xterm/addon-fit |
MMarkdown |
markdown-it (+ optional highlight.js for syntax highlighting) |
MChart |
chart.js vue-chartjs |
MQRCode |
qrcode-generator |
MBarcode |
jsbarcode |
About this project
M3UI Vue started as a set of components I built for my own projects. Over time it grew into something I thought others might find useful, so I decided to open-source it and keep improving it so anyone can use it for free.
That said, this is a personal project maintained in my spare time. I can't offer round-the-clock support or guarantee response times, and features are prioritized around what I need and what I have time for.
Contributions are very welcome:
- Found a bug? Open an issue with steps to reproduce it (a minimal example helps a lot).
- Want to fix something or add a feature? Pull requests are appreciated. For larger changes, opening an issue first to discuss the idea is a good way to avoid wasted effort.
Thanks for using it! ⭐ Starring the repo is also a nice way to show support.
License
MIT
The package bundles code from a few open-source packages (@material/material-color-utilities, shape-morph, highlight.js, @lezer/highlight), emoji data generated from Unicode's emoji files and CLDR annotations (Unicode License v3), and a TypeScript port of the Compose Material 3 carousel engine (Android Open Source Project, Apache-2.0). Their licenses ship in dist/THIRD_PARTY_LICENSES.md, generated on every build. Optional peer dependencies (TipTap, CodeMirror, xterm.js, Chart.js, markdown-it…) aren't bundled: your app installs them with their own licenses.
Related Packages
Version History
| Version | Released | License |
|---|---|---|
| 1.1.4 | MIT | |
| 1.1.3 | MIT | |
| 1.1.2 | MIT | |
| 1.1.1 | MIT | |
| 1.1.0 | MIT | |
| 1.0.3 | MIT | |
| 1.0.2 | MIT | |
| 1.0.1 | MIT | |
| 1.0.0 | MIT | |
| 0.9.3 | MIT | |
| 0.9.2 | MIT | |
| 0.9.1 | MIT | |
| 0.9.0 | MIT | |
| 0.8.8 | MIT | |
| 0.8.7 | MIT | |
| 0.8.6 | MIT | |
| 0.8.5 | MIT | |
| 0.8.4 | MIT | |
| 0.8.3 | MIT | |
| 0.8.2 | MIT | |
| 0.8.1 | MIT | |
| 0.8.0 | MIT | |
| 0.7.1 | MIT | |
| 0.7.0 | MIT | |
| 0.6.4 | MIT | |
| 0.6.3 | MIT | |
| 0.6.2 | MIT | |
| 0.6.1 | MIT | |
| 0.6.0 | MIT | |
| 0.5.10 | MIT |
Showing the latest 30 of 70. See every release on npm