This document describes the complete visual language of SMUI (SpaceMolt UI) -- a terminal theme for shadcn/ui with light and dark modes. Use this as a reference when building interfaces that match the SMUI aesthetic.
SMUI is inspired by the bridge terminals of starships in science fiction. Every design decision stems from three principles:
- Terminal-grade readability -- Monospace type, high-contrast text on dark surfaces, uppercase labels for scanability
- Utilitarian precision -- No decorative border radius, no gradients, no shadows. Everything has hard edges and clear boundaries
- Status at a glance -- A five-color aurora palette makes system states immediately recognizable
The theme supports both light and dark modes. Light mode uses Nord's Snow Storm palette, dark mode uses Polar Night. Extended colors (frost, aurora, surfaces) are contrast-adjusted per mode.
These are the standard shadcn semantic tokens, mapped to the SMUI palette:
| Variable | HSL | Hex | Usage |
|---|---|---|---|
--background |
213 16% 12% |
#1a1e24 |
Page background |
--foreground |
213 27% 88% |
#d8dee9 |
Primary text |
--card |
217 16% 15.5% |
#21262e |
Card/panel backgrounds |
--card-foreground |
213 27% 88% |
#d8dee9 |
Card text |
--primary |
193 44% 67% |
#88c0d0 |
Primary accent (frost blue) |
--primary-foreground |
213 16% 12% |
#1a1e24 |
Text on primary |
--secondary |
216 15% 19% |
#282e37 |
Secondary surfaces |
--secondary-foreground |
213 27% 88% |
#d8dee9 |
Text on secondary |
--muted |
216 15% 19% |
#282e37 |
Muted backgrounds |
--muted-foreground |
213 14% 65% |
#8e99a8 |
Muted/secondary text |
--accent |
193 10% 16% |
#242c30 |
Accent hover backgrounds |
--accent-foreground |
193 44% 67% |
#88c0d0 |
Accent text |
--destructive |
355 52% 64% |
#d4737c |
Error/danger |
--border |
217 17% 28% |
#3b4252 |
Borders |
--input |
217 17% 28% |
#3b4252 |
Input borders |
--ring |
193 44% 67% |
#88c0d0 |
Focus rings |
--radius |
0rem |
-- | No border radius |
Beyond the standard shadcn variables, SMUI defines additional CSS custom properties. These are stored as raw HSL triplets so they can be used with alpha values: hsl(var(--smui-frost-2) / 0.3).
| Variable | HSL | Hex | Usage |
|---|---|---|---|
--smui-frost-1 |
176 25% 65% |
#8fbcbb |
Teal accent |
--smui-frost-2 |
193 44% 67% |
#88c0d0 |
Primary frost blue (= --primary) |
--smui-frost-3 |
210 34% 63% |
#81a1c1 |
Steel blue |
--smui-frost-4 |
213 32% 52% |
#5e81ac |
Deep blue |
These are the core semantic status colors. Use them consistently throughout your UI:
| Variable | HSL | Hex | Meaning |
|---|---|---|---|
--smui-green |
92 28% 65% |
#a3be8c |
Success, online, nominal, positive change |
--smui-yellow |
40 71% 73% |
#ebcb8b |
Warning, standby, caution |
--smui-orange |
14 51% 63% |
#d08770 |
Alert, degraded |
--smui-red |
355 52% 64% |
#d4737c |
Critical, error, danger, destructive |
--smui-purple |
311 24% 63% |
#b48ead |
Info, special, rare |
Four background depth levels for creating visual layering:
| Variable | HSL | Hex | Usage |
|---|---|---|---|
--smui-surface-0 |
213 16% 12% |
#1a1e24 |
Page background (= --background) |
--smui-surface-1 |
217 16% 15.5% |
#21262e |
Cards, panels (= --card) |
--smui-surface-2 |
216 15% 19% |
#282e37 |
Elevated elements, dropdowns |
--smui-surface-3 |
215 14% 22% |
#2f3640 |
Highlights, active states |
| Variable | HSL | Hex | Usage |
|---|---|---|---|
--smui-border-hover |
216 12% 37% |
#4c566a |
Border color on hover |
In light mode (:root), colors are adjusted for contrast against Snow Storm backgrounds:
| Variable | HSL | Hex | Usage |
|---|---|---|---|
--background |
218 27% 94% |
#eceff4 |
Page background (Snow Storm 3) |
--foreground |
220 16% 22% |
#2e3440 |
Primary text (Polar Night 1) |
--card |
218 27% 92% |
#e5e9f0 |
Card/panel backgrounds (Snow Storm 2) |
--primary |
213 32% 44% |
#4c6d94 |
Primary accent (Frost 4, darkened for WCAG contrast) |
--muted-foreground |
220 17% 36% |
#4c566a |
Muted text (Polar Night 4) |
--border |
219 18% 80% |
#c9cfda |
Borders |
| Variable | HSL | Hex | Notes |
|---|---|---|---|
--smui-frost-1 |
176 30% 40% |
#478c89 |
Darkened teal |
--smui-frost-2 |
193 40% 42% |
#407a95 |
Darkened frost blue |
--smui-frost-3 |
210 34% 45% |
#4c6d8e |
Darkened steel |
--smui-frost-4 |
213 32% 40% |
#456487 |
Darkened deep blue |
--smui-green |
92 35% 38% |
#558040 |
Darkened success |
--smui-yellow |
40 70% 38% |
#a57e1d |
Darkened warning |
--smui-orange |
14 55% 48% |
#be5637 |
Darkened alert |
--smui-red |
355 55% 48% |
#be3744 |
Darkened danger |
--smui-purple |
311 28% 45% |
#8d5283 |
Darkened info |
| Variable | HSL | Hex | Usage |
|---|---|---|---|
--smui-surface-0 |
218 27% 94% |
#eceff4 |
Page background |
--smui-surface-1 |
218 27% 92% |
#e5e9f0 |
Cards, panels |
--smui-surface-2 |
219 28% 88% |
#d8dee9 |
Elevated elements |
--smui-surface-3 |
219 20% 82% |
#c9cfda |
Highlights, active states |
Use next-themes for theme switching. Wrap your app:
import { ThemeProvider } from "next-themes";
<ThemeProvider attribute="class" defaultTheme="dark">
{children}
</ThemeProvider>CSS variables automatically switch between light (:root) and dark (.dark) values.
Register the extended palette in your @theme inline block:
@theme inline {
--color-smui-frost-1: hsl(var(--smui-frost-1));
--color-smui-frost-2: hsl(var(--smui-frost-2));
--color-smui-frost-3: hsl(var(--smui-frost-3));
--color-smui-frost-4: hsl(var(--smui-frost-4));
--color-smui-red: hsl(var(--smui-red));
--color-smui-orange: hsl(var(--smui-orange));
--color-smui-yellow: hsl(var(--smui-yellow));
--color-smui-green: hsl(var(--smui-green));
--color-smui-purple: hsl(var(--smui-purple));
--color-smui-surface-0: hsl(var(--smui-surface-0));
--color-smui-surface-1: hsl(var(--smui-surface-1));
--color-smui-surface-2: hsl(var(--smui-surface-2));
--color-smui-surface-3: hsl(var(--smui-surface-3));
--color-smui-border-hover: hsl(var(--smui-border-hover));
}Then use in Tailwind: bg-smui-frost-2, text-smui-red, border-smui-surface-3.
For inline usage with alpha, use the raw HSL triplet: text-[hsl(var(--smui-green))], border-[hsl(var(--smui-yellow)/0.3)], bg-[hsl(var(--smui-frost-2)/0.04)].
JetBrains Mono is the only font. It is used for both --font-sans and --font-mono. All text is monospace.
Load it in your layout:
import { JetBrains_Mono } from "next/font/google";
const mono = JetBrains_Mono({
subsets: ["latin"],
weight: ["300", "400", "500", "600", "700"],
});
// Apply as: <body className={mono.className}>SMUI defines a set of CSS custom properties for font sizes. These are configurable via CSS variables:
| CSS Variable | Default | Tailwind Utility | Usage |
|---|---|---|---|
--text-label |
11px |
text-label |
Labels, badges, status text, kbd shortcuts |
--text-ui |
13px |
text-ui |
Buttons, nav items, table body, alerts |
--text-heading |
22px |
text-heading |
Section headings |
--text-stat |
26px |
text-stat |
Big stat numbers |
--text-hero |
42px |
text-hero |
Hero display text |
Standard Tailwind sizes text-xs (12px) and text-sm (14px) are also used for card titles and body text respectively.
Add a non-inline @theme block (separate from your @theme inline colors). This is important -- @theme inline does not emit CSS custom properties, so font-size utilities won't resolve:
@theme {
--text-label: 11px;
--text-ui: 13px;
--text-heading: 22px;
--text-stat: 26px;
--text-hero: 42px;
}Because text-label, text-ui, etc. look like color classes to tailwind-merge, you must extend your cn() helper so they aren't merged away by color classes like text-foreground:
import { extendTailwindMerge } from "tailwind-merge"
const twMerge = extendTailwindMerge({
extend: {
classGroups: {
"font-size": [
"text-label",
"text-ui",
"text-heading",
"text-stat",
"text-hero",
],
},
},
})
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}To adjust font sizes globally, override the CSS custom properties:
:root {
--text-label: 12px; /* make labels slightly larger */
--text-stat: 28px; /* bigger stat numbers */
}SMUI uses a small number of recurring typography patterns. Apply these consistently:
text-xs text-muted-foreground tracking-[1.5px] uppercase font-normal
Used for card headers, section labels, and panel titles. Always uppercase with wide letter-spacing.
Example: VESSEL CONFIG, SYSTEM READOUTS, CARGO MANIFEST
text-label text-muted-foreground tracking-[1.5px] uppercase block mb-1
Used above form inputs, data fields, and grouped content. Slightly smaller than card titles.
Example: VESSEL NAME, POWER ALLOCATION, AUTH CODE
text-label text-muted-foreground tracking-wider
Used for secondary descriptive text like crew roles, subtitles, and status descriptions.
Example: COMMANDING OFFICER, broadcast vessel identity
text-stat font-medium text-foreground tracking-tight
Used for prominent numeric displays in stat cards.
Example: 1,247,830, 142 / 7
text-xs text-[hsl(var(--smui-green))] mt-0.5
Used below stat numbers to show positive/negative changes. Use --smui-green for positive, --smui-red for negative.
Example: +23,450 this cycle
text-xs text-muted-foreground tracking-[2px] uppercase mb-1.5
Used above section headings as category labels.
Example: COMPONENTS, EXAMPLE // LAYOUT
text-heading font-medium text-foreground tracking-tight mb-1
Used for main section titles on the page.
text-sm text-muted-foreground
Used for descriptive paragraphs.
text-label tracking-wider uppercase px-1.5 py-px border
Used for inline status indicators. Combine with color classes.
- Labels are always uppercase with
tracking-[1.5px]ortracking-wider - Never use serif or sans-serif fonts -- monospace only
- Use muted-foreground for secondary text, foreground for primary
- Primary color for emphasis -- links, active values, important numbers
- Stat numbers use tracking-tight to keep them compact
--radius: 0rem. All components have sharp corners. No exceptions. The only rounded elements are:
- Status indicator dots:
rounded-full(5px circles) - Toggle switch knobs:
rounded-full(part of the switch metaphor) - Avatar fallbacks:
rounded-full(inherent to the component)
The standard card structure used throughout SMUI:
<Card className="card-glow">
<CardHeader className="flex flex-row items-center justify-between py-2.5 px-3.5">
<CardTitle className="text-xs text-muted-foreground tracking-[1.5px] uppercase font-normal">
section title
</CardTitle>
<CardDescription className="text-xs text-muted-foreground flex items-center gap-1">
<span className="inline-block w-[5px] h-[5px] rounded-full bg-[hsl(var(--smui-green))]" />
status
</CardDescription>
</CardHeader>
<CardContent>
{/* content */}
</CardContent>
</Card>Key details:
card-glowadds a subtle border-color transition on hover- Header has
py-2.5 px-3.5compact padding - Title/description are on a single row with
justify-between - Status dots are 5px circles with aurora colors
<Card className="card-glow p-2.5 px-3">
<span className="text-label text-muted-foreground tracking-[1.5px] uppercase block">
total credits
</span>
<div className="text-stat font-medium text-foreground tracking-tight">
1,247,830
</div>
<div className="text-xs text-[hsl(var(--smui-green))] mt-0.5">
+23,450 this cycle
</div>
</Card>For roster-style lists with avatars/icons and status:
<div className="flex items-center gap-2.5 py-[7px] border-b border-border last:border-b-0">
{/* Icon/avatar box */}
<div className="w-[34px] h-[34px] flex items-center justify-center text-xs font-semibold tracking-wider border border-border text-muted-foreground bg-background shrink-0">
CMD
</div>
{/* Content */}
<div className="flex-1">
<div className="text-sm">Kael Voss</div>
<div className="text-label text-muted-foreground tracking-wider">COMMANDING OFFICER</div>
</div>
{/* Status badge */}
<span className="text-label tracking-wider uppercase px-1.5 py-px border text-[hsl(var(--smui-green))] border-[hsl(var(--smui-green)/0.3)]">
online
</span>
</div>For read-only data fields (like location display):
<div>
<span className="text-label text-muted-foreground tracking-[1.5px] uppercase block mb-1">
system
</span>
<div className="text-sm px-2 py-1.5 bg-background border border-border text-primary">
GAMMA DRACONIS
</div>
</div><div className={`flex items-center gap-2 text-ui py-[5px] px-2.5 cursor-pointer transition-all ${
active
? "text-primary bg-primary/10"
: "text-muted-foreground hover:text-foreground hover:bg-secondary"
}`}>
<span className={`text-xs w-3.5 text-center ${active ? "opacity-100" : "opacity-50"}`}>
~
</span>
overview
</div>Always use the same aurora color for the same semantic meaning:
| State | Color Variable | Examples |
|---|---|---|
| Success / Online / Nominal | --smui-green |
"online", "active", "nominal", positive delta |
| Warning / Standby / Caution | --smui-yellow |
"standby", "warning", "degraded" |
| Alert / Degraded | --smui-orange |
"alert", "offline" |
| Critical / Error / Danger | --smui-red |
"critical", "error", "reactor warning" |
| Info / Special | --smui-purple |
special items, rare loot |
| Neutral / Inactive | text-muted-foreground |
"off-duty", disabled |
<span className="text-label tracking-wider uppercase px-1.5 py-px border text-[hsl(var(--smui-green))] border-[hsl(var(--smui-green)/0.3)]">
online
</span>Replace --smui-green with the appropriate aurora color for the status.
<span className="inline-block w-[5px] h-[5px] rounded-full bg-[hsl(var(--smui-green))]" />{/* Info: frost blue */}
<Alert className="border-[hsl(var(--smui-frost-2)/0.25)] bg-[hsl(var(--smui-frost-2)/0.04)] [&>svg]:text-[hsl(var(--smui-frost-2))]">
<Info className="h-3.5 w-3.5" />
<div>
<AlertTitle className="text-[hsl(var(--smui-frost-2))]">Title</AlertTitle>
<AlertDescription>Description</AlertDescription>
</div>
</Alert>
{/* Warning: yellow */}
<Alert className="border-[hsl(var(--smui-yellow)/0.25)] bg-[hsl(var(--smui-yellow)/0.04)] [&>svg]:text-[hsl(var(--smui-yellow))]">
<AlertTriangle className="h-3.5 w-3.5" />
<div>
<AlertTitle className="text-[hsl(var(--smui-yellow))]">Title</AlertTitle>
<AlertDescription>Description</AlertDescription>
</div>
</Alert>
{/* Success: green */}
<Alert className="border-[hsl(var(--smui-green)/0.25)] bg-[hsl(var(--smui-green)/0.04)] [&>svg]:text-[hsl(var(--smui-green))]">
<CheckCircle2 className="h-3.5 w-3.5" />
<div>
<AlertTitle className="text-[hsl(var(--smui-green))]">Title</AlertTitle>
<AlertDescription>Description</AlertDescription>
</div>
</Alert>
{/* Error: use the built-in destructive variant */}
<Alert variant="destructive">
<XCircle className="h-3.5 w-3.5" />
<div>
<AlertTitle>Title</AlertTitle>
<AlertDescription>Description</AlertDescription>
</div>
</Alert>The pattern for custom-colored alerts is:
- Border:
border-[hsl(var(--smui-COLOR)/0.25)](25% opacity) - Background:
bg-[hsl(var(--smui-COLOR)/0.04)](4% opacity) - Icon color:
[&>svg]:text-[hsl(var(--smui-COLOR))] - Title color:
text-[hsl(var(--smui-COLOR))]
const colorMap = {
default: "", // uses --primary
warn: "[&>div]:bg-[hsl(var(--smui-yellow))]",
crit: "[&>div]:bg-[hsl(var(--smui-red))]",
ok: "[&>div]:bg-[hsl(var(--smui-green))]",
};
<Progress value={71} className={colorMap["warn"]} />For item type indicators in tables:
const typeColors = {
ore: "text-[hsl(var(--smui-yellow))] border-[hsl(var(--smui-yellow)/0.3)]",
wpn: "text-[hsl(var(--smui-red))] border-[hsl(var(--smui-red)/0.3)]",
mod: "text-[hsl(var(--smui-frost-3))] border-[hsl(var(--smui-frost-3)/0.3)]",
ref: "text-[hsl(var(--smui-green))] border-[hsl(var(--smui-green)/0.3)]",
};
<span className={`text-label tracking-wider uppercase px-1.5 py-px border ${typeColors[type]}`}>
{type}
</span>Add the card-glow class to cards for a border-color transition on hover:
.card-glow {
transition: border-color 0.15s;
}
.card-glow:hover {
border-color: hsl(var(--smui-border-hover));
}Focus states use the --ring variable (frost blue) via shadcn's built-in focus ring system. No customization needed.
::selection {
background: hsl(193 44% 67% / 0.2);
color: hsl(193 44% 67%);
}@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.skeleton {
background: linear-gradient(90deg,
hsl(var(--smui-surface-2)) 25%,
hsl(var(--smui-surface-3)) 50%,
hsl(var(--smui-surface-2)) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}Use shadcn Button with uppercase text (built into the component variant). Common patterns:
<Button>commit</Button> {/* Primary action */}
<Button variant="outline">cancel</Button> {/* Secondary action */}
<Button variant="ghost" size="sm">abort</Button> {/* Tertiary/dismiss */}
<Button size="sm" className="w-full mt-2.5">submit</Button> {/* Full-width in card */}Active items use primary color, inactive use muted:
<Badge variant="outline" className="text-primary border-primary/30">active</Badge>
<Badge variant="outline" className="text-muted-foreground">inactive</Badge>Use the line variant for underline-style tabs:
<Tabs defaultValue="tab1">
<TabsList variant="line">
<TabsTrigger value="tab1">first</TabsTrigger>
<TabsTrigger value="tab2">second</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Content</TabsContent>
</Tabs>Manual toggle switches (not using the shadcn Switch component):
<div className={`relative w-9 h-[18px] rounded-full border shrink-0 cursor-pointer transition-all ${
on ? "bg-primary/10 border-primary" : "bg-background border-border"
}`}>
<div className={`absolute top-[2px] w-3 h-3 rounded-full transition-all ${
on ? "left-5 bg-primary" : "left-[2px] bg-muted-foreground"
}`} />
</div>Dialogs use the card surface with bordered header/footer:
<div className="bg-card border border-border">
<div className="p-2.5 px-3 border-b border-border">
<div className="text-sm font-medium text-foreground uppercase tracking-wider">title</div>
<div className="text-xs text-muted-foreground mt-0.5">subtitle</div>
</div>
<div className="p-3">
{/* body */}
</div>
<div className="p-2 px-3 border-t border-border flex gap-1 justify-end">
<Button variant="ghost" size="sm">cancel</Button>
<Button size="sm">confirm</Button>
</div>
</div><div className="flex items-center gap-2 text-label text-muted-foreground uppercase tracking-wider">
<Separator className="flex-1" />
<span>or</span>
<Separator className="flex-1" />
</div><kbd className="text-label text-muted-foreground border border-border px-1 bg-background">
ctrl+k
</kbd>Simple bar charts using divs:
<div className="flex items-end gap-1 h-[100px]">
{data.map((value, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-[3px] h-full justify-end">
<div
className="w-full bg-primary opacity-70"
style={{ height: `${value}%` }}
/>
<span className="text-xs text-muted-foreground">
{String(i + 1).padStart(2, "0")}
</span>
</div>
))}
</div>Use bg-[hsl(var(--smui-frost-4))] to highlight specific bars.
Use Lucide React for all icons. Never use emoji. Common sizes:
w-3 h-3-- inline with small textw-3.5 h-3.5-- alerts, list itemsw-5 h-5-- navigation, standalone
The theme supports runtime accent color switching by updating CSS variables on document.documentElement. To change the accent:
- Convert the desired hex color to HSL
- Update
--primary,--ring,--accent-foreground,--sidebar-primary,--sidebar-accent-foreground,--sidebar-ring,--chart-1,--smui-frost-2with the new HSL value - Darken the color and update
--accentand--sidebar-accentwith the darkened value
See demo/src/components/accent-picker.tsx for a complete reference implementation.
These are the most frequently used class combinations in SMUI interfaces:
Card title: text-xs text-muted-foreground tracking-[1.5px] uppercase font-normal
Field label: text-label text-muted-foreground tracking-[1.5px] uppercase block mb-1
Status badge: text-label tracking-wider uppercase px-1.5 py-px border
Big number: text-stat font-medium text-foreground tracking-tight
Section eyebrow: text-xs text-muted-foreground tracking-[2px] uppercase mb-1.5
Section title: text-heading font-medium text-foreground tracking-tight mb-1
Body text: text-sm text-muted-foreground
Status dot: inline-block w-[5px] h-[5px] rounded-full bg-[hsl(var(--smui-COLOR))]
Card hover: card-glow (CSS class)
Active nav: text-primary bg-primary/10
Inactive nav: text-muted-foreground hover:text-foreground hover:bg-secondary