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
data-theme=“nostromo”Mother
Clinical, clean aesthetic - Cyan/teal for medical/healthcare
data-theme=“mother”LV-426
Warm, earthy aesthetic - Orange/amber for nature/outdoor
data-theme=“lv-426”Sulaco
Military, professional aesthetic - Blue/steel for corporate
data-theme=“sulaco”Theme Gallery
Try Themes
View Code
🎨 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 theme2. 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.