Storm's styling system has four layers — from low-level colors to high-level interaction identity:
- Theme — color palette with auto-generated shades, 11 presets, WCAG validation
- StyleSheet — CSS-like selectors with specificity cascading
- Personality — holistic interaction identity (colors + borders + animation + typography)
- Live Stylesheets —
.storm.cssfiles with CSS variables and hot reload
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 infoThe full StormColors type includes semantic groups for syntax, diff, tool status, approval dialogs, input borders, thinking indicators, user/assistant/system roles, and divider color.
Storm ships 11 professionally curated themes: 7 branded palettes with distinct character, plus 4 utility presets.
| Preset | Brand Color | Character |
|---|---|---|
Arctic arcticTheme |
#5CB8C8 storm frost |
Cool Nordic blue-grey, Scandinavian clarity |
Midnight midnightTheme |
#6B9EF0 storm midnight |
Deep blue night, violet info tones |
Ember emberTheme |
#6EB0A0 teal undercurrent |
Warm earth, toasted neutrals, amber glow |
Mist mistTheme |
#78B0E8 gentle blue |
Soft pastels, lavender info, soothing |
Voltage voltageTheme |
#A080E8 blue-violet |
Vivid electric, maximum visual punch |
Dusk duskTheme |
#B898D8 warm iris |
Purple twilight, sunset hues, contemplative |
Horizon horizonTheme |
#2890C8 ocean blue |
Precise blue-gold, clean and engineered |
| Preset | Brand Color | Purpose |
|---|---|---|
Neon neonTheme |
#FFB800 pure gold |
Hyper-saturated, maximum energy |
Calm calmTheme |
#C49848 muted gold |
Desaturated warm tones, long sessions |
High Contrast highContrastTheme |
#FFFFFF |
WCAG accessibility, color-blind friendly |
Monochrome monochromeTheme |
#E0E0E0 |
All greys, minimal distraction |
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.
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).
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 blackPre-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>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",
});Shorthand for extending the default palette:
import { createTheme } from "@orchetron/storm";
const myTheme = createTheme({
brand: { primary: "#FF5F87" },
surface: { base: "#1A0010" },
});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>
);
}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");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);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";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
}| Preset | Character |
|---|---|
| default | Electric Arc Blue, single borders, diamond spinner, standard timing |
| minimal | Same colors, single borders, no animations, dots spinner |
| hacker | Green (#00FF00), ASCII borders, fast animations, braille spinner |
| playful | Pink (#FF6B9D), round borders, bouncy animations, bounce spinner |
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} />;
}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.
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>;
}/* 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 declarationsvar(--name)andvar(--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)
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.
| Property | Type | Applies to |
|---|---|---|
color |
hex string or number | Text, inline components |
backgroundColor |
hex string or number | Box, containers |
bold |
boolean | Text |
dim |
boolean | Text |
italic |
boolean | Text |
underline |
boolean | Text |
strikethrough |
boolean | Text |
inverse |
boolean | Text, Button |
borderStyle |
single, double, round, bold, classic |
Box, containers |
borderColor |
hex string or number | Box, containers |
padding |
number | Containers |
paddingX / paddingY |
number | Containers |
margin |
number | Layout components |
marginX / marginY |
number | Layout components |
gap |
number | Flex containers |
flex |
number | Flex children |
flexDirection |
column, row |
Box |
alignItems |
flex-start, center, flex-end, stretch |
Box |
justifyContent |
flex-start, center, flex-end, space-between, space-around, space-evenly |
Box |
Selectors follow CSS conventions adapted for Storm components:
| Selector | Example | Specificity | Matches |
|---|---|---|---|
| Type | Text |
1 | All Text components |
| Class | .title |
10 | Components with className="title" |
| Type + class | Text.title |
11 | Text components with className="title" |
| Pseudo-class | Button:focus |
11 | Focused Button components |
| ID | #submit |
100 | Component with id="submit" |
| Descendant | Box.sidebar Text |
12 | Text inside a Box with className "sidebar" |
Higher specificity wins when multiple rules match the same element.
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;
}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.
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| Group | Keys | Purpose |
|---|---|---|
brand |
primary, light, glow |
Accent color and variants |
text |
primary, secondary, dim, disabled |
Text hierarchy |
surface |
base, raised, overlay, highlight |
Background levels |
success / warning / error / info |
(string) | Semantic status |
user |
symbol |
User identity indicator |
assistant |
symbol |
AI assistant indicator |
system |
text |
System message color |
thinking |
symbol, shimmer |
Thinking/loading state |
tool |
pending, running, completed, failed, cancelled |
Tool execution status |
approval |
approve, deny, always, header, border |
Approval dialog |
input |
border, borderActive, prompt |
Input field states |
diff |
added, removed, addedBg, removedBg |
Diff highlighting |
syntax |
keyword, string, number, function, type, comment, operator |
Syntax highlighting |
divider |
(string) | Divider/separator lines |