Theme
Prism's theme is MUI v9's theme + extensions (custom shadows,
density tokens, brand presets, runtime primary-color override). The
factory is createPrismTheme — it builds a fully-formed MUI theme from
a small set of high-level options.
import { createPrismTheme } from '@omnitron-dev/prism/theme';
const theme = createPrismTheme({
preset: 'luxury', // brand palette (see Presets below)
mode: 'dark', // 'light' | 'dark' | 'system'
primaryColor: '#7c4dff', // runtime primary override (hex)
borderRadius: 8, // base radius in px (not `shape`)
density: 'standard', // 'compact' | 'standard' | 'comfortable'
});Most apps never call
createPrismThemedirectly —<PrismProvider>builds the theme from the settings store for you. Reach for the factory when you need a theme outside React (SSR, Storybook, tests).
Palette
createPrismTheme does not take a raw MUI palette object. Instead you
pick a preset and (optionally) a primaryColor, and Prism builds the
full palette — including light/dark color schemes and channel variants —
for you. The resulting theme.palette is a standard MUI palette:
theme.palette = {
mode: 'light' | 'dark',
primary: { main, light, dark, contrastText },
secondary: { main, light, dark, contrastText },
error / warning / info / success: { main, light, dark, contrastText },
text: { primary, secondary, disabled },
background: { default, paper, neutral }, // 'neutral' for subtle section bg
action: { active, hover, selected, disabled, disabledBackground, focus },
divider,
}To set the brand colour, pass primaryColor: '#7c4dff' — Prism derives
the light/dark shades and contrast text. For deeper customisation, pass
through raw MUI options via the overrides option
(createPrismTheme({ overrides: { palette: { … } } })).
Semantic colour tokens
Avoid hex codes in app code — use theme tokens:
<Box sx={{ color: 'primary.main', bgcolor: 'background.paper' }} />
<Typography color="text.secondary">…</Typography>This keeps dark mode working automatically and lets you re-skin without touching component code.
Readable status surfaces
Standard and soft alerts, soft chips and soft labels share one contrast
policy. Prism compares the semantic colour tones against their translucent
fill composited over background.paper, selecting a tone with at least
4.5 contrast when available. It preserves the colour token and emits both
scheme rules, so custom CSS variable prefixes and live light/dark switching
continue to work. This also covers MUI Alert and Chip used directly under
PrismProvider or createPrismTheme.
Custom palettes can use either ordinary MUI tonal roles or Prism's inverted
dark scale. When none of the supplied tones is readable, or a colour can
only be resolved by the browser, Prism uses text.primary. Inverted labels
use the theme's white surface and grey text fallback. Browser-resolved
colours, custom parent backgrounds and application sx overrides still
need contrast checks in the final UI; the calculation assumes an opaque
background.paper beneath the translucent fill.
An alert's color overrides its appearance; severity still selects its
meaning and icon. For example, <Alert severity="warning" color="error">
uses the error palette with the warning icon. Use text as well as colour
to explain the state.
Typography
typography: {
fontFamily: '"Inter", "Roboto", sans-serif',
fontSize: 14, // base px
htmlFontSize: 16, // for rem conversion
fontWeightLight: 300,
fontWeightRegular: 400,
fontWeightMedium: 500,
fontWeightBold: 700,
h1: { fontSize: '2.5rem', fontWeight: 700, lineHeight: 1.2 },
h2: { fontSize: '2rem', fontWeight: 700, lineHeight: 1.25 },
// … h3, h4, h5, h6
subtitle1: { fontSize: '1rem', fontWeight: 600, lineHeight: 1.5 },
subtitle2: { fontSize: '0.875rem', fontWeight: 600, lineHeight: 1.5 },
body1: { fontSize: '1rem', fontWeight: 400, lineHeight: 1.5 },
body2: { fontSize: '0.875rem', fontWeight: 400, lineHeight: 1.5 },
caption: { fontSize: '0.75rem', fontWeight: 400, lineHeight: 1.4 },
overline: { fontSize: '0.625rem', fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase' },
button: { fontWeight: 600, textTransform: 'none' }, // Prism default: no SHOUTY buttons
}Apply via <Typography> or sx:
<Typography variant="h3">Section title</Typography>
<Box sx={{ typography: 'body2', color: 'text.secondary' }}>Small print</Box>Spacing
spacing: 8 means theme.spacing(N) === 8 × N pixels. The
sx shorthand interprets numbers in spacing units:
<Box sx={{ p: 2, mt: 4, gap: 1 }} />
// padding: 16px, margin-top: 32px, gap: 8pxConvention: use the spacing unit (p: 2) not pixel values
(p: '16px') — keeps the rhythm consistent and tunable.
Shape & shadows
Set the base radius via the borderRadius option (a number of px) —
createPrismTheme({ borderRadius: 8 }). It surfaces as theme.shape.borderRadius
on the resulting MUI theme. Shadows are generated for you:
shadows: [
'none',
'0 1px 2px rgba(0,0,0,0.05)',
'0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.06)',
// … 24 steps
]
customShadows: {
z1: '...', // softer cards
z4: '...', // floating panels
z8: '...', // modals
z16: '...', // popovers
primary: '...', // primary-tinted shadow
secondary: '...',
error: '...',
warning: '...',
info: '...',
success: '...',
}Use <Paper elevation={N}> for shadows; boxShadow:
'customShadows.z4' via sx for the custom variants.
Dark mode
<PrismProvider> reads the theme mode from the settings store, so it
takes no mode prop — seed the initial mode with defaultSettings, then
read and update it via useSettingsStore. There is no useColorMode
hook.
import { PrismProvider } from '@omnitron-dev/prism/core';
import { useSettingsStore } from '@omnitron-dev/prism';
function App() {
return (
<PrismProvider defaultSettings={{ mode: 'system' }}>
<Outlet />
</PrismProvider>
);
}
function ModeToggle() {
const mode = useSettingsStore((s) => s.mode);
const toggleMode = useSettingsStore((s) => s.toggleMode);
return (
<IconButton onClick={toggleMode}>
{mode === 'dark' ? <SunIcon /> : <MoonIcon />}
</IconButton>
);
}| Mode | Effect |
'light' | Force light |
'dark' | Force dark |
'system' | Track prefers-color-scheme |
mode is the stored preference; the provider resolves 'system' against
prefers-color-scheme internally before building the theme. State is
persisted via the settings store; survives reload; syncs across tabs.
(toggleMode flips light/dark; use setMode('system') to re-enable
system tracking.)
Flash-of-wrong-theme prevention (SSR)
Prism's CSS variables output (theme/css-variables.ts) emits
a stylesheet that applies the correct colour scheme before
React hydrates. Include in your HTML head:
<style id="prism-css-vars">
/* Generated by generateCssVariables(theme) at build time */
</style>Or use the Vite plugin (when one is configured) to inject it automatically.
Presets
Pre-tuned palettes for common aesthetics. Select one by name via the
preset option:
const theme = createPrismTheme({ preset: 'luxury' });Available presets (PRESET_NAMES): default-light, default-dark,
luxury, arctic, nature, ember, dracula, midnight, retro,
minimal. Non-default presets carry an inherent light/dark mode
(e.g. dracula/midnight/ember are dark; luxury/arctic/nature
are light) — selecting them forces that mode, while the two default-*
presets respect the mode option. Pick a preset, then layer
primaryColor / borderRadius / density on top.
Density
Three densities adjust spacing + typography + control sizes:
useSettingsStore.getState().setDensity('compact');
// 'compact' | 'standard' | 'comfortable'Tables, lists, forms, dialogs respond automatically. Useful for information-dense admin surfaces.
Mixins
Reusable style helpers (most are functions returning a CSSObject you
spread into sx):
import { mixins } from '@omnitron-dev/prism/theme';
<Box sx={mixins.textGradient('to right', '#7c4dff', '#00bcd4')}>Gradient text</Box>
<Box sx={mixins.bgBlur({ blur: 6 })}>Backdrop blur card</Box>
<Box sx={(theme) => mixins.scrollbarStyles(theme)}>Custom scrollbar</Box>Built-in mixins (keys on the mixins object):
| Mixin | Effect |
textGradient(direction, ...colors) | Gradient text fill |
bgGradient({ direction, colors, ... }) | Linear gradient background |
bgBlur({ blur, color }) | Backdrop blur |
borderGradient({ color, borderWidth, borderRadius }) | Gradient border |
maxLine({ lines, lineHeight }) | Multi-line truncation with ellipsis |
scrollbarStyles(theme, { autoHide: true }) | Native themed scrollbar; hover/focus reveal on fine pointers |
hideScrollX / hideScrollY | Hidden-scrollbar containers (objects) |
(Standalone scrollbar/ellipsis helpers are also exported by name:
customScrollbarMixin, hideScrollbarMixin, textEllipsisMixin,
multiLineEllipsisMixin, glassMixin, focusRingMixin.)
Components theme overrides
Pass raw MUI component overrides through the overrides option:
createPrismTheme({
overrides: {
components: {
MuiButton: {
defaultProps: { variant: 'contained', size: 'medium' },
styleOverrides: {
root: { borderRadius: 8, textTransform: 'none' },
},
},
MuiTextField: {
defaultProps: { variant: 'outlined', size: 'small' },
},
},
},
});Useful for enforcing project-wide conventions (e.g., "all buttons are contained, no uppercase").
CSS variables export
import { generateCssVariables } from '@omnitron-dev/prism/theme';
const css = generateCssVariables(theme);
// → :root { --palette-primary-main: #7c4dff; ... }Useful for:
Server-side colour-scheme cookie + SSR.
Sharing theme tokens with non-MUI elements (e.g., plain
<svg>strokes that should track the theme).Embedding in
<style>tag at the top of<head>to prevent flash.
Anti-patterns
Hard-coded hex colours in components. Use
theme.palette.X/sx={{ color: 'primary.main' }}so dark mode + theme switches work.pxeverywhere. Use spacing units; relativeremfor typography.Multiple
<ThemeProvider>s in the tree. Confused inheritance, duplicate snackbar hosts.Per-component
styled()for one-off colours. Prefersxfor one-offs;styled()for recurring patterns.Bypassing the settings store for mode persistence. Custom localStorage code drifts; use
useSettingsStore(mode/setMode/toggleMode).:not(:first-of-type)/:nth-of-typesibling selectors instyleOverrides. MUI v9 dropped the legacy of-type pattern in favour of explicit position classes (firstButton/middleButton/lastButtononToggleButtonGroup, similar tags on other grouped components). The of-type form silently misfires once consumers add wrapper elements or fragments; child-position (:first-child/:last-child/:nth-child) is the general-purpose replacement when no position class exists.forwardRefwrappers in component code. React 19 threadsrefthrough props automatically — declareref?: Ref<HTMLElement>on the props interface and use it directly. NoforwardRef, nodisplayNamefollow-up.
See also
Components catalog — components themed via these tokens
Layouts —
<DashboardLayout>reads densityHooks catalog —
useSettingsStore, theme-aware hooksMUI v9 theming docs — underlying foundation
Application icon families
PrismIconProvider from @omnitron-dev/prism/icons supplies a semantic icon set
to layouts, editors, graphs and MUI slots, including portals. Its icons prop
accepts a partial PrismIconSet; missing slots retain the parent or bundled icon.
Use Record<PrismIconName, PrismIconComponent> for compile-time coverage of every
slot, and prismIconNames to enumerate them. Supplied components accept
SvgIconProps, forward the SVG ref and preserve MUI accessibility semantics.
import { PrismIconProvider, type PrismIconSet } from '@omnitron-dev/prism/icons';
const icons = { search: ApplicationSearchIcon } satisfies PrismIconSet;
<PrismIconProvider icons={icons}>
<PrismThemeProvider theme={theme}>{children}</PrismThemeProvider>
</PrismIconProvider>;The application supplies and licenses its artwork. The provider does not fetch icons or replace rendered DOM. Component-level MUI slots and explicit icon props take precedence; nested providers inherit the outer set.