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.

🎨 Quick Start

1. Import Base CSS

// In your entry file (e.g. main.tsx)
import "@nostromo/ui-tw/styles/base.css";
import "@nostromo/ui-tw/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 */
  --color-brand-500: 220 100% 50%;  /* Your brand blue */
  --color-brand-600: 220 100% 40%;  /* Darker variant */
  --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 */
  --color-brand-50: 262 84% 95%;
  --color-brand-100: 262 84% 90%;
  --color-brand-200: 262 84% 80%;
  --color-brand-300: 262 84% 70%;
  --color-brand-400: 262 84% 60%;
  --color-brand-500: 262 84% 52%;  /* Primary brand */
  --color-brand-600: 262 84% 45%;
  --color-brand-700: 262 84% 35%;
  --color-brand-800: 262 84% 25%;
  --color-brand-900: 262 84% 15%;
  --color-brand-950: 262 84% 8%;
 
  /* Neutral colors */
  --color-neutral-50: 0 0% 98%;
  --color-neutral-100: 0 0% 96%;
  --color-neutral-200: 0 0% 90%;
  --color-neutral-300: 0 0% 83%;
  --color-neutral-400: 0 0% 64%;
  --color-neutral-500: 0 0% 45%;
  --color-neutral-600: 0 0% 32%;
  --color-neutral-700: 0 0% 25%;
  --color-neutral-800: 0 0% 15%;
  --color-neutral-900: 0 0% 9%;
  --color-neutral-950: 0 0% 4%;
 
  /* Semantic colors */
  --color-success-500: 142 76% 36%;
  --color-warning-500: 38 92% 50%;
  --color-error-500: 0 84% 60%;
  --color-info-500: 199 89% 48%;
}

Spacing & Sizing

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

Typography

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

🌙 Dark Mode

System-based Dark Mode

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

Manual Dark Mode Toggle

[data-theme="nostromo"][data-color-scheme="dark"] {
  --color-neutral-50: 0 0% 9%;
  --color-neutral-900: 0 0% 98%;
  --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"] {
  --color-brand-500: 262 84% 52%;  /* Purple */
  --color-neutral-900: 0 0% 9%;    /* Dark background */
  --radius-md: 0.5rem;
  --font-heading: "Inter", sans-serif;
}

Mother

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

LV-426

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

Sulaco

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

🛠️ Custom Theming

Create Your Own Theme

[data-theme="mybrand"] {
  /* Brand colors - only change what you need */
  --color-brand-500: 220 100% 50%;  /* Your brand blue */
  --color-brand-600: 220 100% 40%;  /* Darker variant */
  --color-brand-700: 220 100% 30%;  /* Even darker */
  
  /* Typography */
  --font-heading: "Poppins", sans-serif;
  --font-body: "Inter", sans-serif;
  
  /* Styling */
  --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 */
  --color-brand-500: 262 84% 52%;  /* 4.5:1 contrast on white */
  --color-brand-600: 262 84% 45%;  /* 7:1 contrast on white */
  
  /* Neutral colors - safe readability */
  --color-neutral-900: 0 0% 9%;    /* 21:1 contrast on white */
  --color-neutral-700: 0 0% 25%;  /* 12:1 contrast on white */
}

Focus States

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

🚀 Performance

Bundle Size Optimization

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

Tailwind Configuration

// tailwind.config.js
const nostromoPreset = require("@nostromo/ui-tw/tailwind.preset.js");
 
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
    "./node_modules/@nostromo/**/*.{js,ts,jsx,tsx}"
  ],
  presets: [nostromoPreset],
  // Purge unused CSS
  purge: {
    enabled: true,
    content: ['./src/**/*.{js,ts,jsx,tsx}'],
  }
};

📚 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 '@nostromo/ui-core';
 
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.