Theming & Styling

April 3, 2026 · View on GitHub

Storm's styling system has four layers — from low-level colors to high-level interaction identity:

  1. Theme — color palette with auto-generated shades, 11 presets, WCAG validation
  2. StyleSheet — CSS-like selectors with specificity cascading
  3. Personality — holistic interaction identity (colors + borders + animation + typography)
  4. Live Stylesheets.storm.css files with CSS variables and hot reload

Default Theme

The default palette uses Electric Arc Blue as the brand color:

import { colors } from "@orchetron/storm";

colors.brand.primary   // "#82AAFF" -- Electric Arc Blue
colors.brand.light     // "#A8C8FF" -- Lighter Arc (active states)
colors.brand.glow      // "#5A8AE0" -- Deeper Arc (subtle emphasis)

colors.text.primary    // "#D4D4D4" -- Clean light gray
colors.text.secondary  // "#808080" -- Mid gray
colors.text.dim        // "#505050" -- Quiet
colors.text.disabled   // "#333333" -- Near-invisible

colors.surface.base    // "#0A0A0A" -- Near-black
colors.surface.raised  // "#141414" -- Panels
colors.surface.overlay // "#1C1C1C" -- Modals
colors.surface.highlight // "#242424" -- Selection

colors.success  // "#34D399" -- Emerald
colors.warning  // "#FBBF24" -- Amber
colors.error    // "#F87171" -- Soft red
colors.info     // "#7AA2F7" -- Blue info

The full StormColors type includes semantic groups for syntax, diff, tool status, approval dialogs, input borders, thinking indicators, user/assistant/system roles, and divider color.

Built-in Presets

Storm ships 11 professionally curated themes: 7 branded palettes with distinct character, plus 4 utility presets.

Storm-Branded Themes

PresetBrand ColorCharacter
Arctic arcticTheme#5CB8C8 storm frostCool Nordic blue-grey, Scandinavian clarity
Midnight midnightTheme#6B9EF0 storm midnightDeep blue night, violet info tones
Ember emberTheme#6EB0A0 teal undercurrentWarm earth, toasted neutrals, amber glow
Mist mistTheme#78B0E8 gentle blueSoft pastels, lavender info, soothing
Voltage voltageTheme#A080E8 blue-violetVivid electric, maximum visual punch
Dusk duskTheme#B898D8 warm irisPurple twilight, sunset hues, contemplative
Horizon horizonTheme#2890C8 ocean bluePrecise blue-gold, clean and engineered

Utility Presets

PresetBrand ColorPurpose
Neon neonTheme#FFB800 pure goldHyper-saturated, maximum energy
Calm calmTheme#C49848 muted goldDesaturated warm tones, long sessions
High Contrast highContrastTheme#FFFFFFWCAG accessibility, color-blind friendly
Monochrome monochromeTheme#E0E0E0All greys, minimal distraction

ThemeProvider

Wrap your app with ThemeProvider to apply a theme:

import { render, ThemeProvider, midnightTheme } from "@orchetron/storm";

function App() {
  return (
    <ThemeProvider theme={midnightTheme}>
      <MyAppContent />
    </ThemeProvider>
  );
}

render(<App />);

Nest multiple providers to scope themes to subtrees.

useTheme()

Access the active theme in any component:

import { useTheme, Text, Box } from "@orchetron/storm";

function StatusBar() {
  const { colors, shades } = useTheme();

  return (
    <Box borderStyle="single" borderColor={colors.divider}>
      <Text color={colors.brand.primary} bold>Status: OK</Text>
      <Text color={shades.success.lighten1}>All systems go</Text>
    </Box>
  );
}

useTheme() returns a ThemeWithShades object with colors (the raw palette) and shades (auto-generated variants).

Auto-Generated Shades

For every semantic color, Storm generates six variants by mixing with white (lighten) or black (darken):

import { generateShades } from "@orchetron/storm";

const shades = generateShades("#82AAFF");
// shades.base     -- "#82AAFF" (original)
// shades.lighten1 -- +15% toward white
// shades.lighten2 -- +30% toward white
// shades.lighten3 -- +45% toward white
// shades.darken1  -- -15% toward black
// shades.darken2  -- -30% toward black
// shades.darken3  -- -45% toward black

Pre-computed shades for brand, success, warning, error, and info:

const { shades } = useTheme();

<Text color={shades.brand.lighten2}>Bright accent</Text>
<Text color={shades.error.darken1}>Subdued error</Text>

Creating Custom Themes

extendTheme()

Override specific properties while keeping everything else:

import { extendTheme, colors } from "@orchetron/storm";

const myTheme = extendTheme(colors, {
  brand: {
    primary: "#FF5F87",   // Rose
    light: "#FF87AF",
    glow: "#D7005F",
  },
  success: "#87D787",
});

createTheme()

Shorthand for extending the default palette:

import { createTheme } from "@orchetron/storm";

const myTheme = createTheme({
  brand: { primary: "#FF5F87" },
  surface: { base: "#1A0010" },
});

Runtime Theme Switching

