Theming
Modern and performant theme system for Maz-UI built on native CSS features (light-dark(), color-scheme, color-mix(in oklch)) for v5.
Features
- Native
light-dark()+color-scheme- Modern CSS theming with zero JS overhead for the light/dark switch - Native widget theming -
color-schememakes scrollbars, native<select>, date pickers and autofill follow the active mode automatically - Animated theme switch - Optional full-page View Transitions via
setColorMode(..., { animate: true }) - Anti-FART -
<meta name="color-scheme">injected at boot to prevent any Flash of inAccurate coloR Theme (no setup needed) - Automatic color scales - the
-50to-950steps are derived via OKLCh relative color syntax, with an opt-inperceptualmode that keeps their order and hue with any base color - Performance Strategies - Runtime injection or build-time generation according to your needs
- Strict TypeScript - Complete types for perfect DX
- Zero FOUC - Pass the preset object up front; the full CSS is injected synchronously on first paint
- Flexible Presets - Ready-to-use and customizable configurations
Theme Editor
Create your own theme with the Theme Editor.
Quick Usage
1. Plugin Configuration with MazUi plugin
To avoid FOUC (Flash of Unstyled Content), you should provide the theme preset.
// main.ts
import { MazUi } from 'maz-ui/plugins/maz-ui'
import { mazUi } from '@maz-ui/themes/presets/mazUi'
import { createApp } from 'vue'
const app = createApp(App)
app.use(MazUi, {
theme: {
preset: mazUi, // pristine | ocean | obsidian | nova | folio
strategy: 'runtime', // 'runtime' | 'buildtime'
darkModeStrategy: 'class', // 'class' | 'media' (only if mode is `both`)
mode: 'both', // 'light' | 'dark' | 'both' (supported color modes)
colorMode: 'auto', // 'auto' | 'light' | 'dark' (initial color mode, only if mode is 'both')
// Class added to <html> when dark mode is forced (default: 'dark')
darkClass: 'dark',
// Class added to <html> when light mode is forced (default: 'light')
lightClass: 'light',
persistPreset: true, // remember the active preset name across reloads
}
})Alternative: MazUiProvider
You can also initialize the theme via the MazUiProvider component for lazy-loaded pages or subtree-scoped themes.
2. Setup your CSS to support theme foundation and dark mode
html {
font-size: var(--maz-base-font-size);
font-family: var(--maz-font-family);
background-color: var(--maz-surface);
color: var(--maz-foreground);
}3. Using in your components
<script setup>
import { useTheme } from '@maz-ui/themes'
const { toggleDarkMode, isDark, updateTheme } = useTheme()
</script>
<template>
<div class="bg-surface text-foreground">
<button
class="rounded-md bg-primary text-primary-foreground"
@click="toggleDarkMode"
>
{{ isDark ? '☀️' : '🌙' }} Toggle theme
</button>
</div>
</template>Configuration
preset: The theme preset to useoverrides(optional): Override specific parts of the themestrategy(optional, default'runtime'): The rendering strategy to use —'runtime' | 'buildtime'darkModeStrategy(optional, default'class'): The dark mode strategy —'class' | 'media', only if you use modebothmode(optional, default'both'): The supported color modes —'light' | 'dark' | 'both'colorMode(optional, default'auto'): The initial color mode —'light' | 'dark' | 'auto'(only if mode is'both')darkClass(optional, default'dark'): Class added to<html>whencolorMode === 'dark'lightClass(optional, default'light'): Class added to<html>whencolorMode === 'light'— mirror ofdarkClasspersistPreset(optional, defaulttrue): Persist the active preset name in themaz-presetcookie so it is restored on reload.
Persistence of the user preferences
@maz-ui/themes stores the preferences of the user in two cookies (1-year TTL, SameSite=Lax), so the server can render them:
| Cookie | Value | Example |
|---|---|---|
maz-color-mode | <choice>:<system>, each part optional: the color mode chosen with setColorMode() and the last system preference seen by the browser | dark, auto:light, :dark |
maz-preset | <configured>:<active>: the preset declared in options.preset and the one applied with updateTheme() | folio:ocean |
- The configured preset always wins over a foreign cookie: a switch is only restored when the
configuredpart matchesoptions.preset, so a cookie written by another app on the same host (or by a previous config) is ignored. A switch to a preset that no longer exists falls back to the configured one. - The cookies are only written when their value changes.
- Reactive: in the browsers that support the Cookie Store API, a change of a cookie (another tab, the server, the devtools) is applied without a reload, and a deleted cookie restores the config. The other browsers keep the values read at boot.
Set persistPreset: false to never read nor write the maz-preset cookie:
app.use(MazUi, {
theme: {
preset: mazUi,
persistPreset: false,
},
})Useful when the active preset is fully driven by the app and no end-user switching is exposed.
Color modes and class toggling
darkClass and lightClass interact with darkModeStrategy to control how <html> is decorated.
darkModeStrategy: 'class' (default)
setColorMode('dark')addsdarkClass(default.dark) to<html>→color-scheme: only dark.setColorMode('light')addslightClass(default.light) to<html>→color-scheme: only light.setColorMode('auto')adds the class of the system preference, and follows its changes.
Forcing the class on the root ensures native widgets (scrollbars, native <select>, date pickers, autofill backgrounds, …) match the color mode of the page.
A host that toggles the classes itself (VitePress, a Tailwind toggle, a blocking script) stays in charge: at boot, a class already on <html> is kept and becomes the state of useTheme(), and a later change of the class updates the state. A host that only toggles darkClass gets lightClass added back in light mode.
darkModeStrategy: 'media'
- No class is ever added to
<html>. - The browser always follows
prefers-color-schemeviacolor-scheme: light dark. setColorMode()still updates the persisted cookie but does not force a visual override.
Server rendering (SSR and SSG)
@maz-ui/nuxt does it for you. With another server, render the theme of the user in the <head> so the first paint has it, then install the plugin as usual:
import { getRequestThemeContext, getThemeHead, renderThemeHead } from '@maz-ui/themes'
export async function renderHead(request: Request) {
const head = await getThemeHead(themeOptions, getRequestThemeContext({
'cookie': request.headers.get('cookie') ?? undefined,
'sec-ch-prefers-color-scheme': request.headers.get('sec-ch-prefers-color-scheme') ?? undefined,
}))
return {
htmlClass: head.htmlClass,
head: renderThemeHead(head),
}
}getThemeHead(options, context)resolves the preset and the color mode of the user from the cookies and theSec-CH-Prefers-Color-Schemeclient hint. It returns the class of<html>(undefined when the server cannot know it), the CSS of the preset, thecolor-schememeta and a blocking script that applies the live system preference before the first paint.renderThemeHead(head)renders these tags as HTML.getThemeHeadTags(head)returns them in the format of unhead (useHead).resolveThemeContext(options)returns the same context without the tags, in the browser or on the server.options.presetaccepts the name of a built-in preset or a preset object. A name loads the preset on demand: pass the object to the client plugin to avoid a flash before it is loaded.
Animated theme switch
For a full-page animated swap (rather than per-variable color tweens), pass { animate: true } to setColorMode or toggleDarkMode:
<script setup>
import { useTheme } from '@maz-ui/themes'
const { toggleDarkMode, setColorMode } = useTheme()
async function onToggle() {
await toggleDarkMode({ animate: true })
}
async function forceDark() {
await setColorMode('dark', { animate: true })
}
</script>- Wraps the switch in
document.startViewTransition()so the browser captures a snapshot of the page before/after and crossfades between them. - The View Transition glue is lazy-imported — when
animateis not used, it stays out of the boot bundle. - Graceful degradation: in browsers that don't support the API, the change is applied immediately with no animation (no error thrown).
Interactive Demo
Real-time theme control
<script setup>
import { useTheme } from '@maz-ui/themes'
const { presetName, updateTheme, colorMode, setColorMode } = useTheme()
</script>
<template>
<div class="demo-theme-controls">
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<MazBtn color="primary">Primary Button</MazBtn>
<MazBtn color="secondary">Secondary Button</MazBtn>
<MazBtn color="success">Success Button</MazBtn>
<MazBtn color="warning">Warning Button</MazBtn>
</div>
<div class="theme-controls space-y-4">
<div class="flex items-center gap-4">
<label class="text-sm font-medium">Mode:</label>
<MazBtn
size="sm"
:color="colorMode === 'light' ? 'primary' : 'secondary'"
@click="setColorMode('light')"
>
☀️ Light
</MazBtn>
<MazBtn
size="sm"
:color="colorMode === 'dark' ? 'primary' : 'secondary'"
@click="setColorMode('dark')"
>
🌙 Dark
</MazBtn>
<MazBtn
size="sm"
:color="colorMode === 'auto' ? 'primary' : 'secondary'"
@click="setColorMode('auto')"
>
🔄 Auto
</MazBtn>
</div>
<div class="space-y-2">
<label class="text-sm font-medium">Preset:</label>
<div class="flex gap-2">
<MazBtn
size="sm"
:color="presetName === 'maz-ui' ? 'primary' : 'secondary'"
@click="updateTheme('mazUi')"
>
Maz-UI
</MazBtn>
<MazBtn
size="sm"
:color="presetName === 'ocean' ? 'primary' : 'secondary'"
@click="updateTheme('ocean')"
>
Ocean
</MazBtn>
<MazBtn
size="sm"
:color="presetName === 'pristine' ? 'primary' : 'secondary'"
@click="updateTheme('pristine')"
>
Pristine
</MazBtn>
<MazBtn
size="sm"
:color="presetName === 'obsidian' ? 'primary' : 'secondary'"
@click="updateTheme('obsidian')"
>
Obsidian
</MazBtn>
</div>
</div>
</div>
</div>
</div>
</template>Available Presets
Maz-UI (Default)
The default theme inspired by Maz-UI identity with vibrant and modern colors.
import { mazUi } from '@maz-ui/themes/presets'Pristine
A minimalist and elegant theme inspired by Apple design with pure black and white aesthetics.
import { pristine } from '@maz-ui/themes/presets'Ocean
A vibrant ocean-inspired theme with deep blues, aquatic greens, and coral accents. Features rounded borders and Poppins font for a fresh, modern look.
import { ocean } from '@maz-ui/themes/presets'Obsidian
A dark and elegant theme with a focus on readability and minimalism.
import { obsidian } from '@maz-ui/themes/presets'Nova
A modern startup / AI / creative preset — electric violet primary, hot coral secondary, cyan accent. Tight 0.5rem md radius, Geist + Inter font stack, snappy ease-out spring. Built for product / AI surfaces (think Linear, Vercel, OpenAI energy) where the UI itself should feel alive.
import { nova } from '@maz-ui/themes/presets'Folio
An electric and modern preset made for long reading, and the one of this documentation: an electric blue primary, an electric magenta secondary and a cyan accent that blend into a plasma gradient, on faintly violet surfaces (almost white in light mode, violet-black in dark mode). Fraunces (serif) for the headings, Inter for the text, Geist Mono for the code, soft 0.625rem md radius. In dark mode the color roles turn into bright tones carrying dark text, so they stay readable as text on the surface too. Every text pair reaches WCAG AA.
import { folio } from '@maz-ui/themes/presets'Fonts
Like the other presets, Folio only declares font stacks: load Fraunces, Inter and Geist Mono yourself (self-hosted or from a font service), the stacks fall back to system fonts otherwise.
Rendering Strategies
⚡ Runtime (Recommended)
CSS is generated and injected synchronously on first paint. Pass the preset object directly (preset: mazUi) so the full stylesheet is rendered before the first frame — no FOUC.
import { mazUi } from '@maz-ui/themes/presets'
app.use(MazUi, {
theme: {
preset: mazUi,
strategy: 'runtime'
}
})🏗️ Buildtime
CSS generated at build-time and included in the bundle. You have to build your CSS files before running your app.
See Build-time for more details.
import { mazUi } from '@maz-ui/themes/presets'
app.use(MazUi, {
theme: {
preset: mazUi,
strategy: 'buildtime'
}
})Creating Custom Themes
Basic Theme
Create your own theme
Default theme
import { definePreset, MazUiTheme, type ThemePresetOverrides, ocean } from '@maz-ui/themes'
const customTheme = await definePreset({
base: 'ocean',
overrides: {
name: 'custom-purple',
foundation: {
'font-family': 'Inter, sans-serif'
},
scales: {
rounded: { md: '1rem' }
},
colors: {
light: {
primary: 'oklch(0.62 0.24 305)',
secondary: 'oklch(0.94 0.04 320)',
accent: 'oklch(0.7 0.22 290)'
},
dark: {
primary: 'oklch(0.72 0.22 305)',
secondary: 'oklch(0.28 0.06 320)',
accent: 'oklch(0.78 0.2 290)'
}
}
},
})
/**
* or with preset object
* The function is synchronous when using a preset object
*/
const customTheme = definePreset({
base: ocean,
overrides,
})
// Usage
// with plugin
app.use(MazUiTheme, {
preset: customTheme,
})
// or with composable
const { updateTheme } = useTheme()
updateTheme(customTheme)Advanced Theme with Overrides
import { definePreset } from '@maz-ui/themes'
const brandTheme = await definePreset({
base: 'maz-ui',
overrides: {
name: 'brand',
foundation: {
'border-width': '2px',
'font-family': 'Inter, sans-serif'
},
scales: {
rounded: { md: '0.75rem' }
},
colors: {
light: {
primary: 'oklch(0.6 0.18 254)',
secondary: 'oklch(0.96 0.01 254)',
surface: 'oklch(0.99 0.005 254)',
accent: 'oklch(0.7 0.22 305)'
},
dark: {
primary: 'oklch(0.7 0.18 254)',
secondary: 'oklch(0.22 0.03 254)',
surface: 'oklch(0.18 0.02 254)',
accent: 'oklch(0.78 0.2 305)'
}
}
}
})Customizing scales
Each ThemeScales entry maps to a Tailwind utility — override the keys you care about and the rest stays untouched:
const denseTheme = definePreset({
base: 'maz-ui',
overrides: {
name: 'dense',
foundation: {
// Tighter spacing — every p-N / gap-N / m-N rescales
space: '0.2rem',
},
scales: {
// Sharper corners on the whole radius scale
rounded: {
'xs': '0.0625rem',
'sm': '0.125rem',
'md': '0.375rem',
'lg': '0.5rem',
'xl': '0.75rem',
'2xl': '1rem',
'3xl': '1.5rem',
},
// Custom elevated-surface shadow
shadow: {
elevation: '0 8px 24px rgba(0, 0, 0, 0.15)',
},
},
},
})Foundation gets new optional keys for fonts and disabled-state behaviour:
const codeTheme = definePreset({
base: 'maz-ui',
overrides: {
foundation: {
'font-mono-stack': '"JetBrains Mono", ui-monospace, SFMono-Regular, monospace',
'font-display-stack': '"Cal Sans", Manrope, sans-serif',
'disabled-opacity': '0.4',
'disabled-cursor': 'wait',
},
},
})Color scales
The steps -50 to -950 of the brand colors (primary, secondary, accent, info, success, warning, destructive) are derived from the base color, which is the 500 step. scales.color picks the derivation:
relative(default): a fixed lightness offset per step. With a very dark or very light base, the scale collapses: half of a navy scale is black, half of a gold scale is white.perceptual: each step travels a share of the way between the base and a light or a dark bound, with a chroma capped near white and black. The steps keep their order and their hue whatever the base, and the classes do not change (bg-accent-300,text-primary-700...).
const brandTheme = definePreset({
base: 'maz-ui',
overrides: {
colors: {
light: { secondary: '#0E1450', accent: '#FFC93C' },
},
scales: {
color: 'perceptual',
},
},
})Navy #0E1450 - relative
Navy #0E1450 - perceptual
Gold #FFC93C - relative
Gold #FFC93C - perceptual
- The scales stay dynamic: they follow
updateTheme()and the light and dark values of the base. A step is relative to the base of each mode:accent-300gives two colors in light and dark when the two bases differ. surface,foreground,contrastanddividerkeep the relative scale: the components use it as "one notch darker or lighter" (bg-surface-600).- The steps are redefined for the maz-ui components (
--maz-color-<role>-<step>) and for the Tailwind classes of your project (--color-<role>-<step>). They take precedence over a step you redefine in your own@theme. - The browsers clip an out-of-gamut color channel by channel, which can shift its hue: the chroma cap keeps this shift under the visible threshold for almost every color, the most saturated blues and yellows excepted.
Per-component overrides
components is a small, validated set of per-component knobs. Every entry is per-mode where appropriate so light and dark can drift independently:
const surfaceTheme = definePreset({
base: 'maz-ui',
overrides: {
name: 'cool-surfaces',
components: {
btn: {
// Bias every button text heavier
'font-weight': '600',
},
container: {
// Cards / dialogs / popovers / drawers — light = page surface, dark = a tier above
bg: { light: 'var(--maz-surface)', dark: 'var(--maz-surface-300)' },
},
input: {
// All form-control surfaces
bg: { light: 'var(--maz-surface-100)', dark: 'var(--maz-surface-400)' },
},
},
},
})Components consume these via bg-container / bg-input Tailwind utilities, so a single override propagates everywhere - no per-component class hunt.
Dialog sizing
components.dialog controls the width of MazDialog on tablet and up. The two keys are emitted as --maz-dialog-max-width / --maz-dialog-min-width and consumed by the component through var(--maz-dialog-max-width, 38rem) / var(--maz-dialog-min-width, 32rem):
const wideDialogTheme = definePreset({
base: 'maz-ui',
overrides: {
name: 'wide-dialogs',
components: {
dialog: {
'max-width': '48rem',
'min-width': '36rem',
},
},
},
})Both values are in rem, so they scale with the preset's base-font-size. Bundled presets anchored on 14px use 38rem / 32rem; ocean (16px) ships scaled rem values (33.25rem / 28rem) to keep the same rendered pixel width. The max-width / min-width props on MazDialog still override the preset per instance.
useTheme Composable API
const {
// Reactive state
preset, // ComputedRef<ThemePreset>
presetName, // ComputedRef<string>
colorMode, // Ref<'light' | 'dark' | 'auto'> — also accepts assignment (computed setter)
isDark, // ComputedRef<boolean>
strategy, // ComputedRef<'runtime' | 'buildtime'>
mode, // ComputedRef<'light' | 'dark' | 'both'>
darkModeStrategy, // ComputedRef<'class' | 'media'>
// Actions
updateTheme, // (preset: ThemePreset | ThemePresetName | ThemePresetOverrides) => Promise<void>
setColorMode, // (mode: 'light' | 'dark' | 'auto', options?: { animate?: boolean }) => Promise<void>
toggleDarkMode, // (options?: { animate?: boolean }) => Promise<void>
} = useTheme()
// Set the color mode imperatively
await setColorMode('dark')
// Or assign to `colorMode` — it's a computed with a setter
colorMode.value = 'dark'
// Animated full-page switch via View Transitions
await toggleDarkMode({ animate: true })setColorMode and toggleDarkMode are async (Promise<void>). The promise resolves once the change — and the optional View Transition — has been applied. Callers can ignore the return value when no transition is needed.
Advanced Usage Examples
<script setup>
import { useTheme } from '@maz-ui/themes'
import { computed } from 'vue'
const { presetName, isDark, updateTheme, setColorMode } = useTheme()
// Computed for interface
const themeIcon = computed(() => isDark.value ? '☀️' : '🌙')
// Function to apply a temporary custom theme
function previewColor(color: string) {
updateTheme({
colors: {
light: { primary: color },
dark: { primary: color }
}
})
}
// Auto mode with system detection
function enableAutoMode() {
setColorMode('auto')
}
</script>Build-time Strategy: Complete Guide
The build-time strategy allows you to generate your theme CSS files at compile time, offering the best performance in production.
Step-by-step configuration
1. Generate CSS
import { mkdirSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import {
// Build all css in one file
buildThemeCSS,
// Build multiple themes with one file each
generateThemeBundle,
// Build separate css files
buildSeparateThemeFiles,
definePreset,
mazUi,
} from '@maz-ui/themes'
import App from './App.vue'
const _dirname = dirname(fileURLToPath(import.meta.url))
// Custom theme
const customPreset = definePreset({
base: mazUi,
overrides: {
name: 'custom',
colors: {
light: { primary: 'oklch(0.6 0.18 254)' },
dark: { primary: 'oklch(0.7 0.18 254)' },
},
},
})
/**
* Generate complete CSS
*/
const fullCSS = buildThemeCSS({
preset: customPreset,
mode: 'both',
})
writeFileSync(join(_dirname, 'public/custom.css'), fullCSS)
/**
* Or generate per-mode CSS files
*/
const { full, lightOnly, darkOnly } = buildSeparateThemeFiles(customPreset, {
darkSelector: 'class',
})
writeFileSync(join(_dirname, 'public/custom-full.css'), full)
/**
* Or generate bundle for multiple themes in one file each
*/
const themeBundle = generateThemeBundle([customPreset, mazUi], {
mode: 'both',
})
Object.entries(themeBundle).forEach(([name, css]) => {
writeFileSync(join(_dirname, `public/${name}.css`), css)
})
const app = createApp(App)
app.use(MazUiTheme, {
preset: customPreset,
strategy: 'buildtime', // Important!
darkModeStrategy: 'class',
})
app.mount('#app')import {
buildThemeCSS,
generateThemeBundle,
buildSeparateThemeFiles,
createThemeStylesheet,
definePreset,
mazUi,
CSS_ID,
} from '@maz-ui/themes'
// Custom theme
const customPreset = definePreset({
base: mazUi,
overrides: {
name: 'custom',
colors: {
light: { primary: 'oklch(0.6 0.18 254)' },
dark: { primary: 'oklch(0.7 0.18 254)' },
},
},
})
// Generate complete CSS
const fullCSS = buildThemeCSS({
preset: customPreset,
mode: 'both',
})
// Or generate per-mode CSS files
const { full, lightOnly, darkOnly } = buildSeparateThemeFiles(customPreset, {
darkSelector: 'class',
})
// Or generate bundle for multiple themes
const themeBundle = generateThemeBundle([customPreset, mazUi], {
mode: 'both',
})
export default defineConfig<DefaultTheme.Config>({
head: [
['style', { id: CSS_ID, type: 'text/css' }, full],
] satisfies HeadConfig[],
})/*
* You dont need to do anything,
* The module will handle it for you.
*
* Just provide a preset to the module
*/
const customPreset = definePreset({
base: mazUi,
overrides: {
name: 'custom',
colors: {
light: { primary: 'oklch(0.6 0.18 254)' },
dark: { primary: 'oklch(0.7 0.18 254)' },
},
},
})
export default defineConfig({
modules: ['@maz-ui/nuxt'],
mazUi: {
theme: {
preset: customPreset,
mode: 'both',
colorMode: 'auto',
},
},
})2. Include in your HTML (Only for Vue users)
<link rel="stylesheet" href="/themes/custom-full.css">Utility functions
buildThemeCSS(options)
Generates complete CSS for a theme.
const css = buildThemeCSS({
preset: customPreset,
mode: 'both', // 'light' | 'dark' | 'both'
darkSelector: 'class', // 'class' | 'media'
})generateThemeBundle(presets, options)
Generates a bundle containing multiple themes.
const bundle = generateThemeBundle([theme1, theme2], {
mode: 'both',
darkSelector: 'class',
})
// Result: { 'theme1': 'css...', 'theme2': 'css...' }buildSeparateThemeFiles(preset, options)
Generates separate files for different use cases.
const files = buildSeparateThemeFiles(preset)
// Result: { full, lightOnly, darkOnly }createThemeStylesheet(css, options)
Creates a <style> @reference "../../.vitepress/theme/main.css"; tag with the provided CSS.
const styleTag = createThemeStylesheet(css, {
id: 'my-theme',
media: 'screen', // optional
})Build-time strategy advantages
- Optimal performance - No client-side CSS generation
- Efficient caching - CSS files cached by CDN
- Reduced bundle - Generation utilities excluded from client
- Full compatibility - Works even with JavaScript disabled
Recommended use cases
- Production applications with static themes
- Sites with critical performance requirements
- Projects with multiple predefined themes
- Applications requiring fine control over CSS loading
Token Reference
Every preset emits the same set of CSS variables on :root. With mode: 'both', each base color is a single light-dark(L, D) declaration that the browser resolves to the active scheme. Use these names directly in your own CSS — var(--maz-primary), calc(var(--maz-space) * 4), etc.
Foundation
Single-value design tokens. Set via foundation.<key> on the preset.
| Preset key | CSS variable | Default | Notes |
|---|---|---|---|
base-font-size | --maz-base-font-size | 14px | Anchors rem for the whole app. Apply on html { font-size: var(--maz-base-font-size) }. |
border-width | --maz-border-width | 1px | Default border for components and the global *::before/::after reset. |
space | --maz-space | 0.25rem | Base step for every Tailwind spacing utility (p-1, gap-2, …). |
font-family | --maz-font-family | preset-specific | Sans / body font stack. Bridged to Tailwind --font-sans. |
font-mono-stack | --maz-font-mono-stack | ui-monospace, … | Monospace stack — MazInputCode, <code>, <kbd>. Bridged to --font-mono. |
font-display-stack | --maz-font-display-stack | same as font-family | Display / heading stack. Bridged to --font-display. |
motion-fast | --maz-motion-fast | 100ms | Bridged to Tailwind --duration-fast. |
motion-normal | --maz-motion-normal | 200ms | Default transition duration (--default-transition-duration). |
motion-slow | --maz-motion-slow | 300ms | Bridged to --duration-slow. |
easing-out | --maz-easing-out | cubic-bezier(0.4, 0, 0.2, 1) | Default easing (--ease-out, also --default-transition-timing-function). |
easing-in | --maz-easing-in | cubic-bezier(0.4, 0, 1, 1) | Bridged to --ease-in. |
easing-in-out | --maz-easing-in-out | cubic-bezier(0.4, 0, 0.2, 1) | Bridged to --ease-in-out. |
disabled-opacity | --maz-disabled-opacity | 0.5 | Applied to disabled buttons / inputs. |
disabled-cursor | --maz-disabled-cursor | not-allowed | Cursor for disabled interactive elements. |
Why
font-mono-stack/font-display-stack/motion-*? Under Tailwind v4prefix(maz), the bridge LHS gets prefixed (--font-mono→--maz-font-mono), so a same-named source token would self-cycle. The-stackandmotion-suffixes break the collision.
Colors
Defined per mode under colors.light / colors.dark. Each base color also gets an automatic 50–950 scale.
| Preset key | CSS variable (base) | Auto scale | Notes |
|---|---|---|---|
surface | --maz-surface | --maz-surface-50..900 | Page / container background. |
foreground | --maz-foreground | --maz-foreground-50..900 | Default text color. |
primary | --maz-primary | --maz-primary-50..900 | Brand color. |
primary-foreground | --maz-primary-foreground | — | Text on bg-primary. |
secondary | --maz-secondary | --maz-secondary-50..900 | |
secondary-foreground | --maz-secondary-foreground | — | |
accent | --maz-accent | --maz-accent-50..900 | |
accent-foreground | --maz-accent-foreground | — | |
info | --maz-info | --maz-info-50..900 | Status — informational. |
info-foreground | --maz-info-foreground | — | |
success | --maz-success | --maz-success-50..900 | Status — success. |
success-foreground | --maz-success-foreground | — | |
warning | --maz-warning | --maz-warning-50..900 | Status — warning. |
warning-foreground | --maz-warning-foreground | — | |
destructive | --maz-destructive | --maz-destructive-50..900 | Status — error / danger. |
destructive-foreground | --maz-destructive-foreground | — | |
contrast | --maz-contrast | --maz-contrast-50..900 | High-contrast accent (light = near-black, dark = near-white). |
contrast-foreground | --maz-contrast-foreground | — | |
divider | --maz-divider | --maz-divider-50..900 | Default border / separator color. |
muted | --maz-muted | --maz-muted-50..900 | Muted / secondary text. |
overlay | --maz-overlay | --maz-overlay-50..900 | Backdrop / dialog scrim. |
shadow | --maz-shadow | --maz-shadow-50..900 | Tint used by Tailwind shadow-* utilities (via --color-elevation). |
Scales
Multi-step scales under scales.<key>.
| Preset key | CSS variables | Tailwind utility |
|---|---|---|
rounded.xs..3xl | --maz-rounded-xs..3xl | rounded-{key} (bridged to --radius-{key}) |
shadow.{sm, md, lg, xl, elevation} | --maz-shadow-style-{key} | shadow-{key} (bridged to --shadow-{key}) |
color ('relative' | 'perceptual') | --maz-color-<role>-50..950, --color-<role>-50..950 | bg-primary-300... (see Color scales) |
Same naming-collision avoidance as foundation:
rounded.*(instead ofradius.*) andshadow-style-*(instead of plainshadow-*) keep the bridge cycle-free underprefix(maz).
Components
Per-component knobs under components.<key>. All optional — omit to fall back to the surface tokens.
| Preset key | CSS variable | Notes |
|---|---|---|
btn.font-weight | --maz-btn-font-weight | Font-weight on .m-btn. Defaults to '500'. |
container.bg.light / .dark | --maz-container-bg (per mode) | Background of Card, Container, Dialog, Popover, Drawer, … Defaults to var(--maz-surface). Bridged to --color-container. |
input.bg.light / .dark | --maz-input-bg (per mode) | Background of Input, Textarea, Select, Checkbox, … Defaults to var(--maz-surface) light, var(--maz-surface-400) dark. Bridged to --color-input. |
dialog.max-width | --maz-dialog-max-width | Max-width of MazDialog on tablet and up. Defaults to 38rem (33.25rem on ocean). Overridable via the max-width prop. |
dialog.min-width | --maz-dialog-min-width | Min-width of MazDialog on tablet and up. Defaults to 32rem (28rem on ocean). Overridable via the min-width prop. |
Sample output
Each base color is emitted as a light-dark() value; scales --maz-X-50 through --maz-X-950 are derived via OKLCh relative color syntax so they automatically follow base color overrides at runtime, preserving chroma at every step.
:root {
color-scheme: light dark;
/* Base colors — one declaration each, resolved by the browser */
--maz-primary: light-dark(oklch(0.65 0.19 254), oklch(0.72 0.18 254));
--maz-primary-foreground: light-dark(oklch(1 0 0), oklch(0.16 0.01 254));
--maz-surface: light-dark(oklch(1 0 0), oklch(0.18 0.01 254));
--maz-foreground: light-dark(oklch(0.26 0.01 254), oklch(0.96 0.005 254));
/* Auto-generated 50–950 scale — derived via relative color syntax */
--maz-primary-500: var(--maz-primary);
--maz-primary-600: oklch(from var(--maz-primary) clamp(0, calc(l - 0.07), 1) c h);
/* ... */
/* Foundation */
--maz-base-font-size: 14px;
--maz-border-width: 1px;
--maz-space: 0.25rem;
--maz-font-family: Manrope, sans-serif, …;
--maz-motion-normal: 200ms;
/* Scales */
--maz-rounded-md: 0.7rem;
--maz-shadow-style-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), …;
}
/* With darkModeStrategy: 'class' — explicit user choice overrides the system */
.dark { color-scheme: only dark; }
.light { color-scheme: only light; }For deeper detail on the generated CSS contract, see the @maz-ui/themes README.
Usage with Nuxt
For Nuxt users, check the dedicated Nuxt documentation which covers installation and framework-specific configuration.
Migration from Legacy System
If you're using the legacy theme system with CLI:
// maz-ui.config.ts
export default defineConfig({
theme: {
colors: {
primary: 'hsl(210, 100%, 56%)',
secondary: 'hsl(164, 76%, 46%)'
}
}
})// main.ts
import { definePreset, mazUi } from '@maz-ui/themes'
import { MazUiTheme } from '@maz-ui/themes/plugin'
const myTheme = definePreset({
base: mazUi,
overrides: {
colors: {
light: {
primary: 'oklch(0.65 0.19 254)',
secondary: 'oklch(0.72 0.16 168)'
}
}
}
})
app.use(MazUiTheme, { preset: myTheme })The new system offers much more flexibility and performance!