@atomazing-org/design-system
v2.0.1
Published
A library providing a set of useful utils, MUI style extensions, and components to build your application.
Readme
@atomazing-org/design-system
Modern MUI v7 + Emotion design system with strongly differentiated visual presets, scheme-based light/dark tokens, SSR-safe theming, and persistence.
Preview

Why use it
- True light/dark themes: each preset ships two curated schemes (
light+dark), so backgrounds, cards, and text actually change (not justpalette.mode). - Works out of the box: includes ready-made presets you can use immediately.
- Easy to extend: bring your own presets, or combine defaults + custom.
- Consistent UI: global styles + MUI component overrides for predictable visuals.
- Safe persistence: remembers selected theme + dark mode with SSR-safe guards.
Installation
Install the library:
npm install @atomazing-org/design-systemInstall required peer dependencies (React 18/19 + MUI v7 + Emotion core):
npm install react react-dom @mui/material @emotion/react @emotion/styledOptional peers:
@mui/icons-material(only if your app imports MUI icons)@emotion/css(only if your project uses Emotioncsshelpers)
npm install @mui/icons-material @emotion/cssPackage format (v2)
@atomazing-org/design-system v2 is ESM-only.
Use ESM imports (import ... from ...). CommonJS require() is not supported.
Quick start
Use built-in presets (recommended starting point):
import { ThemeProviderWrapper } from "@atomazing-org/design-system";
import { defaultThemes } from "@atomazing-org/design-system/presets";
export function App() {
return (
<ThemeProviderWrapper themes={defaultThemes}>
{/* your app */}
</ThemeProviderWrapper>
);
}Notes:
themescan be default presets, your presets, or both.- Theme choice + dark mode are persisted (storage key:
appSettings).
Built-in presets
Built-in presets are ready-made theme packs shipped with the library.
You can use them as-is, or add your own presets alongside them.
| Preset | Stable id | Best for |
|---|---|---|
| Warm Earth | warm-earth | Warm and friendly apps |
| Editorial Classic | editorial-classic | Content-heavy UIs |
| Modern Minimal | modern-minimal | Dashboards and tooling |
| Neo Glass | neo-glass | Modern “glass” style UIs |
| Retro Terminal | retro-terminal | Terminal-inspired branding |
Import them from:
import { defaultThemes } from "@atomazing-org/design-system/presets";Switching theme (UI)
Build a simple theme switcher using the library’s theme settings hook.
import { ToggleButton, ToggleButtonGroup } from "@mui/material";
import { useThemeSettings } from "@atomazing-org/design-system";
import { defaultThemes } from "@atomazing-org/design-system/presets";
export function ThemeSwitcher() {
const { theme, setTheme } = useThemeSettings();
return (
<ToggleButtonGroup
size="small"
exclusive
value={theme}
onChange={(_, next) => next && setTheme(next)}
>
{defaultThemes.map((t) => (
<ToggleButton key={t.id} value={t.id}>
{t.label}
</ToggleButton>
))}
</ToggleButtonGroup>
);
}Dark mode
Dark mode switches the active scheme inside the current preset.
Supported values
light— always use the preset’s light schemedark— always use the preset’s dark schemesystem— follow OS/browser preference (prefers-color-scheme)
Why system exists
It’s the most common UX: the app automatically follows the user’s OS preference without custom code in every project.
Example selector:
import { RadioGroup, FormControlLabel, Radio } from "@mui/material";
import { useThemeSettings } from "@atomazing-org/design-system";
export function DarkModeSelector() {
const { darkMode, setDarkMode } = useThemeSettings();
return (
<RadioGroup row value={darkMode} onChange={(e) => setDarkMode(e.target.value as any)}>
<FormControlLabel value="system" control={<Radio />} label="System" />
<FormControlLabel value="light" control={<Radio />} label="Light" />
<FormControlLabel value="dark" control={<Radio />} label="Dark" />
</RadioGroup>
);
}Forcing mode (optional)
If your app does not support dark mode, you can force the effective palette mode:
import { ThemeProviderWrapper } from "@atomazing-org/design-system";
import { defaultThemes } from "@atomazing-org/design-system/presets";
export function App() {
return (
<ThemeProviderWrapper themes={defaultThemes} darkMode="light">
{/* your app */}
</ThemeProviderWrapper>
);
}Note: when darkMode is set, darkMode is locked and setDarkMode is ignored.
Persistence (appSettings)
Theme selection is persisted in localStorage under the key appSettings.
v2 JSON shape:
{ "themeId": "editorial-classic", "darkMode": "system" }Notes:
- v2 stores only
themeIdanddarkMode. - v2 does not migrate legacy stored formats (
theme,version,auto); invalid/legacy payloads reset to defaults.
App migration routes (repo docs)
If you are migrating an app to v2, use the route runbooks under:
migrations/docs/migrations/design-system/README.mdmigrations/docs/migrations/design-system/routes/*
examples/react-app is the canonical Vite consumer smoke app for local v2 validation (npm run smoke:react-app).
examples/next-app-router is the canonical Next.js App Router SSR reference consumer (npm run smoke:next).
Custom themes
Provide your own preset(s) to the provider. A preset is one identifiable theme with two schemes: light and dark.
API reference — ThemePreset
| Field | Type | What it means |
|---|---|---|
| id | string | Stable identifier used for persistence and deduplication. Must be unique. |
| label | string | Name shown in UI selectors. |
| colorSchemes.light | ThemeOptions | MUI ThemeOptions for light mode. |
| colorSchemes.dark | ThemeOptions | MUI ThemeOptions for dark mode. |
Minimum required tokens per scheme
Each scheme must define:
palette.background.default— page backgroundpalette.background.paper— Card/Paper backgroundpalette.text.primarypalette.text.secondarypalette.divider
Example:
import type { ThemePreset } from "@atomazing-org/design-system";
export const myPreset: ThemePreset = {
id: "my-brand",
label: "My Brand",
colorSchemes: {
light: {
palette: {
background: { default: "#ffffff", paper: "#ffffff" },
text: { primary: "#111111", secondary: "#444444" },
divider: "rgba(0,0,0,0.12)",
},
},
dark: {
palette: {
background: { default: "#0f1115", paper: "#151922" },
text: { primary: "#f1f3f5", secondary: "#c6cbd1" },
divider: "rgba(241,243,245,0.16)",
},
},
},
};Using defaults + custom presets
import { defaultThemes } from "@atomazing-org/design-system/presets";
const themes = [...defaultThemes, myPreset];Then pass themes into ThemeProviderWrapper.
Typography variants
The library adds extra typography variants (beyond MUI defaults) so your text styles are consistent across the app.
import { Typography } from "@mui/material";
export function TypographyDemo() {
return (
<>
<Typography variant="header_lg_bold">Page title</Typography>
<Typography variant="text_md_regular">Body text</Typography>
<Typography variant="text_sm_bold">Caption / label</Typography>
</>
);
}Notes:
- Typography sizing is expected to be rem-based (scales with the user’s browser font size settings).
- Variants are defined in the design system theme (no local “one-off” styles needed).
Global styles
ThemeProviderWrapper applies global styles (via Emotion) and MUI component defaults, so the app looks consistent immediately.
Set a custom font stack:
import { ThemeProviderWrapper } from "@atomazing-org/design-system";
export function App() {
return (
<ThemeProviderWrapper fontFamily="Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif">
{/* your app */}
</ThemeProviderWrapper>
);
}Animations
The library exports reusable keyframes so you can keep motion consistent.
import styled from "@emotion/styled";
import { fadeIn } from "@atomazing-org/design-system";
const Panel = styled.div`
animation: ${fadeIn} 240ms ease-in both;
`;
export function AnimatedPanel() {
return <Panel>Content</Panel>;
}SSR notes
- The library is written to be SSR-safe: no
window/document/localStorageaccess at module scope. - If you use system dark mode, the effective mode is derived from
prefers-color-schemeon the client. - In Next.js, place the provider inside a client boundary (e.g., a component with
"use client"), then wrap your app withThemeProviderWrapper.
Consumer alias guidance (optional)
Aliases are optional. They help you avoid long relative imports inside your app.
Local development (repo)
From the repository root:
pnpm lint
pnpm build
pnpm test
pnpm -C examples/react-app dev
pnpm -C examples/next-app-router devPeer dependencies
Make sure your app installs these (MUI v7 + Emotion):
reactreact-dom@mui/material@mui/icons-material(optional, only if you use icons from MUI)@emotion/react@emotion/styled@emotion/css(optional)
Troubleshooting
Dark mode switches, but background/cards do not change
- Ensure your preset provides both schemes:
colorSchemes.light.palette.background.default/papercolorSchemes.dark.palette.background.default/paper
- Avoid hardcoded colors in overrides. Use
theme.palette.*tokens.
Text is hard to read in dark mode
- Do not reuse light “ink” constants in dark mode.
- Check:
palette.text.primaryvspalette.background.defaultpalette.text.secondaryvspalette.background.paper
I want to add my own presets
- Create a
ThemePresetwithcolorSchemes.lightandcolorSchemes.dark. - Combine with defaults:
const themes = [...defaultThemes, myPreset];
Examples (examples/react-app, examples/next-app-router)
The repo includes runnable example apps that demonstrate:
- preset switching
- dark mode switching
- background + card surfaces changing correctly
- token/debug view (if enabled in the example)
- Next.js App Router SSR integration with a client provider boundary
Run it from the repository root:
pnpm -C examples/react-app dev
pnpm -C examples/next-app-router devWhat to check in the UI:
- switching to dark changes the page background and Card/Paper background
- text stays readable on both backgrounds
- inputs, buttons, menus, and dialogs remain usable
- Next.js example keeps SSR stable while theme state finalizes on the client
API reference — ThemeProviderWrapper
ThemeProviderWrapper wires MUI ThemeProvider, global styles, and persisted settings.
| Prop | Type | Default | What it does |
|---|---|---|---|
| themes | ThemePreset[] (or built-in defaultThemes) | defaultThemes | List of presets available to the user. |
| fontFamily | string | (theme default) | Optional global font stack for the whole app. |
| darkMode | DarkModeOptions | - | Forces mode regardless of persisted settings and system preference. |
| children | ReactNode | — | Your application tree. |
Minimal usage:
import { ThemeProviderWrapper } from "@atomazing-org/design-system";
import { defaultThemes } from "@atomazing-org/design-system/presets";
export function App() {
return (
<ThemeProviderWrapper themes={defaultThemes}>
{/* your app */}
</ThemeProviderWrapper>
);
}Public exports (recommended imports)
Use only these stable entry points:
Core API
import { ThemeProviderWrapper, useThemeSettings } from "@atomazing-org/design-system";Built-in presets
import { defaultThemes } from "@atomazing-org/design-system/presets";Tip:
- Avoid deep imports from internal folders. They may change during refactors.
Migration notes (if upgrading)
If you previously passed a single theme object or a legacy “theme list”:
- Move your theme into a preset shape with two schemes:
colorSchemes.lightcolorSchemes.dark
- Ensure each scheme defines the required tokens:
background.default,background.papertext.primary,text.secondarydivider
- Pass presets into the provider:
themes={[...defaultThemes, myPreset]}or only[myPreset]
Goal:
- dark mode changes real palette tokens, so backgrounds/cards/text update together.
License
MIT