Pass a new theme prop to <ThemeProvider> from a parent component that owns the state. Downstream useTheme() calls will pick up the change automatically:

import { useState } from "react";
import { ThemeProvider, useTheme, arcticTheme, voltageTheme, colors } from "@orchetron/storm";

function ThemePicker() {
  const [theme, setTheme] = useState(colors);

  return (
    <ThemeProvider theme={theme}>
      <Box flexDirection="row" gap={1}>
        <Button label="Arctic" onPress={() => setTheme(arcticTheme)} />
        <Button label="Voltage" onPress={() => setTheme(voltageTheme)} />
      </Box>
      <MyContent />
    </ThemeProvider>
  );
}

Loading Themes from Files

import { loadTheme, saveTheme, parseTheme, serializeTheme } from "@orchetron/storm";

// Load from JSON file (partial — deep-merged with defaults)
const theme = loadTheme("./my-theme.json");

// Parse from JSON string
const theme2 = parseTheme('{"brand":{"primary":"#FF5F87"}}');

// Save to file
saveTheme(myTheme, "./my-theme.json");

Theme Validation

import { validateTheme, validateContrast } from "@orchetron/storm";

// Check structure (valid hex, required fields)
const result = validateTheme(myTheme);
if (!result.valid) {
  for (const error of result.errors) {
    console.error(`${error.path}: ${error.message}`);
  }
}

// Audit WCAG contrast compliance (4.5:1 AA target)
const contrastResult = validateContrast(myTheme);

Personality System

A personality goes beyond theming — it defines the complete interaction identity: colors, borders, animation timing, typography, and component defaults in one coherent object.

import type { StormPersonality } from "@orchetron/storm";

Structure

interface StormPersonality {
  colors: StormColors;                    // Color palette (any theme)

  borders: {
    default: BorderStyle;                 // Normal state
    focused: BorderStyle;                 // Focused elements
    accent: BorderStyle;                  // Accent containers
    panel: BorderStyle;                   // Panel borders
  };

  animation: {
    durationFast: number;                 // Quick transitions (ms)
    durationNormal: number;               // Standard animations (ms)
    durationSlow: number;                 // Slow/emphasis (ms)
    easing: string;                       // Default easing
    reducedMotion: boolean;               // Respect prefers-reduced-motion
    spinnerType: string;                  // Default spinner variant
  };

  typography: {
    headingBold: boolean;                 // Bold headings
    headingColor: string;                 // Heading color
    codeBg: string;                       // Code block background
    linkColor: string;                    // Link color
    linkUnderline: boolean;               // Underline links
  };

  interaction: {
    focusIndicator: "bar" | "border" | "highlight" | "arrow";
    selectionChar: string;                // "◆" — selection indicator
    promptChar: string;                   // "›" — input prompt
    cursorStyle: string;                  // Cursor appearance
    collapseHint: string;                 // Collapse indicator
  };

  components: Record<string, Record<string, unknown>>;  // Per-component defaults
}

Built-in Personality Presets

