Skip to Content
Theming

Theming & Design Tokens

Nostromo UI’s theming system is built around CSS variables in HSL format that integrate directly with Tailwind CSS. This provides maximum flexibility and performance without runtime overhead.

💡 Live Examples: The four themes and their token values are documented under Predefined Themes below. This site itself renders in Nostromo; you pick a theme in your own app by importing its stylesheet, as shown in Quick Start.

🎨 Predefined Themes

Nostromo UI includes 4 complete themes, each with a unique aesthetic:

Nostromo

Default theme - Purple/blue inspired by space and technology

Primary: Purple/Blue
Neutral: Industrial grays
data-theme=“nostromo”

Mother

Clinical, clean aesthetic - Cyan/teal for medical/healthcare

Primary: Cyan/Teal
Neutral: Clean whites
data-theme=“mother”

LV-426

Warm, earthy aesthetic - Orange/amber for nature/outdoor

Primary: Orange/Amber
Neutral: Warm grays
data-theme=“lv-426”

Sulaco

Military, professional aesthetic - Blue/steel for corporate

Primary: Blue/Steel
Neutral: Cool grays
data-theme=“sulaco”
Primary: Purple/Blue
Neutral: Industrial grays
Light
Dark
Primary: Cyan/Teal

Neutral: Clean whites

Light
Dark

Primary: Orange/Amber

Neutral: Warm grays
Light
Dark
Primary: Blue/Steel
Neutral: Cool grays
Light
Dark

Try Themes

Live Example
☀️
Copy
View Code
function ThemeDemo() { const [theme, setTheme] = React.useState('nostromo'); const themes = [ { id: 'nostromo', name: 'Nostromo', color: 'hsl(262 84% 52%)' }, { id: 'mother', name: 'Mother', color: 'hsl(195 100% 50%)' }, { id: 'lv-426', name: 'LV-426', color: 'hsl(25 95% 53%)' }, { id: 'sulaco', name: 'Sulaco', color: 'hsl(220 100% 50%)' }, ];...

🎨 Quick Start

1. Import Base CSS

// In your entry file (e.g. main.tsx) import "@jarllyng/nostromo/tailwind.css"; import "@jarllyng/nostromo/themes/nostromo.css"; // choose or customize theme

2. Apply Theme

<html data-theme="nostromo" data-color-scheme="light"> <!-- Your content --> </html>

3. Customize Brand Colors

[data-theme="mybrand"] { /* Only change what you need */ --nostromo-color-brand-500: 220 100% 50%; /* Your brand blue */ --nostromo-color-brand-600: 220 100% 40%; /* Darker variant */ --nostromo-color-brand-700: 220 100% 30%; /* Even darker */ }

🎯 Design Tokens

Color System

All colors use HSL format for easy manipulation:

[data-theme="nostromo"] { /* Brand colors */ --nostromo-color-brand-50: 262 84% 95%; --nostromo-color-brand-100: 262 84% 90%; --nostromo-color-brand-200: 262 84% 80%; --nostromo-color-brand-300: 262 84% 70%; --nostromo-color-brand-400: 262 84% 60%; --nostromo-color-brand-500: 262 84% 52%; /* Primary brand */ --nostromo-color-brand-600: 262 84% 45%; --nostromo-color-brand-700: 262 84% 35%; --nostromo-color-brand-800: 262 84% 25%; --nostromo-color-brand-900: 262 84% 15%; --nostromo-color-brand-950: 262 84% 8%; /* Neutral colors */ --nostromo-color-neutral-50: 0 0% 98%; --nostromo-color-neutral-100: 0 0% 96%; --nostromo-color-neutral-200: 0 0% 90%; --nostromo-color-neutral-300: 0 0% 83%; --nostromo-color-neutral-400: 0 0% 64%; --nostromo-color-neutral-500: 0 0% 45%; --nostromo-color-neutral-600: 0 0% 32%; --nostromo-color-neutral-700: 0 0% 25%; --nostromo-color-neutral-800: 0 0% 15%; --nostromo-color-neutral-900: 0 0% 9%; --nostromo-color-neutral-950: 0 0% 4%; /* Success Colors - Full scale (50-950) */ --nostromo-color-success-50: 142 76% 95%; --nostromo-color-success-100: 142 76% 90%; --nostromo-color-success-200: 142 76% 80%; --nostromo-color-success-300: 142 76% 70%; --nostromo-color-success-400: 142 76% 60%; --nostromo-color-success-500: 142 76% 36%; --nostromo-color-success-600: 142 76% 30%; --nostromo-color-success-700: 142 76% 25%; --nostromo-color-success-800: 142 76% 20%; --nostromo-color-success-900: 142 76% 15%; --nostromo-color-success-950: 142 76% 8%; /* Warning Colors - Full scale (50-950) */ --nostromo-color-warning-50: 38 92% 95%; --nostromo-color-warning-100: 38 92% 90%; --nostromo-color-warning-200: 38 92% 80%; --nostromo-color-warning-300: 38 92% 70%; --nostromo-color-warning-400: 38 92% 60%; --nostromo-color-warning-500: 38 92% 50%; --nostromo-color-warning-600: 38 92% 45%; --nostromo-color-warning-700: 38 92% 35%; --nostromo-color-warning-800: 38 92% 25%; --nostromo-color-warning-900: 38 92% 15%; --nostromo-color-warning-950: 38 92% 8%; /* Error Colors - Full scale (50-950) */ --nostromo-color-error-50: 0 84% 95%; --nostromo-color-error-100: 0 84% 90%; --nostromo-color-error-200: 0 84% 80%; --nostromo-color-error-300: 0 84% 70%; --nostromo-color-error-400: 0 84% 60%; --nostromo-color-error-500: 0 84% 60%; --nostromo-color-error-600: 0 84% 50%; --nostromo-color-error-700: 0 84% 40%; --nostromo-color-error-800: 0 84% 30%; --nostromo-color-error-900: 0 84% 20%; --nostromo-color-error-950: 0 84% 10%; /* Info Colors - Full scale (50-950) */ --nostromo-color-info-50: 199 89% 95%; --nostromo-color-info-100: 199 89% 90%; --nostromo-color-info-200: 199 89% 80%; --nostromo-color-info-300: 199 89% 70%; --nostromo-color-info-400: 199 89% 60%; --nostromo-color-info-500: 199 89% 48%; --nostromo-color-info-600: 199 89% 40%; --nostromo-color-info-700: 199 89% 30%; --nostromo-color-info-800: 199 89% 20%; --nostromo-color-info-900: 199 89% 15%; --nostromo-color-info-950: 199 89% 8%; /* Semantic tokens for component theming (WCAG AA validated) */ --nostromo-color-success-foreground: hsl(var(--nostromo-color-neutral-50)); --nostromo-color-warning-foreground: hsl(var(--nostromo-color-neutral-900)); --nostromo-color-error-foreground: hsl(var(--nostromo-color-neutral-50)); --nostromo-color-info-foreground: hsl(var(--nostromo-color-neutral-50)); }

Spacing & Sizing

[data-theme="nostromo"] { /* Spacing scale */ --nostromo-spacing-xs: 0.25rem; /* 4px */ --nostromo-spacing-sm: 0.5rem; /* 8px */ --nostromo-spacing-md: 1rem; /* 16px */ --nostromo-spacing-lg: 1.5rem; /* 24px */ --nostromo-spacing-xl: 2rem; /* 32px */ --nostromo-spacing-2xl: 3rem; /* 48px */ --nostromo-spacing-3xl: 4rem; /* 64px */ /* Border radius */ --nostromo-radius-none: 0px; --nostromo-radius-sm: 0.25rem; /* 4px */ --nostromo-radius-md: 0.5rem; /* 8px */ --nostromo-radius-lg: 0.75rem; /* 12px */ --nostromo-radius-xl: 1rem; /* 16px */ --nostromo-radius-full: 9999px; /* Shadows */ --nostromo-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); --nostromo-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); --nostromo-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); --nostromo-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1); }

