Maz-UI v5.0.0 Migration Guide
Maz-UI v5 is a major release. For most consumers the migration is short — you keep installing the package, importing its styles, using the components. Below is what actually changes for you.
Solo maintainer notice
Maz-UI is maintained by a single developer. After the v5 stable release, v4 will no longer receive any support — no security fixes, no backports. Please plan your upgrade.
Run the upgrade tool
A first pass that does the mechanical work for you:
# Preview
npx @maz-ui/upgrade ./ --dry-run
# Apply
npx @maz-ui/upgrade ./It handles the mechanical part of the migration (CSS subpath imports, prop renames, CSS var renames, the MazAvatar size scale fix, Nuxt config keys, custom preset color keys, and package.json version bumps)
What it can't decide for you: the bound values of the MazIcon API simplification (section 4), the px / em MazBadge sizes (section 9, the rem sizes are rewritten), foundation.radius → scales.rounded.md reshape (section 11) and MazChart update-mode defaults (section 8). Those are best handled with the MCP server below.
See the full list of transforms in the @maz-ui/upgrade README.
Migrate with the Maz-UI MCP server
You don't have to scroll through this page by hand. The official @maz-ui/mcp server exposes this guide (and every other Maz-UI doc — components, composables, plugins, helpers) to your AI assistant via the Model Context Protocol. Once it's wired up, the assistant can search the migration guide, pull the exact section it needs, and patch your codebase with full context on what changed in v5.
One-time setup
# Claude Code
claude mcp add maz-ui npx @maz-ui/mcp --scope projectFor Cursor, Windsurf, VS Code Copilot, Cline and Claude Desktop, see the MCP guide. Then restart your assistant.
A migration flow that works
Pin the guide. Open the assistant in your project root and prime it:
Use the
maz-uiMCP server. Fetch the v5 migration guide (get_docon guidemigration-v5) and read it in full before touching any code.Let it scan your codebase against the checklist. Once it has the guide loaded:
Walk the migration checklist top to bottom. For each numbered item, search my codebase, list every match, and tell me which ones need a change. Do not modify anything yet.
Apply the easy ones in batch. The mechanical renames are safe to automate:
Apply the codemod for "Required changes" sections 2, 3, 6 and 14 (CSS subpath,
rounded-size="base",left-icon/right-icon, NuxtinjectMainCss). Show me a diff before saving.Discuss the judgment calls. For sections that have more than a rename —
MazIconAPI,MazChartupdate-mode, theme preset reshape — let the assistant ask before rewriting:For "Required changes" sections 4, 8 and 10–12, do not patch automatically. Tell me which of my call sites are affected and propose options.
Verify. After patches:
Run my typecheck and unit tests, then re-read sections you applied and confirm I haven't missed anything.
Useful prompts
- "Search the Maz-UI docs for
useToastand tell me if my call sites still match the v5 signature." - "My custom theme preset still defines
colors.light.background. Look up the v5 preset shape from the MCP and rewrite my preset." - "I see
hsl(var(--maz-primary))in my CSS. Apply the v5 fix everywhere with the alpha cases handled viacolor-mix." - "Compare my
MazIconusages against the v5 API and split the codemod into a deterministic part (URL/Vue component) and a part you want me to confirm."
The MCP server is read-only — it ships docs, not code edits — so the assistant still has to write the patches against your repo with whatever tooling it has. But it removes the "did I miss a section?" risk: the guide and the rest of the v5 docs are always one tool call away.
Migration checklist
- Bump Node to ≥ 22.12 and confirm your audience has modern browsers (see below).
- If your custom CSS uses
hsl(var(--maz-<color>)), simplify tovar(--maz-<color>). - Rename
import 'maz-ui/styles'toimport 'maz-ui/style.css'(andmaz-ui/aos-styles→maz-ui/aos.css). - Replace any
rounded-size="base"withrounded-size="md"(or drop the prop — the new default is visually identical). - Replace any numeric
MazBadgesize (size="0.8rem",size="1.2em", …) with one of the standardized keywords ('mini' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'). MazIconAPI simplified — dropname,pathandsrcprops; use a singleiconprop that accepts a Vue component, a URL/data:URI, or a raw SVG string.- Rename
left-icon/right-icontostart-icon/end-icon(and the matching slots /--has-*-iconclasses) onMazBtn,MazInput,MazLink,MazContainer,MazSelect. Same idea forMazCard'sfooter-alignandMazDrawer'svariant—'left' | 'right'becomes'start' | 'end'. MazChartdropsvue-chartjs(lighter bundle, no eager registration of unused chart types). Theupdate-modeprop now defaults to'none'— passupdate-mode="default"if you want animated data updates.MazAvatarsize scale fixed. A CSS-unitsizenow renders at its real value (size="2rem"is a 32px avatar, it was ~96px before). Multiply your unit values by 3 to keep the same render, or switch to aMazSizekeyword ('mini' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'). The upgrade tool does the ×3 rewrite for you on static values.- Rename
MazPullToRefresh'son-clickprop toon-refresh(handled by the upgrade tool). - Replace bare
from 'maz-ui'utility imports withfrom '@maz-ui/utils'(the root re-export of utils was removed; handled by the upgrade tool). Components/composables/directives/plugins stay on their subpaths; types remain importable frommaz-ui. - If you use
useFormValidator/useFormFieldwith Valibot, addvalibotto your own dependencies (it is no longer installed bymaz-ui). The composables now accept any Standard Schema library (Valibot, Zod, ArkType...). - That's it for most apps. Everything else is opt-in.
Prerequisites
Node
node -v # must be >= 22.12Browser support
v5 requires native support for a few modern CSS features (color-mix(), @property, native CSS nesting). Minimum versions:
| Browser | Minimum |
|---|---|
| Chromium (Chrome, Edge, Opera, Brave, Arc) | 111 (March 2023) |
| Safari | 16.4 (March 2023) |
| Firefox | 128 (July 2024) |
| Samsung Internet | 22 |
There is no polyfill path. If your audience still relies on older browsers, stay on v4. See the dedicated Browser Support page for the rationale.
Required changes
1. hsl(var(--maz-<color>)) → var(--maz-<color>) in your CSS
The biggest — and usually only — change to chase down in your codebase.
In v4 the CSS variables injected by @maz-ui/themes held raw HSL channels (210 100% 56%), so you had to wrap them yourself:
.brand {
background-color: hsl(var(--maz-primary));
border-color: hsl(var(--maz-primary) / 0.5);
}In v5 the same variables hold complete CSS colors (hsl(210 100% 56%)). Wrapping in hsl(...) again produces hsl(hsl(...)), which is invalid. Update your own CSS:
.brand {
background-color: var(--maz-primary);
border-color: color-mix(in srgb, var(--maz-primary) 50%, transparent);
}For the alpha case, use color-mix(). The old / <alpha> syntax no longer applies to already-wrapped values.
Quick search-and-fix:
# Find the patterns to update
rg "hsl\(\s*var\(--maz-" src/2. CSS subpath renames
The two CSS entry points have been renamed to include their real file extension, so TypeScript treats them as plain side-effect CSS imports (no more .d.ts stubs required):
| v4 | v5 |
|---|---|
import 'maz-ui/styles' | import 'maz-ui/style.css' |
import 'maz-ui/aos-styles' | import 'maz-ui/aos.css' |
Both resolve to the same compiled CSS they did in v4, only the subpath key changes.
# Find the patterns to update
rg "['\"]maz-ui/(styles|aos-styles)['\"]" src/3. rounded-size="base" removed
The 'base' value of the rounded-size prop has been removed across the affected components. The default radius is now 'md' (which maps to the same visual radius v4 used for 'base').
| Component | Old default | New default |
|---|---|---|
MazAlert | 'base' | 'md' |
MazAvatar | 'base' | 'md' |
MazBtn | 'base' | 'md' |
MazContainer | 'base' | 'md' |
MazInput | 'base' | 'md' |
MazSkeleton | 'base' | 'md' |
MazTable | 'lg' | 'md' |
MazTextarea | (none) | 'md' |
MazTimeline | 'base' | 'md' |
Search for explicit usages and replace them:
rg "rounded-?size\s*=\s*['\"]base['\"]" src/<!-- v4 -->
<MazBtn rounded-size="base" />
<!-- v5 -->
<MazBtn rounded-size="md" />
<!-- or just drop the prop, the new default is identical visually -->
<MazBtn />4. MazIcon API simplified
MazIcon v4 had four ways to specify an icon (icon, src, path+name, name). v5 collapses everything into a single icon prop that accepts:
- A Vue component (lazy or static, from
@maz-ui/icons) - A URL or
data:URI to a.svgfile - A raw SVG string (
'<svg>…</svg>')
<!-- v4 -->
<MazIcon name="home" />
<MazIcon name="home" path="/icons" />
<MazIcon src="/assets/star.svg" />
<MazIcon :icon="MazStar" />
<!-- v5 -->
<MazIcon icon="/home.svg" /> <!-- relative URL — `mazIconPath` is prepended in SSR -->
<MazIcon icon="/icons/home.svg" /> <!-- absolute path -->
<MazIcon icon="/assets/star.svg" />
<MazIcon :icon="MazStar" /> <!-- unchanged -->The mazIconPath provide (and Nuxt's mazUi.general.defaultMazIconPath) is still honored — it now acts as a base URL prefix for relative URL icons, mainly useful in SSR where fetch('/icons/home.svg') cannot resolve without a host.
New props:
fallback— same shape asicon. Used wheniconis missing or fails to load. Defaults toMazQuestionMarkCircle.svg-attributes— extra attributes injected onto the rendered<svg>.flip-icon-for-rtl— mirror the icon horizontally when the document direction is RTL (useful for chevrons/arrows).
Removed:
loadedandunloadedevents (onlyerrorremains, useful for telemetry/fallback wiring).
The <MazIcon> rendering also got a small a11y polish — it sets aria-hidden="true" by default and switches to role="img" (without aria-hidden) automatically when an aria-label is provided.
5. New raw icon import path — @maz-ui/icons/raw/*
Each bundled icon is now also exported as a raw SVG string under the raw/ subpath. Pass it to <MazIcon :icon="…" /> to inline the SVG without paying the cost of a Vue component or an async chunk.
<script setup>
import MazIcon from 'maz-ui/components/MazIcon'
import { MazStar } from '@maz-ui/icons/raw/MazStar'
</script>
<template>
<MazIcon :icon="MazStar" />
</template>The existing static/ (eager Vue component) and lazy/ (async Vue component) entries are still available — pick what fits the situation. See the icons guide for the full decision matrix.
6. Logical direction props — left-icon/right-icon → start-icon/end-icon
To make components RTL-correct out of the box, every prop / slot / class that names a visual edge has been renamed to a logical edge:
left/rightdescribe a physical position; they don't flip whendir="rtl".start/endfollow the inline direction —startis the side where reading begins (left in LTR, right in RTL).
This is consistent with native CSS (margin-inline-start, padding-inline-end, text-align: start) and with how design systems like Radix UI and MUI handle direction.
Components
| Component | v4 | v5 |
|---|---|---|
MazBtn | left-icon / right-icon props, #left-icon / #right-icon slots | start-icon / end-icon props, #start-icon / #end-icon slots |
MazInput | left-icon / right-icon props, #left-icon / #right-icon slots | start-icon / end-icon props, #start-icon / #end-icon slots |
MazLink | left-icon / right-icon props, #left-icon / #right-icon slots | start-icon / end-icon props, #start-icon / #end-icon slots |
MazContainer | left-icon / right-icon props, #icon-left / #icon-right slots | start-icon / end-icon props, #icon-start / #icon-end slots |
MazSelect | #left-icon / #right-icon slots | #start-icon / #end-icon slots |
MazCard | footer-align: 'left' | 'right' | footer-align: 'start' | 'end' |
MazDrawer | variant: 'left' | 'right' | 'top' | 'bottom' | variant: 'start' | 'end' | 'top' | 'bottom' (top / bottom unchanged — physical) |
top and bottom are intentionally kept (block-axis edges that don't flip in RTL).
Internal CSS hooks
If you target maz-ui's internal selectors from your own CSS, also rename:
| v4 | v5 |
|---|---|
.--has-left-icon / .--has-right-icon | .--has-start-icon / .--has-end-icon (on MazBtn, MazInput) |
.m-input-wrapper-left / .m-input-wrapper-right | .m-input-wrapper-start / .m-input-wrapper-end |
drawer-anim-left / drawer-anim-right transitions | drawer-anim-start / drawer-anim-end (auto-mirrored under [dir="rtl"]) |
Migration
<!-- v4 -->
<MazBtn left-icon="user" right-icon="arrow-right">Save</MazBtn>
<MazInput :left-icon="MazSearch" />
<MazContainer title="Settings" left-icon="cog-6-tooth" />
<MazCard footer-align="right" />
<MazDrawer variant="right" />
<!-- v5 -->
<MazBtn start-icon="/user.svg" end-icon="/arrow-right.svg">Save</MazBtn>
<MazInput :start-icon="MazSearch" />
<MazContainer title="Settings" start-icon="/cog-6-tooth.svg" />
<MazCard footer-align="end" />
<MazDrawer variant="end" />Icon names
In v4, a string icon prop (left-icon="user") was an icon name, resolved to ${mazIconPath}/user.svg. In v5, a string is a URL or a raw SVG: a bare name renders nothing. Use the URL of the same file (start-icon="/user.svg", mazIconPath is still prepended to relative URLs) or an icon component. The @maz-ui/upgrade codemod rewrites the static values for you.
# Quick search to chase down the renames
rg "(left|right)-icon|(left|right)Icon|footer-align=\"(left|right)\"|variant=\"(left|right)\"" src/7. Icon-consuming components accept the full MazIconProps object
MazBtn, MazInput, MazLink, MazContainer and MazDropdown all forward their start-icon / end-icon / icon / dropdown-icon to an internal <MazIcon>. In v5 these props now accept either:
- A bare value — Vue component, URL/
data:URI, or raw SVG string (the same shapeMazIcon'siconprop accepts). This is the existing common case. - A full
MazIconPropsobject —{ icon, size, title, svgAttributes, fallback, flipIconForRtl }. Useful when you need to override the inherited size, set a<title>for screen readers, or pass per-icon SVG attributes.
<!-- v4 only accepted a bare value -->
<MazBtn :left-icon="MazStar" />
<!-- v5 — bare value still works -->
<MazBtn :start-icon="MazStar" />
<MazBtn start-icon="/icons/star.svg" />
<!-- v5 — full props object for fine-grained control -->
<MazBtn
size="sm"
:start-icon="{
icon: MazStar,
size: 'lg', // override the size derived from `size='sm'`
title: 'Favorite', // adds <title> for screen readers
flipIconForRtl: true,
}"
/>Falling back to passing undefined (or just omitting the prop) opts the icon out, as before.
8. MazChart no longer depends on vue-chartjs
MazChart v4 wrapped vue-chartjs, eagerly registered every Chart.js controller / element / scale at import time, and updated the chart with the default animation mode on every reactive change. v5 talks to chart.js directly, lazy-loads the engine on mount, and only registers the modules required by the chart type you requested.
What this means for you:
- The peer dependency on
vue-chartjsis gone. If you were importing anything from it directly, switch tochart.js. - The
update-modeprop now defaults to'none'(previously'default'). Subsequent data / options changes no longer animate by default — this is what makes large dashboards stop freezing on prop updates. The initial render still animates per the chart'soptions. Passupdate-mode="default"to restore the v4 behavior. - The re-exported types (
MazChartData,MazChartType,MazChartUpdateMode,MazChartPlugin,MazChartDefaultDataPoint) are unchanged — they now come fromchart.jsinstead ofvue-chartjsbut are otherwise the same. TheChartPropslink in the v4 docs (which pointed tovue-chartjs/src/types.ts) is replaced by theMazChartPropsinterface documented on the MazChart page.
<!-- v4 — animated update on every data change -->
<MazChart :type :data :options />
<!-- v5 — same call, but data updates skip animations by default -->
<MazChart :type :data :options />
<!-- v5 — restore v4 animated updates -->
<MazChart :type :data :options update-mode="default" />If you weren't using vue-chartjs directly and were happy with the default animation behavior on every update, you can ignore this — only add update-mode="default" where the animation matters.
9. MazBadge size prop now uses MazSize
MazBadge's size prop used to accept any CSS length string (e.g. "0.8rem", "1.2em"). It now follows the same MazSize contract as the rest of the library (MazBtn, MazInput, …): one of 'mini' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'. The default is 'md'.
Padding, line-height and dimensions still scale relative to the chosen font-size via em units, so a single keyword controls the whole footprint.
<!-- v4 -->
<MazBadge size="0.8rem" />
<MazBadge size="1.2em" />
<!-- v5 -->
<MazBadge size="sm" />
<MazBadge size="md" />Approximate mapping if you want to preserve your previous footprint:
| v4 (font-size) | v5 |
|---|---|
| ≤ 0.65rem (~10px) | 'mini' |
| 0.7rem (~11px) | 'xs' |
| 0.75–0.8rem (12px) | 'sm' |
| 0.85–0.9rem (~14px) | 'md' (default) |
| 1rem (16px) | 'lg' |
| ≥ 1.125rem (18px) | 'xl' |
The @maz-ui/upgrade codemod rewrites the static rem sizes to the closest keyword. px and em sizes depend on the root or parent font size: they are reported at the end of the run so you pick the keyword.
# Find badges still using a numeric size
rg "MazBadge[^/]*size=\"[0-9]" src/If you really need a custom size, wrap or override via your own class — pass size="md" (or omit it) and apply a Tailwind text utility:
<MazBadge size="md" class="maz:text-[0.7rem]" />10. Theme preset rename — background → surface, border → divider
Preset keys now use the same names that components and Tailwind utilities have used all along. There is no compatibility alias — rename the keys in your custom preset.
// v4
{
colors: {
light: { background: '0 0% 100%', border: '220 13% 91%', /* ... */ },
dark: { background: '235 16% 15%', border: '238 17% 25%', /* ... */ },
},
}
// v5
{
colors: {
light: { surface: '0 0% 100%', divider: '220 13% 91%', /* ... */ },
dark: { surface: '235 16% 15%', divider: '238 17% 25%', /* ... */ },
},
}The runtime CSS variables follow:
| v4 | v5 |
|---|---|
--maz-background[-N] | --maz-surface[-N] |
--maz-border[-N] | --maz-divider[-N] |
Tailwind utilities maz:bg-surface, maz:text-surface-700, maz:border-divider are stable — they pointed at these renamed vars even in v4 and continue to work.
# Find leftovers
rg "(--maz-background|--maz-border\b|colors\.(light|dark)\.(background|border))" src/11. Theme preset: foundation.radius → scales.rounded.md
The single foundation radius is replaced by a full radius scale. Move your value to scales.rounded.md and pick the rest of the scale (or spread the bundled defaults).
// v4
{
foundation: {
'base-font-size': '14px',
'border-width': '1px',
'radius': '0.7rem',
},
}
// v5
{
foundation: {
'base-font-size': '14px',
'border-width': '1px',
'space': '0.25rem',
},
scales: {
rounded: {
'xs': '0.125rem',
'sm': '0.25rem',
'md': '0.7rem',
'lg': '1rem',
'xl': '1.5rem',
'2xl': '2rem',
'3xl': '3rem',
},
shadow: { /* ... */ },
},
}The bridge maps every --maz-rounded-{key} to its matching Tailwind --radius-{key}, so maz:rounded-md, maz:rounded-lg, etc. all move with your preset.
12. New optional preset blocks: scales and components
@maz-ui/themes now lets you drive the rounded / shadow scales (and space via foundation) plus a small set of per-component tokens:
{
foundation: {
space: '0.25rem',
},
scales: {
rounded: { /* xs..3xl */ },
shadow: { sm, md, lg, xl, elevation },
},
components: {
btn: { 'font-weight': '500' },
container: { bg: { light: 'var(--maz-surface)', dark: 'var(--maz-surface)' } },
input: { bg: { light: 'var(--maz-surface)', dark: 'var(--maz-surface-400)' } },
},
}Both blocks are optional; the bundled presets ship with sensible defaults. New foundation keys also landed and are all optional: font-mono, font-display, disabled-opacity, disabled-cursor. See the themes guide for the full surface.
13. Theme strategy: 'hybrid' removed, no more critical CSS
The @maz-ui/themes rendering pipeline collapsed back to a single path: the full CSS is generated and injected synchronously on first paint. The two-stage critical-CSS / async-full-CSS dance is gone.
What changed:
Strategyis now'runtime' | 'buildtime'—'hybrid'no longer exists. The runtime default is'runtime'.MazUiThemeplugin options no longer acceptinjectCriticalCSS/injectFullCSS.@maz-ui/nuxt'sthemeconfig no longer acceptsinjectAllCSSOnServer.generateCSS/buildThemeCSS/buildSeparateThemeFilesno longer acceptonlyCritical,criticalColors,criticalFoundation, orincludeColorScales.buildSeparateThemeFilesnow returns{ full, lightOnly, darkOnly }only — no morecritical.
What you need to do:
app.use(MazUiTheme, {
preset: mazUi,
- strategy: 'hybrid',
+ strategy: 'runtime',
- injectCriticalCSS: true,
- injectFullCSS: true,
}) export default defineNuxtConfig({
mazUi: {
theme: {
- strategy: 'hybrid',
+ strategy: 'runtime',
- injectAllCSSOnServer: true,
},
},
})To keep first-paint clean, pass the preset object (not just a string name) so the full CSS is rendered before the first frame.
14. Nuxt module: mazUi.css.injectMainCss → mazUi.css.injectCss
The Nuxt module config key that toggles the global maz-ui stylesheet has been renamed. The old name is silently ignored — your app would build but ship without the maz-ui CSS — so update it.
export default defineNuxtConfig({
mazUi: {
css: {
- injectMainCss: true,
+ injectCss: true,
},
},
})15. color="background" → color="surface" on components
The components that exposed color="background" in their public prop now use color="surface". The visual result is identical — only the literal changes.
| Component | v4 | v5 |
|---|---|---|
MazBtn | color="background" | color="surface" |
MazBadge | color="background" | color="surface" |
MazLink | color="background" | color="surface" |
MazPopover | color="background" (default) | color="surface" (default) |
MazPagination | active-color="background" (default) | active-color="surface" (default) |
MazRadioButtons | color="background" | color="surface" |
# Find prop usages to update
rg "color\s*=\s*['\"]background['\"]" src/16. MazAvatar size scale fixed + MazSize keywords
MazAvatar's size prop now accepts the standard MazSize keywords on top of CSS units, and a long-standing scale bug is fixed.
The scale bug: the avatar wrapper is sized at 3em of a root font-size: size, so a CSS-unit size rendered the avatar 3x larger than the value you passed. size="2rem" produced a ~96px avatar instead of 32px. In v5 the value is honored 1:1 - size="2rem" is a 32px avatar.
New MazSize keywords (aligned with MazBtn, MazInput, ...):
| Keyword | Rendered size |
|---|---|
mini | 24px |
xs | 32px |
sm | 40px |
md | 48px (matches the old default) |
lg | 56px |
xl | 64px |
<!-- v5 - keyword sizes -->
<MazAvatar size="sm" />
<MazAvatar size="lg" />Custom CSS units still work - you just need to triple the value to keep the exact rendering you had in v4:
<!-- v4 - rendered ~96px -->
<MazAvatar size="2rem" />
<!-- v5 - same ~96px render -->
<MazAvatar size="6rem" />
<!-- or adopt the corrected scale / a keyword -->
<MazAvatar size="2rem" />
<MazAvatar size="xl" />The upgrade tool rewrites static size="<unit>" values on <MazAvatar> automatically (every CSS unit is supported). Bound expressions (:size="…") can't be rewritten safely - they are left untouched and listed at the end of the run so you can check them by hand.
# Find avatars still passing a size you may want to review
rg "<MazAvatar[^>]*\bsize=" src/17. MazPullToRefresh — on-click → on-refresh
The callback prop of MazPullToRefresh was renamed from on-click to on-refresh to reflect what it actually does: it runs when the refresh is triggered on release, not on a click.
<!-- v4 -->
<MazPullToRefresh :on-click="fetchData" />
<!-- v5 -->
<MazPullToRefresh :on-refresh="fetchData" />The upgrade tool rewrites this automatically on <MazPullToRefresh> tags (both on-click and onClick, bound or static).
# Find usages to update
rg "<MazPullToRefresh[^>]*\bon-?[Cc]lick" src/The component was also rewritten on top of the new usePullToRefresh composable. Behavior changes to check:
- The pull follows the scroll container. It starts only when the closest scrollable ancestor (or
container-selector, or the window) is at the top. In v4, only the window scroll was read, so a layout with a scrolling<main>refreshed in the middle of a list. distanceis now the indicator distance after resistance (default80, was100of raw finger movement). With the defaultresistanceof0.5, the trigger needs about 160px of finger movement. Lowerdistanceor raiseresistancefor a shorter gesture.spinner-colordefaults toprimary(wascontrast) and also colors the progress ring.offsetis deprecated and has no effect.- Texts are translated (
pullToRefresh.*in@maz-ui/translations) instead of hard coded in English. Thepull-before,pull-readyandpull-loadingslots still replace them, and now receive{ progress, distance }. - An error thrown by
on-refreshis emitted (error, thenfinish) and no longer thrown again, so it does not end up as an unhandled rejection.
18. Root maz-ui import removed for utilities
In v4, the bare maz-ui entry re-exported @maz-ui/utils, so you could write import { sleep } from 'maz-ui'. In v5 that root re-export is gone — import helpers from their package directly:
// v4
import { sleep, formatDate } from 'maz-ui'
// v5
import { sleep, formatDate } from '@maz-ui/utils'Components, composables, directives and plugins were already subpath-only in v4 (maz-ui/components/MazBtn, maz-ui/composables/useToast, …) and are unchanged. The upgrade tool rewrites the utility imports for you.
The maz-ui root now exposes types only (zero runtime cost), so shared types stay importable from the short path:
import type { MazColor, MazSize } from 'maz-ui'19. ESM-only
maz-ui and its @maz-ui/* packages ship ESM only (no CommonJS build). This was already the case in v4, but if you consume the library from a CommonJS context (old Jest without ESM, require()), use a bundler/test runner with ESM support (Vitest, Jest ≥ 29 with ESM, or native import).
20. useFormValidator: validation library agnostic (Standard Schema)
useFormValidator and useFormField no longer depend on Valibot. They accept any library implementing Standard Schema: Valibot (>= 1.0), Zod (>= 3.24), ArkType (>= 2.0), or your own validator. Your existing Valibot schemas keep working as is: same validation, same error messages, same inferred types for model and the handleSubmit payload.
// v4 and v5 - unchanged
import { pipe, string, minLength } from 'valibot'
useFormValidator({ schema: { name: pipe(string(), minLength(3)) } })
// v5 - Zod (or mix both in the same schema)
import { z } from 'zod'
useFormValidator({ schema: { name: z.string().min(3) } })What can break:
valibot is no longer a dependency of maz-ui. Valibot, Zod and ArkType are now declared as optional peer dependencies (valibot >=1.0.0 <2.0.0, zod >=3.24.0 <5.0.0, arktype >=2.0.0 <3.0.0). If your app imports valibot without declaring it (it was installed transitively), add it:
pnpm add valibotIssue types are now the Standard Schema ones. ValidationIssues (returned by errors, fieldsStates[field].errors, useFormField().errors and the onError payload of handleSubmit) is now typed as an array of { message: string, path?: ... } instead of Valibot's BaseIssue<unknown>. Only TypeScript is affected: at runtime, the issues are still the native Valibot objects. If you read Valibot specific properties (kind, type, input, expected, received, requirement...), cast the issue:
import type { BaseIssue } from 'valibot'
// v4
const type = errors.value.email[0].type
// v5
const type = (errors.value.email[0] as BaseIssue<unknown>).typeFormSchema values are typed as StandardSchemaV1. If you typed a schema object with FormSchema<Model> and then read Valibot specific properties of its entries (schema.email.type, schema.email.pipe...), declare the object with as const satisfies FormSchema<Model> or without annotation, so TypeScript keeps the Valibot types.
# Find usages to check
rg "useFormValidator|useFormField" src/
rg "from 'valibot'" src/Informational changes (probably no action needed)
Theme colors are now emitted as OKLCh
@maz-ui/themes now ships its bundled presets (mazUi, ocean, pristine, obsidian, nova) in oklch() form, and the runtime CSS variables (--maz-primary, --maz-primary-100, …) therefore hold OKLCh colors. The 50–950 scale is derived in CSS via OKLCh relative color syntax — oklch(from var(--maz-X) clamp(0, calc(l ± offset), 1) calc(c * mult) h) — perceptually uniform, chroma-preserving, and live-reactive to base color overrides.
This is transparent for consumers:
- Components keep working unchanged.
- Custom CSS using
var(--maz-<color>)keeps working —oklch()is a regular CSS color. color-mix(in srgb, var(--maz-primary) 50%, transparent)still works;color-mix(in oklab, …)will give you slightly better blending if you care.- Custom presets written in
hsl(),rgb(),#hexor the legacy'210 100% 56%'form still parse — they get converted to OKLCh on the fly.
You only need to do something if you were:
- Reading the channel format of a color from
--maz-*in JS — the value used to be anhsl(...)string, it's now anoklch(...)string (and at runtime the base may resolve throughlight-dark()). UsegetComputedStyle(...).getPropertyValue(...)and a CSS color parser if you need channels. - Calling the legacy JS palette helpers (
generateColorScale,adjustColorLightness,getContrastColor,parseHSL) — these exports were removed in v5. See BREAKING — removed exports for replacements.
Why the switch: OKLCh is perceptually uniform, so generated scales (primary-50 → primary-950) ramp consistently across hues — yellow-700 won't look muddy compared to blue-700 anymore. It also unlocks Display P3 colors when you author a vivid preset.
Preset type: HSL → CSSColor
If you authored a custom preset via @maz-ui/themes, the color field type is now the looser CSSColor = string instead of the template literal HSL.
- Your existing raw values (
'210 100% 56%') keep working — they are auto-wrapped inhsl(...)at runtime bynormalizeColor. - You can now use any CSS color form:
'hsl(210 100% 56%)','#22c55e','oklch(0.7 0.15 30)','rgb(22 194 211)'. - Type checking on custom colors is now permissive. If you want strict validation, keep using the raw HSL form or validate at your boundary.
No action required for existing presets.
Default border color
Tailwind v4 changed the default border color from the configured gray to currentColor. Inside maz-ui components we restore the v3 behavior so shadows, dividers and outlines keep their neutral tint; your own styles are unaffected (you get the Tailwind v4 default). If you also want the v3 default in your own code, see the official Tailwind upgrade guide.
Visual parity
Most visual regressions were caught during the migration, but keep an eye on:
- Shadows, radius and blur scales — Tailwind v4 shifted a few names (
shadow→shadow-sm,blur→blur-sm, etc.). Maz-ui components have been updated internally; your own code is affected only if you used these classes directly. outline-none→outline-hidden(same story).
nova preset added, bundled palettes refreshed
Four bundled presets remain (mazUi, pristine, ocean, obsidian) plus a new nova startup / AI / creative preset (electric violet primary, cyan accent, hot coral secondary, Geist font stack). Each existing preset's palette has been retuned so the secondary / accent voices actually pop in MazCardSpotlight, secondary MazBtn variants and badges:
| Preset | Identity |
|---|---|
pristine | Sober Apple — near-black primary, system blue accent, system purple secondary, SF system font, Apple spring easing. |
ocean | Calm water — teal primary, deep navy secondary, sandy ochre accent. |
obsidian | Dark luxe — indigo primary, gold accent, fuchsia secondary, snappy expo easing. |
nova | Modern AI — electric violet primary, cyan accent, hot coral secondary. |
Switching to one of the bundled presets does not require any code change beyond the preset name. If you depended on the previous (washed-out gray) secondary color in a custom theme, override it back via colors.{light,dark}.secondary in your preset.
Theming: native CSS rewrite (new — non-breaking by default)
The @maz-ui/themes CSS pipeline was rewritten to lean on native CSS instead of JS:
- Base colors are now emitted as a single
--maz-X: light-dark(L, D)declaration. The browser resolves it for you. - Color scales
--maz-X-50through--maz-X-950are derived via OKLCh relative color syntax at runtime, so any override on the base color cascades through the whole scale automatically (and preserves its chroma). :rootdeclarescolor-scheme: light darkso native widgets (scrollbars, native<select>, date pickers, autofill) follow the active mode out of the box.- A
<meta name="color-scheme">tag is automatically injected at boot (Vue + Nuxt SSR). No user action — prevents the Flash of inAccurate coloR Theme.
Consumer-visible CSS surface is unchanged: var(--maz-primary), --maz-primary-500, Tailwind utilities bg-primary/60, bg-primary-500, etc. all keep working.
lightClass (new — optional, default 'light')
Mirror of darkClass. When colorMode === 'light' and darkModeStrategy === 'class', this class is added to <html> to force color-scheme: only light. Default 'light' — set to false/custom string if it clashes with an existing class in your app:
app.use(MazUi, {
theme: {
preset: mazUi,
darkClass: 'dark',
lightClass: 'light', // new
},
})Non-breaking: if you never explicitly forced light, nothing changes.
setColorMode / toggleDarkMode now return Promise<void>
Both functions are now async to support the optional { animate: true } parameter (View Transitions). Existing callers that ignored the return value keep working — await is only required when you need to chain.
const { setColorMode, toggleDarkMode } = useTheme()
// Existing v4 call — still works, return value ignored
setColorMode('dark')
toggleDarkMode()
// v5 — await to chain after the change is applied
await setColorMode('dark')
// v5 — animated full-page switch via document.startViewTransition()
await toggleDarkMode({ animate: true })
await setColorMode('dark', { animate: true })The View Transitions glue is lazy-imported — when animate is not used, it stays out of your boot bundle. Browsers that don't support the API apply the change immediately with no animation.
BREAKING — removed exports
The JS-based palette generator is gone. The following helpers used to be exported from @maz-ui/themes and no longer exist in v5:
| Removed export | Replacement |
|---|---|
generateColorScale(base, mode) | The scale is now generated in CSS via oklch(from var(--maz-X) clamp(0, calc(l ± offset), 1) calc(c * mult) h). Read the live value from --maz-X-{50..950}, or replicate the formula inline. |
parseHSL(value) | Use a CSS color parser or parseColorAsOklch from @maz-ui/themes/utils/color-conversions. |
adjustColorLightness(color, amount) | Inline with oklch(from <color> calc(l + N) c h) to shift lightness while preserving chroma and hue. |
getContrastColor(color) | Use the preset's *-foreground token, which is what components consume internally. |
colorToHex, parseColorAsOklch and formatAsOklch are preserved, now exported from @maz-ui/themes/utils/color-conversions.
If you were importing any of the removed helpers from @maz-ui/themes, replace them before upgrading.
Preset name persistence (new — opt-out, no breaking change)
@maz-ui/themes now persists the active preset name in a maz-preset cookie (1-year TTL, SameSite=Lax) — same shape as the existing maz-color-mode cookie:
- The cookie is read at boot and takes priority over
options.preset— even whenoptions.presetis a preset object.options.preset(string or object) is the default the app boots with; the cookie carries the user's last explicit choice and wins. - It is written after every successful preset resolution and after every
useTheme().updateTheme()call. - If the saved name no longer resolves, the cookie is cleared and the runtime falls back to
options.preset(or default).
Persistence is enabled by default. To opt out (privacy, no end-user switching, …), set persistPreset: false:
app.use(MazUiTheme, {
preset: mazUi,
persistPreset: false,
})The same option is exposed in the Nuxt module config under mazUi.theme.persistPreset.
Troubleshooting
Colors look wrong in devtools — I see hsl(hsl(...)) somewhere. You still have hsl(var(--maz-<color>)) in your CSS. See the required change above.
A maz-ui component's border is a weird color. Expected if you were overriding --maz-divider (renamed from --maz-border in v5) with a non-color value. Confirm it's a valid CSS color.
My custom preset's colors are ignored. Check the preset's type — if you imported HSL explicitly, switch to CSSColor (or just remove the type annotation). Raw HSL strings still work at runtime.
Advanced: you had your own Tailwind setup
If you were already running Tailwind v3 in your app, shipping classes like maz-flex yourself, you need the same migration as any Tailwind v3 → v4 upgrade:
- Run the official Tailwind upgrade tool:
npx @tailwindcss/upgrade— it handles 95% of the work (utility renames,@apply !importantsyntax,tailwind.config→ CSS-first, …). - Replace any
hsl(var(--maz-<color>))in your own CSS (see above). - Read the Tailwind integration page to learn how to import maz-ui's bridge and expose your theme tokens to your own Tailwind config.
This is a niche path — most consumers never wrote maz-prefixed classes in their own code.
See also
- Tailwind CSS integration — optional, for consumers who have their own Tailwind setup
- Theming — preset authoring and runtime theme switching
- Browser Support — exact minimum versions and the features they enable