Documentation/frontend/Theme
In this section

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 createPrismTheme directly — <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: 8px

Convention: 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>
  );
}
ModeEffect
'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):

MixinEffect
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 / hideScrollYHidden-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.

  • px everywhere. Use spacing units; relative rem for typography.

  • Multiple <ThemeProvider>s in the tree. Confused inheritance, duplicate snackbar hosts.

  • Per-component styled() for one-off colours. Prefer sx for 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-type sibling selectors in styleOverrides. MUI v9 dropped the legacy of-type pattern in favour of explicit position classes (firstButton / middleButton / lastButton on ToggleButtonGroup, 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.

  • forwardRef wrappers in component code. React 19 threads ref through props automatically — declare ref?: Ref<HTMLElement> on the props interface and use it directly. No forwardRef, no displayName follow-up.

See also

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.