Typography

[data-theme="nostromo"] { /* Font families */ --nostromo-font-heading: "Inter", system-ui, sans-serif; --nostromo-font-body: "Inter", system-ui, sans-serif; --nostromo-font-mono: "JetBrains Mono", "Fira Code", monospace; /* Font sizes */ --nostromo-text-xs: 0.75rem; /* 12px */ --nostromo-text-sm: 0.875rem; /* 14px */ --nostromo-text-base: 1rem; /* 16px */ --nostromo-text-lg: 1.125rem; /* 18px */ --nostromo-text-xl: 1.25rem; /* 20px */ --nostromo-text-2xl: 1.5rem; /* 24px */ --nostromo-text-3xl: 1.875rem; /* 30px */ --nostromo-text-4xl: 2.25rem; /* 36px */ --nostromo-text-5xl: 3rem; /* 48px */ /* Line heights */ --nostromo-leading-tight: 1.25; --nostromo-leading-normal: 1.5; --nostromo-leading-relaxed: 1.75; /* Font weights */ --nostromo-font-normal: 400; --nostromo-font-medium: 500; --nostromo-font-semibold: 600; --nostromo-font-bold: 700; }

🌙 Dark Mode

System-based Dark Mode

@media (prefers-color-scheme: dark) { [data-theme="nostromo"] { --nostromo-color-neutral-50: 0 0% 9%; --nostromo-color-neutral-900: 0 0% 98%; } }

Manual Dark Mode Toggle

[data-theme="nostromo"][data-color-scheme="dark"] { --nostromo-color-neutral-50: 0 0% 9%; --nostromo-color-neutral-900: 0 0% 98%; --nostromo-color-brand-500: 262 84% 60%; /* Lighter brand for contrast */ }
// React hook for dark mode function useDarkMode() { const [isDark, setIsDark] = useState(false); useEffect(() => { const root = document.documentElement; root.setAttribute("data-color-scheme", isDark ? "dark" : "light"); }, [isDark]); return [isDark, setIsDark]; }

🎨 Predefined Themes

Nostromo (Default)

