A comprehensive React and Angular component library with a complete design system, built for modern web applications.
Current releases:
{
"@mezzanine-ui/core": "1.0.3",
"@mezzanine-ui/react": "1.0.3",
"@mezzanine-ui/ng": "1.0.0-rc.2",
"@mezzanine-ui/system": "1.0.2",
"@mezzanine-ui/icons": "1.0.2"
}
@mezzanine-ui/ngis published under thercdist-tag while the Angular port stabilises. Install with@rc(see below).
- Storybook — unified hub that composes the React and Angular Storybooks side by side
https://storybook.mezzanine-ui.org/react/— React canonical URLhttps://storybook.mezzanine-ui.org/angular/— Angular canonical URL
- Migration Guide — Upgrading from previous versions
| Browser | Minimum Version |
|---|---|
| Google Chrome | 64 (2018) |
| Edge | 79 (2020) |
| Safari | 13.1 (2020) |
| Firefox | 69 (2019) |
Mezzanine UI fully supports Next.js including the App Router. All React components include the 'use client' directive, making them compatible with React Server Components architecture.
// app/layout.tsx - Works seamlessly with Next.js App Router
import { CalendarConfigProviderDayjs, CalendarLocale } from '@mezzanine-ui/react/dayjs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<CalendarConfigProviderDayjs locale={CalendarLocale.ZH_TW}>{children}</CalendarConfigProviderDayjs>
</body>
</html>
);
}All hooks and utilities are also SSR-safe and can be used in Next.js projects without additional configuration.
yarn add @mezzanine-ui/core @mezzanine-ui/react @mezzanine-ui/system @mezzanine-ui/iconsRequires Angular 21+.
yarn add @mezzanine-ui/core @mezzanine-ui/system @mezzanine-ui/icons
yarn add @mezzanine-ui/ng@rc
yarn add @angular/animations @angular/cdk @angular/common @angular/coreIf you plan to use date-related components (DatePicker, Calendar, TimePicker, etc.), install one of the supported date libraries:
# Choose one:
yarn add dayjs # Recommended - lightweight
yarn add moment # Legacy support
yarn add luxon # AlternativeCreate a main.scss file in your project:
@use '@mezzanine-ui/system' as mzn-system;
@use '@mezzanine-ui/core' as mzn-core;
// Apply design system variables
:root {
@include mzn-system.colors(light);
@include mzn-system.common-variables(default);
}
// Optional: Dark mode support
[data-theme='dark'] {
@include mzn-system.colors(dark);
}
// Optional: Compact mode support
[data-density='compact'] {
@include mzn-system.common-variables(compact);
}
// Import component styles
@include mzn-core.styles();Import the stylesheet at your app's entry point:
import './main.scss';
function App() {
return <div>Your App</div>;
}import { Button, Typography } from '@mezzanine-ui/react';
import { PlusIcon } from '@mezzanine-ui/icons';
function App() {
return (
<div>
<Typography variant="h1">Welcome to Mezzanine UI</Typography>
<Button icon={PlusIcon} iconType="leading" size="main" variant="base-primary">
Click Me
</Button>
</div>
);
}If your application uses date-related components (DatePicker, DateRangePicker, Calendar, TimePicker, etc.), you must wrap your app with a CalendarConfigProvider. This provider configures the date library and locale settings.
Choose one of the following date libraries based on your project needs:
yarn add dayjsimport { CalendarConfigProviderDayjs, CalendarLocale } from '@mezzanine-ui/react/dayjs';
function App({ children }) {
return <CalendarConfigProviderDayjs locale={CalendarLocale.ZH_TW}>{children}</CalendarConfigProviderDayjs>;
}yarn add momentimport { CalendarConfigProviderMoment, CalendarLocale } from '@mezzanine-ui/react/moment';
function App({ children }) {
return <CalendarConfigProviderMoment locale={CalendarLocale.ZH_TW}>{children}</CalendarConfigProviderMoment>;
}yarn add luxonimport { CalendarConfigProviderLuxon, CalendarLocale } from '@mezzanine-ui/react/luxon';
function App({ children }) {
return <CalendarConfigProviderLuxon locale={CalendarLocale.ZH_TW}>{children}</CalendarConfigProviderLuxon>;
}
⚠️ Important: Import the provider from the specific entry point (e.g.,@mezzanine-ui/react/dayjs) to avoid bundling unused date libraries.
| Prop | Type | Default | Description |
|---|---|---|---|
locale |
CalendarLocaleValue |
CalendarLocale.EN_US |
Controls calendar display: first day of week, month/weekday names |
defaultDateFormat |
string |
'YYYY-MM-DD' |
Default format string for date values |
defaultTimeFormat |
string |
'HH:mm:ss' |
Default format string for time values |
Common locale values (see CalendarLocale enum for full list):
| Locale | Value | First Day of Week |
|---|---|---|
CalendarLocale.EN_US |
'en-US' |
Sunday |
CalendarLocale.EN_GB |
'en-GB' |
Monday |
CalendarLocale.ZH_TW |
'zh-TW' |
Sunday |
CalendarLocale.ZH_CN |
'zh-CN' |
Monday |
CalendarLocale.JA_JP |
'ja-JP' |
Sunday |
CalendarLocale.KO_KR |
'ko-KR' |
Sunday |
CalendarLocale.DE_DE |
'de-DE' |
Monday |
CalendarLocale.FR_FR |
'fr-FR' |
Monday |
For Next.js App Router, place the provider in your root layout:
// app/layout.tsx
import { CalendarConfigProviderDayjs, CalendarLocale } from '@mezzanine-ui/react/dayjs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<CalendarConfigProviderDayjs locale={CalendarLocale.ZH_TW}>{children}</CalendarConfigProviderDayjs>
</body>
</html>
);
}import { CalendarConfigProviderDayjs, CalendarLocale } from '@mezzanine-ui/react/dayjs';
function App({ children }) {
return (
<CalendarConfigProviderDayjs defaultDateFormat="YYYY/MM/DD" defaultTimeFormat="HH:mm" locale={CalendarLocale.ZH_TW}>
{children}
</CalendarConfigProviderDayjs>
);
}| Library | Bundle Size | Tree-Shakeable | ISO Week Support | Note |
|---|---|---|---|---|
| Day.js | ~2KB | Yes | Yes | Recommended for most projects |
| Moment | ~70KB | No | Yes | Legacy support |
| Luxon | ~20KB | Yes | Yes (ISO only) | Always uses Monday as first day |
The Angular package mirrors the React API prop-for-prop. Components are distributed as secondary entry points — import each component from its own path so that Angular's tree-shaker only pulls in what you use.
@mezzanine-ui/ng consumes the same @mezzanine-ui/core + @mezzanine-ui/system tokens as React. Reuse the main.scss shown in the React Quick Start verbatim and load it from your Angular app's styles array (angular.json) or global stylesheet.
// app.component.ts
import { Component } from '@angular/core';
import { MznButton } from '@mezzanine-ui/ng/button';
import { MznTypography } from '@mezzanine-ui/ng/typography';
import { MznIcon } from '@mezzanine-ui/ng/icon';
import { PlusIcon } from '@mezzanine-ui/icons';
@Component({
selector: 'app-root',
standalone: true,
imports: [MznButton, MznTypography, MznIcon],
template: `
<mzn-typography variant="h1">Welcome to Mezzanine UI</mzn-typography>
<mzn-button variant="base-primary" size="main" [icon]="plusIcon" iconType="leading"> Click Me </mzn-button>
`,
})
export class AppComponent {
plusIcon = PlusIcon;
}
⚠️ Do not barrel-import from@mezzanine-ui/ng. Always use the per-component path (e.g.@mezzanine-ui/ng/button) so ng-packagr's secondary entry points can be code-split correctly.
Angular inputs/outputs mirror React props 1:1, including both flat top-level props (e.g. disabledMonthSwitch, placeholderLeft) and bundle props (e.g. calendarProps, popperProps). When both are provided, flat props win. See .claude/skills/architecting-angular-components/SKILL.md for the full contract.
Mezzanine UI v2 uses a two-layer design token system:
- Primitives: Raw values (e.g.,
#3b82f6,16px) - Semantic: Contextual tokens that reference primitives (e.g.,
text-brand,padding-base)
💡 Best Practice: Always use semantic tokens in your application for automatic theme switching support.
Override palette colors by passing a custom configuration:
@use '@mezzanine-ui/system' as mzn-system;
$custom-palette: (
background: (
base: (
light: #000,
dark: #fff,
),
menu: (
light: #fff,
dark: #9a9a9a,
),
// ...
), // ...
);
:root {
@include mzn-system.colors(light, $custom-palette);
}
[data-theme='dark'] {
@include mzn-system.colors(dark, $custom-palette);
}Override typography settings:
@use '@mezzanine-ui/system' as mzn-system;
@use '@mezzanine-ui/system/typography' as typography;
$custom-variables: (
typography: (
h3: (
font-size: 18px,
font-weight: 700,
line-height: 26px,
letter-spacing: 0,
),
// ...
),
);
:root {
@include mzn-system.common-variables(default, $custom-variables);
}Override spacing values:
@use '@mezzanine-ui/system' as mzn-system;
$custom-variables: (
spacing: (
size: (
element: (
hairline: (
default: 2px,
compact: 2px,
),
),
),
),
);
:root {
@include mzn-system.common-variables(default, $custom-variables);
}
[data-density='compact'] {
@include mzn-system.common-variables(compact, $custom-variables);
}@use '@mezzanine-ui/system/palette' as palette;
@use '@mezzanine-ui/system/spacing' as spacing;
@use '@mezzanine-ui/system/radius' as radius;
@use '@mezzanine-ui/system/typography' as typography;
.my-component {
// Colors - use semantic variables
color: palette.semantic-variable(text, brand);
background-color: palette.semantic-variable(background, base);
border-color: palette.semantic-variable(border, neutral);
// Spacing - use semantic variables
padding: spacing.semantic-variable(padding, horizontal, base);
gap: spacing.semantic-variable(gap, tight);
// Border radius
border-radius: radius.variable(base);
// Typography - apply full semantic typography
@include typography.semantic-variable(body);
}| Module | Purpose | Example |
|---|---|---|
palette |
Colors (text, background, etc.) | palette.semantic-variable(text, brand) |
spacing |
Padding, margin, gap | spacing.semantic-variable(padding, base) |
radius |
Border radius | radius.variable(base) |
typography |
Font settings | @include typography.semantic-variable(button) |
effect |
Shadows, focus rings | effect.variable(focus, primary) |
size |
Element sizes | size.semantic-variable(element, main) |
import { Button } from '@mezzanine-ui/react';
import { PlusIcon } from '@mezzanine-ui/icons';
<Button variant="base-primary" size="main">
Primary Button
</Button>
<Button variant="base-secondary" size="sub" disabled>
Disabled Button
</Button>
<Button icon={PlusIcon} iconType="leading" size="minor" variant="outlined-primary">
With Icon
</Button>import { Typography } from '@mezzanine-ui/react';
<Typography variant="h1">Heading 1</Typography>
<Typography variant="body">Body text</Typography>
<Typography variant="caption" color="text-neutral">
Caption text
</Typography>Requires
CalendarConfigProviderwrapper (see Setup CalendarConfigProvider)
import { useState } from 'react';
import { DatePicker } from '@mezzanine-ui/react';
function Example() {
const [date, setDate] = useState<string>();
return (
<DatePicker
onChange={setDate}
placeholder="Select a date"
value={date}
/>
);
}import { useState } from 'react';
import { DateRangePicker } from '@mezzanine-ui/react';
function Example() {
const [range, setRange] = useState<[string, string]>();
return (
<DateRangePicker
onChange={setRange}
value={range}
/>
);
}Layout is the top-level page shell for building full-application frames. It manages a responsive structure with a sticky navigation sidebar and optional resizable side panels. The sub-components (Layout.Main, Layout.LeftPanel, Layout.RightPanel) can be placed in any order — the layout always renders them in the correct visual sequence.
| Sub-component | Purpose |
|---|---|
<Navigation> |
Sticky left navigation sidebar |
<Layout.Main> |
Main scrollable content area (required) |
<Layout.LeftPanel> |
Resizable left panel (optional) |
<Layout.RightPanel> |
Resizable right panel (optional) |
Basic layout with navigation:
import { useState } from 'react';
import { Layout, Navigation, NavigationFooter, NavigationHeader, NavigationOption } from '@mezzanine-ui/react';
import { FileIcon, HomeIcon } from '@mezzanine-ui/icons';
function App() {
const [activatedPath, setActivatedPath] = useState(['Home']);
return (
<Layout>
<Navigation activatedPath={activatedPath} onOptionClick={(path) => path && setActivatedPath(path)}>
<NavigationHeader title="My App" />
<NavigationOption icon={HomeIcon} title="Home" />
<NavigationOption icon={FileIcon} title="Reports">
<NavigationOption title="Traffic" />
<NavigationOption title="Conversion" />
</NavigationOption>
<NavigationFooter />
</Navigation>
<Layout.Main>
<h1>Page Content</h1>
</Layout.Main>
</Layout>
);
}With a toggleable right panel:
import { useState } from 'react';
import { Layout, Navigation, NavigationFooter, NavigationHeader } from '@mezzanine-ui/react';
function App() {
const [rightOpen, setRightOpen] = useState(false);
return (
<Layout>
<Navigation>
<NavigationHeader title="My App" />
<NavigationFooter />
</Navigation>
<Layout.Main>
<button onClick={() => setRightOpen(true)}>Open Details</button>
</Layout.Main>
<Layout.RightPanel defaultWidth={320} open={rightOpen}>
<div>
<h2>Details</h2>
<button onClick={() => setRightOpen(false)}>Close</button>
</div>
</Layout.RightPanel>
</Layout>
);
}With dual panels (left + right):
import { useState } from 'react';
import { Layout, Navigation, NavigationFooter, NavigationHeader } from '@mezzanine-ui/react';
function App() {
const [leftOpen, setLeftOpen] = useState(true);
const [rightOpen, setRightOpen] = useState(false);
return (
<Layout>
<Navigation>
<NavigationHeader title="My App" />
<NavigationFooter />
</Navigation>
<Layout.LeftPanel defaultWidth={240} open={leftOpen}>
<div>Sidebar filters, navigation trees, etc.</div>
</Layout.LeftPanel>
<Layout.Main>
<h1>Main Content</h1>
{!rightOpen && <button onClick={() => setRightOpen(true)}>Open Right</button>}
</Layout.Main>
<Layout.RightPanel defaultWidth={320} open={rightOpen}>
<div>Detail view, preview, contextual actions, etc.</div>
</Layout.RightPanel>
</Layout>
);
}Layout.LeftPanel / Layout.RightPanel props:
| Prop | Type | Default | Description |
|---|---|---|---|
open |
boolean |
false |
Controls panel visibility |
defaultWidth |
number |
320 |
Initial width in px (minimum 240) |
onWidthChange |
(width: number) => void |
— | Callback when the panel is resized |
scrollbarProps |
ScrollbarProps |
— | Props forwarded to the inner scrollbar |
Panels are resizable by dragging the divider. Focus the divider and use
←/→arrow keys to resize with keyboard.
Mezzanine provides several utility hooks for common UI patterns:
| Hook | Description |
|---|---|
useClickAway |
Detect clicks outside an element (useful for dropdowns, modals) |
useComposeRefs |
Compose multiple refs into one |
useDocumentEscapeKeyDown |
Listen for ESC key press on document |
useDocumentTabKeyDown |
Listen for Tab key press on document |
useDocumentEvents |
Generic document event listener with cleanup |
useIsomorphicLayoutEffect |
SSR-safe useLayoutEffect (uses useEffect on server) |
useLastCallback |
Stable callback reference that always calls the latest version |
useLastValue |
Ref that always holds the latest value |
usePreviousValue |
Access the previous render's value |
useScrollLock |
Lock body scroll (for modals/overlays) with scrollbar gap compensation |
useTopStack |
Manage stacking order for overlays |
useWindowWidth |
Track window width with resize listener |
import { useRef } from 'react';
import { useClickAway } from '@mezzanine-ui/react';
function Dropdown({ onClose }) {
const dropdownRef = useRef();
useClickAway(() => (event) => onClose(event), dropdownRef, [onClose]);
return <div ref={dropdownRef}>Dropdown content</div>;
}import { useScrollLock } from '@mezzanine-ui/react';
function Modal({ isOpen }) {
// Automatically locks scroll when modal is open
useScrollLock({ enabled: isOpen });
return isOpen ? <div className="modal">Modal content</div> : null;
}Mezzanine also exports commonly used utility functions:
| Utility | Description |
|---|---|
formatNumberWithCommas |
Format numbers with locale-aware thousands separators |
parseNumberWithCommas |
Parse comma-formatted string back to number |
getCSSVariableValue |
Get computed CSS variable value from :root |
getNumericCSSVariablePixelValue |
Get CSS variable as numeric pixel value |
arrayMove |
Move array item from one index to another (immutable) |
cx |
Classname utility (re-exported from clsx) |
composeRefs |
Compose multiple refs (non-hook version) |
getScrollbarWidth |
Get current scrollbar width in pixels |
import { formatNumberWithCommas, parseNumberWithCommas } from '@mezzanine-ui/react';
// Format number for display
formatNumberWithCommas(1234567); // '1,234,567'
formatNumberWithCommas(1234567, 'de-DE'); // '1.234.567'
// Parse back to number
parseNumberWithCommas('1,234,567'); // 1234567import { getCSSVariableValue } from '@mezzanine-ui/react';
// Read design token values at runtime
const brandColor = getCSSVariableValue('--mzn-color-text-brand');Mezzanine UI v2 has built-in support for light and dark modes:
// In your SCSS
:root {
@include mzn-system.colors(light);
}
[data-theme='dark'] {
@include mzn-system.colors(dark);
}Toggle theme in your React app:
function ThemeToggle() {
const [theme, setTheme] = useState('light');
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return <Button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</Button>;
}Switch between comfortable and compact spacing:
:root {
@include mzn-system.common-variables(default);
}
[data-density='compact'] {
@include mzn-system.common-variables(compact);
}Toggle density in your React app:
function DensityToggle() {
const [density, setDensity] = useState('default');
useEffect(() => {
document.documentElement.setAttribute('data-density', density);
}, [density]);
return <Button onClick={() => setDensity(density === 'default' ? 'compact' : 'default')}>Toggle Density</Button>;
}Use icons from the @mezzanine-ui/icons package:
import Icon from '@mezzanine-ui/react/Icon';
import { CheckIcon, ChevronDownIcon, PlusIcon } from '@mezzanine-ui/icons';
function Example() {
return (
<div>
<Icon icon={ChevronDownIcon} />
<Icon icon={PlusIcon} />
<Icon icon={CheckIcon} />
</div>
);
}We welcome contributions! Please see our Development Guidelines for:
- Setting up the development environment
- Understanding the project architecture
- Following coding conventions
- Writing tests and documentation
MIT License - see LICENSE for details.