PresetCharacter
defaultElectric Arc Blue, single borders, diamond spinner, standard timing
minimalSame colors, single borders, no animations, dots spinner
hackerGreen (#00FF00), ASCII borders, fast animations, braille spinner
playfulPink (#FF6B9D), round borders, bouncy animations, bounce spinner

usePersonality()

Components read the active personality for defaults:

import { usePersonality } from "@orchetron/storm";

function MyComponent() {
  const personality = usePersonality();
  const spinnerType = personality.animation.spinnerType;   // "diamond"
  const promptChar = personality.interaction.promptChar;    // "›"
  const selectionChar = personality.interaction.selectionChar; // "◆"

  return <Spinner type={spinnerType} />;
}

StyleSheet (CSS-like Selectors)

Storm supports CSS-like stylesheets with selector specificity:

import { createStyleSheet } from "@orchetron/storm";

const styles = createStyleSheet({
  "Text.title": { bold: true, color: "#82AAFF" },
  "Button:focus": { inverse: true, borderColor: "#FFB800" },
  "Box.sidebar Text": { dim: true },           // Descendant combinator
  "#submit": { color: "#34D399", bold: true },  // ID selector
});

Specificity scoring: ID=100, pseudo-class/class=10, type=1.


Live Stylesheets (.storm.css)

Load stylesheets from files with hot reload — edit the file and see changes instantly:

import { useStyleSheet } from "@orchetron/storm";

function App() {
  useStyleSheet({ path: "./app.storm.css", watch: true });
  return <Box className="sidebar">...</Box>;
}

.storm.css Format

/* CSS variables */
:root {
  --primary: #82AAFF;
  --accent: #FFB800;
  --surface: #1E1E2E;
}

Text.title {
  color: var(--primary);
  bold: true;
}

Button:focus {
  borderColor: var(--accent);
  inverse: true;
}

Box.card {
  backgroundColor: var(--surface);
  borderStyle: single;
}

/* Fallback values */
Text.subtitle {
  color: var(--secondary, #888888);
}

Supports:

  • CSS-like block syntax with selectors
  • :root { --name: value; } variable declarations
  • var(--name) and var(--name, fallback) references
  • Nested var() in fallbacks
  • Block comments (/* */) and line comments (//)
  • Auto-parsed values: numbers, booleans, percentages, hex colors, quoted strings
  • File watching with 100ms debounce (auto-enabled in development)

Available Style Properties

These are the properties you can use in .storm.css rules. Values are auto-parsed from strings: true/false become booleans, integer/float strings become numbers, percentages are preserved as strings, and everything else stays as a string.

PropertyTypeApplies to
colorhex string or numberText, inline components
backgroundColorhex string or numberBox, containers
boldbooleanText
dimbooleanText
italicbooleanText
underlinebooleanText
strikethroughbooleanText
inversebooleanText, Button
borderStylesingle, double, round, bold, classicBox, containers
borderColorhex string or numberBox, containers
paddingnumberContainers
paddingX / paddingYnumberContainers
marginnumberLayout components
marginX / marginYnumberLayout components
gapnumberFlex containers
flexnumberFlex children
flexDirectioncolumn, rowBox
alignItemsflex-start, center, flex-end, stretchBox
justifyContentflex-start, center, flex-end, space-between, space-around, space-evenlyBox

Selector Types

Selectors follow CSS conventions adapted for Storm components:

SelectorExampleSpecificityMatches
TypeText1All Text components
Class.title10Components with className="title"
Type + classText.title11Text components with className="title"
Pseudo-classButton:focus11Focused Button components
ID#submit100Component with id="submit"
DescendantBox.sidebar Text12Text inside a Box with className "sidebar"

Higher specificity wins when multiple rules match the same element.

Complete .storm.css Example

Here is a full example showing a dashboard stylesheet:

/* dashboard.storm.css */

:root {
  /* Theme variables */
  --brand: #82AAFF;
  --brand-light: #A8C8FF;
  --surface: #141414;
  --surface-raised: #1C1C1C;
  --dim: #505050;

  /* Storm theme overrides (picked up by useStyleSheet themeOverrides) */
  --storm-brand-primary: #82AAFF;
  --storm-surface-base: #0A0A0A;
  --storm-success: #34D399;
}

// Global defaults
Box {
  padding: 0;
}

Text {
  color: #D4D4D4;
}

// Title styling
Text.title {
  color: var(--brand);
  bold: true;
}

Text.subtitle {
  color: var(--dim);
  dim: true;
}

// Sidebar layout
Box.sidebar {
  borderStyle: single;
  borderColor: var(--dim);
  backgroundColor: var(--surface);
  padding: 1;
}

Box.sidebar Text {
  dim: true;
}

// Cards
Box.card {
  borderStyle: round;
  borderColor: var(--brand);
  backgroundColor: var(--surface-raised);
  padding: 2;
  margin: 1;
}

// Interactive states
Button:focus {
  inverse: true;
  borderColor: var(--brand-light);
}

#submit {
  color: #34D399;
  bold: true;
}

Theme Overrides via CSS Variables

Variables named --storm-{group}-{key} are automatically extracted and can be passed to ThemeProvider, bridging the gap between .storm.css files and the theme system:

import { useStyleSheet, extendTheme, ThemeProvider, colors } from "@orchetron/storm";

function App() {
  const { themeOverrides } = useStyleSheet({ path: "./app.storm.css", watch: true });
  const mergedTheme = extendTheme(colors, themeOverrides);

  return (
    <ThemeProvider theme={mergedTheme}>
      <MyContent />
    </ThemeProvider>
  );
}

The naming convention for theme variables:

  • Flat fields: --storm-success, --storm-warning, --storm-error, --storm-info, --storm-divider
  • Nested fields: --storm-brand-primary, --storm-text-dim, --storm-surface-base, --storm-input-border-active (hyphens after the group name are converted to camelCase)

Valid group names: brand, text, surface, system, user, assistant, thinking, tool, approval, input, diff, syntax.


Spacing Tokens

Consistent padding, margin, and gap values:

import { spacing } from "@orchetron/storm";

spacing.none  // 0
spacing.xs    // 1 — Tight: icon-to-label
spacing.sm    // 1 — Standard: within components
spacing.md    // 2 — Comfortable: between components
spacing.lg    // 3 — Spacious: between sections
spacing.xl    // 4 — Major: page-level separation

Theme Color Reference

GroupKeysPurpose
brandprimary, light, glowAccent color and variants
textprimary, secondary, dim, disabledText hierarchy
surfacebase, raised, overlay, highlightBackground levels
success / warning / error / info(string)Semantic status
usersymbolUser identity indicator
assistantsymbolAI assistant indicator
systemtextSystem message color
thinkingsymbol, shimmerThinking/loading state
toolpending, running, completed, failed, cancelledTool execution status
approvalapprove, deny, always, header, borderApproval dialog
inputborder, borderActive, promptInput field states
diffadded, removed, addedBg, removedBgDiff highlighting
syntaxkeyword, string, number, function, type, comment, operatorSyntax highlighting
divider(string)Divider/separator lines