[data-theme="nostromo"] { --nostromo-color-brand-500: 262 84% 52%; /* Purple */ --nostromo-color-neutral-900: 0 0% 9%; /* Dark background */ --nostromo-radius-md: 0.5rem; --nostromo-font-heading: "Inter", sans-serif; }

Mother

[data-theme="mother"] { --nostromo-color-brand-500: 200 100% 50%; /* Cyan */ --nostromo-color-neutral-900: 220 13% 9%; /* Dark blue-gray */ --nostromo-radius-sm: 0.25rem; --nostromo-font-heading: "Inter", sans-serif; }

LV-426

[data-theme="lv-426"] { --nostromo-color-brand-500: 25 95% 53%; /* Orange */ --nostromo-color-neutral-900: 0 0% 8%; /* Very dark */ --nostromo-radius-lg: 0.75rem; --nostromo-font-heading: "Inter", sans-serif; }

Sulaco

[data-theme="sulaco"] { --nostromo-color-brand-500: 210 40% 50%; /* Blue */ --nostromo-color-neutral-900: 0 0% 10%; /* Dark */ --nostromo-radius-md: 0.5rem; --nostromo-font-heading: "Inter", sans-serif; }

🛠️ Custom Theming

Create Your Own Theme

[data-theme="mybrand"] { /* Brand colors - only change what you need */ --nostromo-color-brand-500: 220 100% 50%; /* Your brand blue */ --nostromo-color-brand-600: 220 100% 40%; /* Darker variant */ --nostromo-color-brand-700: 220 100% 30%; /* Even darker */ /* Typography */ --nostromo-font-heading: "Poppins", sans-serif; --nostromo-font-body: "Inter", sans-serif; /* Styling */ --nostromo-radius-md: 0.75rem; }

Apply Theme

<html data-theme="mybrand"> <!-- Your content --> </html>

Dynamic Theme Switching

// React example function ThemeToggle() { const [theme, setTheme] = useState("nostromo"); const toggleTheme = () => { const newTheme = theme === "nostromo" ? "mother" : "nostromo"; setTheme(newTheme); document.documentElement.setAttribute("data-theme", newTheme); }; return ( <button onClick={toggleTheme}> Switch to {theme === "nostromo" ? "Mother" : "Nostromo"} </button> ); }

♿ Accessibility

Contrast Guidelines

All colors are designed to meet WCAG 2.1 AA standards:

[data-theme="nostromo"] { /* Brand colors - validated contrast */ --nostromo-color-brand-500: 262 84% 52%; /* 4.5:1 contrast on white */ --nostromo-color-brand-600: 262 84% 45%; /* 7:1 contrast on white */ /* Neutral colors - safe readability */ --nostromo-color-neutral-900: 0 0% 9%; /* 21:1 contrast on white */ --nostromo-color-neutral-700: 0 0% 25%; /* 12:1 contrast on white */ }

Focus States

/* Automatic focus states */ .focus-visible { outline: 2px solid hsl(var(--nostromo-color-brand-500)); outline-offset: 2px; }

🚀 Performance

Bundle Size Optimization

// ✅ Recommended: Per-component imports (smallest bundle) import { Button } from "@jarllyng/nostromo/button"; import { Input } from "@jarllyng/nostromo/input"; // ✅ Also OK: Barrel imports import { Button, Input } from "@jarllyng/nostromo"; // ❌ Avoid: Full library import import * as Nostromo from "@jarllyng/nostromo";

Tailwind Configuration

Nothing to configure. Two imports are the entire setup, and Tailwind v4 only emits the utilities actually used, so there is no purge step either.

@import "@jarllyng/nostromo/tailwind.css"; @import "@jarllyng/nostromo/themes/nostromo.css";

📚 Form Integration

React Hook Form + Zod

import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Input, HelperText, ErrorMessage } from "@jarllyng/nostromo"; const schema = z.object({ email: z.string().email("Invalid email"), password: z.string().min(8, "Password must be at least 8 characters"), }); function LoginForm() { const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(schema), }); return ( <form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <div> <Input {...register("email")} placeholder="Email" className={errors.email ? "border-error-500" : ""} /> {errors.email && <ErrorMessage>{errors.email.message}</ErrorMessage>} </div> <div> <Input {...register("password")} type="password" placeholder="Password" className={errors.password ? "border-error-500" : ""} /> {errors.password && ( <ErrorMessage>{errors.password.message}</ErrorMessage> )} <HelperText>Password must be at least 8 characters</HelperText> </div> </form> ); }

🎯 Live Examples

Theme Playground

// Live theme switcher - try different themes function ThemePlayground() { const themes = ["nostromo", "mother", "lv-426", "sulaco"]; const [currentTheme, setCurrentTheme] = useState("nostromo"); return ( <div className="space-y-4"> <select value={currentTheme} onChange={(e) => setCurrentTheme(e.target.value)} className="px-3 py-2 border rounded-md" > {themes.map((theme) => ( <option key={theme} value={theme}> {theme} </option> ))} </select> <div data-theme={currentTheme} className="p-4 border rounded-lg"> <Button>Test Button</Button> <Input placeholder="Test Input" /> </div> </div> ); }

This theming system gives you maximum flexibility to create consistent, performant, and beautiful user interfaces.

Last updated on