INSTALL SYSTEM
IN 30 SECONDS_
Pick your AI. Hit copy. Paste it into a fresh chat in your project. Watch the theme arrive.
Install the Wardrobe System theme in my project. I'm using Claude Code / Cursor. Apply every step below as written.
Step 1 — Add this to globals.css:
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Space+Grotesk:wght@300;400;500;600&display=swap");
/* =========================================================
Wardrobe — SYSTEM tokens
Scoped via :where() for low specificity.
Activate by adding data-system="system" to a body or wrapper.
========================================================= */
:where([data-system="system"]) {
/* Surface */
--color-bg: #F4F4F4;
--color-card: #FFFFFF;
--color-fg: #0D0D0D;
--color-fg-muted: #888888;
--color-fg-faint: #BBBBBB;
--color-fg-ghost: #EEEEEE;
--color-divider: #F0F0F0;
/* Accents */
--color-accent: #6CEFA0;
--color-accent-blue: #6CDDEF;
--color-accent-purple: #B06CEF;
--color-accent-orange: #EF9B6C;
--color-success: #6CEFA0;
--color-fail: #FF6B6B;
/* Dark inversions (analysis panels in light dashboards) */
--color-dark-bg: #1A1A1A;
--color-dark-fg: #DDDDDD;
--color-dark-muted: #888888;
/* Borders */
--border: 1px solid rgba(0, 0, 0, 0.05);
--border-strong: 1px solid #EEEEEE;
--border-color: rgba(0, 0, 0, 0.05);
/* Radius — sharp! 2px MAX. Never pill. */
--radius-sm: 1px;
--radius-md: 2px;
--radius-lg: 2px;
--radius-full: 9999px;
/* Typography */
--font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, sans-serif;
--font-body: "Space Grotesk", -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-micro: 10px;
--text-xs: 11px;
--text-sm: 12px;
--text-base: 13px;
--text-md: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 48px;
--text-6xl: 64px;
--display-letter-spacing: -2px;
--display-line-height: 1;
--label-letter-spacing: 1.5px;
--button-letter-spacing: 1px;
/* Weights */
--weight-light: 300;
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
/* Spacing */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
/* Shadow — soft only. Borders are nearly invisible. */
--shadow-soft: 0 4px 20px rgba(0, 0, 0, 0.04);
--shadow-flat: 0 1px 2px rgba(0, 0, 0, 0.03);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="system"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
Step 2 — Create components/ui/wardrobe/cn.ts:
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
Step 3 — Create the following 10 component files in components/ui/wardrobe/:
--- components/ui/wardrobe/Button.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive" | "mint";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn("sys-btn", `sys-btn--${variant}`, `sys-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
--- components/ui/wardrobe/Input.tsx ---
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
display?: boolean;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
export function Input({
size = "md",
variant = "default",
label,
hint,
display,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<label className="sys-field" htmlFor={inputId}>
{label && <span className="sys-field__label">{label}</span>}
<input
id={inputId}
className={cn(
"sys-input",
`sys-input--${size}`,
variant === "error" && "sys-input--error",
display && "sys-input--display",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"sys-field__hint",
variant === "error" && "sys-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Textarea.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TextareaProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
} & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">;
export function Textarea({
size = "md",
variant = "default",
label,
hint,
className,
id,
...props
}: TextareaProps) {
const inputId = id ?? React.useId();
return (
<label className="sys-field" htmlFor={inputId}>
{label && <span className="sys-field__label">{label}</span>}
<textarea
id={inputId}
className={cn(
"sys-textarea",
`sys-input--${size}`,
variant === "error" && "sys-textarea--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"sys-field__hint",
variant === "error" && "sys-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Select.tsx ---
import * as React from "react";
import { cn } from "./cn";
type SelectProps = {
options: Array<{ value: string; label: string }>;
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
label?: string;
} & Omit<
React.SelectHTMLAttributes<HTMLSelectElement>,
"value" | "onChange" | "size"
>;
export function Select({
options,
value,
onValueChange,
placeholder,
label,
className,
id,
...props
}: SelectProps) {
const inputId = id ?? React.useId();
return (
<label className="sys-field" htmlFor={inputId}>
{label && <span className="sys-field__label">{label}</span>}
<select
id={inputId}
value={value}
onChange={(e) => onValueChange(e.target.value)}
className={cn("sys-select", className)}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</label>
);
}
--- components/ui/wardrobe/Card.tsx ---
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "dark" | "accent";
size?: "default" | "lg";
codeTexture?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
size = "default",
codeTexture,
className,
children,
...props
}: CardProps) {
return (
<div
className={cn(
"sys-card",
variant !== "default" && `sys-card--${variant}`,
size !== "default" && `sys-card--${size}`,
className,
)}
{...props}
>
{codeTexture && (
<span className="sys-card__code-texture">{codeTexture}</span>
)}
{children}
</div>
);
}
--- components/ui/wardrobe/Badge.tsx ---
import * as React from "react";
import { cn } from "./cn";
type BadgeProps = {
variant?: "default" | "filled";
children: React.ReactNode;
className?: string;
};
export function Badge({ variant = "default", children, className }: BadgeProps) {
return (
<span
className={cn(
"sys-badge",
variant === "filled" && "sys-badge--filled",
className,
)}
>
{children}
</span>
);
}
--- components/ui/wardrobe/Dialog.tsx ---
import * as React from "react";
type DialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
children: React.ReactNode;
};
export function Dialog({ open, onOpenChange, title, children }: DialogProps) {
const dialogRef = React.useRef<HTMLDivElement>(null);
const previousFocus = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
if (!open) return;
previousFocus.current = document.activeElement as HTMLElement;
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
focusable?.[0]?.focus();
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") {
e.preventDefault();
onOpenChange(false);
return;
}
if (e.key !== "Tab" || !focusable || focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
previousFocus.current?.focus();
};
}, [open, onOpenChange]);
if (!open) return null;
return (
<div
className="sys-dialog-backdrop"
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div
ref={dialogRef}
className="sys-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="sys-dialog-title"
>
<button
type="button"
className="sys-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
×
</button>
<h2 id="sys-dialog-title" className="sys-dialog__title">
{title}
</h2>
{children}
</div>
</div>
);
}
--- components/ui/wardrobe/Tabs.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TabsProps = {
tabs: Array<{ id: string; label: string }>;
activeId: string;
onTabChange: (id: string) => void;
className?: string;
};
export function Tabs({ tabs, activeId, onTabChange, className }: TabsProps) {
return (
<div role="tablist" className={cn("sys-tabs", className)}>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={tab.id === activeId}
type="button"
className={cn(
"sys-tabs__btn",
tab.id === activeId && "sys-tabs__btn--active",
)}
onClick={() => onTabChange(tab.id)}
onKeyDown={(e) => {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
e.preventDefault();
const idx = tabs.findIndex((t) => t.id === activeId);
const next =
e.key === "ArrowRight"
? (idx + 1) % tabs.length
: (idx - 1 + tabs.length) % tabs.length;
onTabChange(tabs[next].id);
}}
>
{tab.label}
</button>
))}
</div>
);
}
--- components/ui/wardrobe/Switch.tsx ---
import * as React from "react";
import { cn } from "./cn";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
id?: string;
terminal?: boolean;
};
export function Switch({
checked,
onCheckedChange,
label,
id,
terminal,
}: SwitchProps) {
const inputId = id ?? React.useId();
return (
<label
className={cn("sys-switch", terminal && "sys-switch--terminal")}
htmlFor={inputId}
>
<input
id={inputId}
type="checkbox"
role="switch"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
className="sys-switch__input"
/>
{terminal ? (
<>
<span className="sys-switch__bracket">
{checked ? "[X]" : "[ ]"}
</span>
{label && <span>{label}</span>}
</>
) : (
<>
<span className="sys-switch__box">
<svg
className="sys-switch__check"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
<polyline points="2 6 5 9 10 3" />
</svg>
</span>
{label && <span>{label}</span>}
</>
)}
</label>
);
}
--- components/ui/wardrobe/Toast.tsx ---
import * as React from "react";
import { cn } from "./cn";
export type ToastVariant = "default" | "success" | "error";
export type ToastProps = {
title: string;
description?: string;
variant?: ToastVariant;
};
type ToastEntry = ToastProps & { id: number; time: string };
type ToastContextValue = {
show: (toast: ToastProps) => void;
};
const ToastContext = React.createContext<ToastContextValue | null>(null);
export function useToast() {
const ctx = React.useContext(ToastContext);
if (!ctx) throw new Error("useToast must be used within <ToastProvider>");
return ctx;
}
function timestamp(): string {
const d = new Date();
const pad = (n: number) => String(n).padStart(2, "0");
const ms = String(Math.floor(d.getMilliseconds() / 10)).padStart(2, "0");
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${ms}`;
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<ToastEntry[]>([]);
const idRef = React.useRef(0);
const show = React.useCallback((toast: ToastProps) => {
const id = ++idRef.current;
setToasts((prev) => [...prev, { ...toast, id, time: timestamp() }]);
window.setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 4000);
}, []);
return (
<ToastContext.Provider value={{ show }}>
{children}
<div
className="sys-toast-region"
role="region"
aria-label="Notifications"
aria-live="polite"
>
{toasts.map((t) => (
<Toast key={t.id} {...t} />
))}
</div>
</ToastContext.Provider>
);
}
type ToastViewProps = ToastProps & { time?: string };
export function Toast({
title,
description,
variant = "default",
time,
}: ToastViewProps) {
return (
<div
className={cn(
"sys-toast",
variant === "success" && "sys-toast--success",
variant === "error" && "sys-toast--error",
variant === "default" && "sys-toast--default",
)}
role="status"
>
<span className="sys-toast__title">{title}</span>
{time && <span className="sys-toast__time">{time}</span>}
{description && (
<span className="sys-toast__desc">{description}</span>
)}
</div>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — SYSTEM globals
Loads display + mono fonts and base component styles.
Only active on elements with data-system="system".
========================================================= */
[data-system="system"] *,
[data-system="system"] *::before,
[data-system="system"] *::after {
box-sizing: border-box;
}
[data-system="system"] {
min-height: 100vh;
}
/* ----- LABEL MICRO — the System signature ----- */
.sys-label-micro {
font-family: var(--font-mono);
font-size: var(--text-micro);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
color: var(--color-fg-muted);
line-height: 1.4;
display: block;
}
/* ----- BUTTON ----- */
.sys-btn {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
font-size: var(--text-sm);
border: 1px solid var(--color-fg);
border-radius: var(--radius-md);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing),
opacity var(--duration-fast) var(--easing);
text-decoration: none;
line-height: 1;
background: var(--color-fg);
color: var(--color-card);
padding: 0 18px;
height: 40px;
}
.sys-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.sys-btn--primary {
background: var(--color-fg);
color: var(--color-card);
}
.sys-btn--primary:hover:not(:disabled) {
background: #2a2a2a;
}
.sys-btn--secondary {
background: var(--color-card);
color: var(--color-fg);
border: 1px solid var(--color-fg);
}
.sys-btn--secondary:hover:not(:disabled) {
background: var(--color-fg);
color: var(--color-card);
}
.sys-btn--destructive {
background: var(--color-fail);
color: #FFFFFF;
border-color: var(--color-fail);
}
.sys-btn--mint {
background: var(--color-accent);
color: var(--color-fg);
border-color: var(--color-accent);
}
.sys-btn--sm { height: 32px; padding: 0 14px; font-size: var(--text-xs); }
.sys-btn--md { height: 40px; padding: 0 18px; font-size: var(--text-sm); }
.sys-btn--lg { height: 48px; padding: 0 22px; font-size: var(--text-md); }
.sys-btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ----- INPUT / TEXTAREA — bottom-border only ----- */
.sys-field {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.sys-field__label { /* alias of sys-label-micro for in-field labels */
font-family: var(--font-mono);
font-size: var(--text-micro);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
color: var(--color-fg-muted);
line-height: 1;
}
.sys-field__hint {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-fg-muted);
}
.sys-field__hint--error { color: var(--color-fail); }
.sys-input,
.sys-textarea {
font-family: var(--font-mono);
font-size: var(--text-md);
color: var(--color-fg);
background: transparent;
border: none;
border-bottom: 1px solid #E0E0E0;
border-radius: 0;
padding: 8px 0;
width: 100%;
outline: none;
transition: border-color var(--duration-base) var(--easing);
}
.sys-input:focus,
.sys-textarea:focus { border-bottom-color: var(--color-fg); }
.sys-input--error,
.sys-textarea--error { border-bottom-color: var(--color-fail); }
.sys-input::placeholder,
.sys-textarea::placeholder { color: var(--color-fg-faint); }
.sys-input--sm { font-size: var(--text-sm); padding: 6px 0; }
.sys-input--md { font-size: var(--text-md); }
.sys-input--lg { font-size: var(--text-xl); padding: 10px 0; }
.sys-input--display {
font-family: var(--font-display);
font-weight: var(--weight-light);
font-size: var(--text-5xl);
letter-spacing: var(--display-letter-spacing);
line-height: var(--display-line-height);
padding: 6px 0;
}
.sys-textarea {
min-height: 100px;
resize: vertical;
line-height: 1.55;
}
/* ----- SELECT ----- */
.sys-select {
font-family: var(--font-mono);
font-size: var(--text-sm);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
color: var(--color-fg);
background: var(--color-card);
border: 1px solid #E0E0E0;
border-radius: var(--radius-md);
padding: 10px 36px 10px 14px;
width: 100%;
appearance: none;
-webkit-appearance: none;
cursor: pointer;
transition: border-color var(--duration-base) var(--easing),
box-shadow var(--duration-base) var(--easing);
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%230D0D0D' stroke-width='2' stroke-linecap='square' stroke-linejoin='miter'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 14px center;
outline: none;
}
.sys-select:focus {
border-color: var(--color-fg);
box-shadow: var(--shadow-soft);
}
/* ----- CARD ----- */
.sys-card {
background: var(--color-card);
border-radius: var(--radius-md);
box-shadow: var(--shadow-soft);
padding: var(--space-6);
position: relative;
overflow: hidden;
}
.sys-card--lg { padding: var(--space-8); }
.sys-card--dark {
background: var(--color-dark-bg);
color: var(--color-dark-fg);
}
.sys-card--accent {
border-left: 3px solid var(--color-accent);
}
.sys-card__code-texture {
position: absolute;
top: 20px;
right: 20px;
font-family: var(--font-mono);
font-size: 8px;
line-height: 1.4;
color: var(--color-fg-ghost);
text-align: right;
pointer-events: none;
white-space: pre;
}
.sys-card--dark .sys-card__code-texture {
color: rgba(255, 255, 255, 0.08);
}
/* ----- BADGE ----- */
.sys-badge {
display: inline-flex;
align-items: center;
background: var(--color-card);
color: var(--color-fg-muted);
border: 1px solid #DDDDDD;
border-radius: var(--radius-sm);
padding: 2px 8px;
font-family: var(--font-mono);
font-size: 9px;
line-height: 1.4;
letter-spacing: var(--button-letter-spacing);
text-transform: uppercase;
}
.sys-badge--filled {
background: var(--color-accent);
color: var(--color-fg);
border-color: var(--color-accent);
}
/* ----- DIALOG ----- */
.sys-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(13, 13, 13, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
animation: sys-fade-in 200ms var(--easing);
}
.sys-dialog {
background: var(--color-card);
border-radius: var(--radius-md);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
max-width: 480px;
width: 100%;
padding: var(--space-8);
position: relative;
animation: sys-pop-in 220ms var(--easing);
}
.sys-dialog__title {
font-family: var(--font-display);
font-weight: var(--weight-normal);
font-size: 28px;
line-height: 1.1;
letter-spacing: -0.5px;
margin: 0 0 var(--space-3);
}
.sys-dialog__close {
position: absolute;
top: 16px;
right: 16px;
width: 28px;
height: 28px;
border: 1px solid #DDDDDD;
border-radius: var(--radius-sm);
background: var(--color-card);
cursor: pointer;
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-fg-muted);
line-height: 1;
}
.sys-dialog__close:hover {
background: var(--color-fg);
color: var(--color-card);
border-color: var(--color-fg);
}
@keyframes sys-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes sys-pop-in {
from { opacity: 0; transform: translateY(8px) scale(0.99); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ----- TABS — segmented nav-items ----- */
.sys-tabs {
display: inline-flex;
background: var(--color-card);
border: 1px solid var(--color-divider);
padding: 0;
gap: 0;
}
.sys-tabs__btn {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
font-size: var(--text-sm);
padding: 10px 16px;
border: none;
background: transparent;
color: var(--color-fg);
cursor: pointer;
border-radius: 0;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
line-height: 1;
}
.sys-tabs__btn:not(.sys-tabs__btn--active):hover {
background: var(--color-divider);
}
.sys-tabs__btn--active {
background: var(--color-fg);
color: var(--color-card);
}
.sys-tabs__btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: -2px;
}
/* ----- SWITCH — square checkbox ----- */
.sys-switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
font-family: var(--font-mono);
font-size: var(--text-sm);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
}
.sys-switch__box {
width: 18px;
height: 18px;
border: 2px solid #DDDDDD;
border-radius: var(--radius-md);
background: transparent;
position: relative;
flex-shrink: 0;
transition: background var(--duration-base) var(--easing),
border-color var(--duration-base) var(--easing);
display: flex;
align-items: center;
justify-content: center;
}
.sys-switch__check {
width: 10px;
height: 10px;
color: var(--color-card);
opacity: 0;
transition: opacity var(--duration-fast) var(--easing);
}
.sys-switch__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.sys-switch__input:checked + .sys-switch__box {
background: var(--color-accent);
border-color: var(--color-accent);
}
.sys-switch__input:checked + .sys-switch__box .sys-switch__check {
opacity: 1;
color: var(--color-fg);
}
.sys-switch__input:focus-visible + .sys-switch__box {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* terminal switch variant — "[X]/[ ] LABEL" pattern */
.sys-switch--terminal {
gap: 8px;
}
.sys-switch--terminal .sys-switch__box {
display: none;
}
.sys-switch--terminal .sys-switch__bracket {
font-family: var(--font-mono);
font-size: var(--text-base);
color: var(--color-fg);
line-height: 1;
}
.sys-switch--terminal .sys-switch__input:not(:checked) ~ .sys-switch__bracket,
.sys-switch--terminal .sys-switch__input:not(:checked) ~ * {
opacity: 0.5;
}
.sys-switch--terminal .sys-switch__input:checked ~ .sys-switch__bracket,
.sys-switch--terminal .sys-switch__input:checked ~ * {
opacity: 1;
}
/* ----- TOAST ----- */
.sys-toast-region {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 1100;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
width: max-content;
max-width: calc(100vw - 48px);
}
.sys-toast {
background: var(--color-card);
color: var(--color-fg);
border-radius: var(--radius-md);
box-shadow: var(--shadow-soft);
padding: 14px 18px;
border-left: 3px solid var(--color-accent);
font-family: var(--font-mono);
font-size: var(--text-xs);
pointer-events: auto;
animation: sys-toast-in 280ms var(--easing);
display: grid;
grid-template-columns: 1fr auto;
gap: 8px 16px;
min-width: 280px;
align-items: baseline;
}
.sys-toast__title {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
font-size: var(--text-xs);
}
.sys-toast__time {
font-family: var(--font-mono);
font-size: 10px;
color: var(--color-fg-muted);
text-align: right;
grid-column: 2;
grid-row: 1;
}
.sys-toast__desc {
font-family: var(--font-mono);
font-size: 11px;
color: var(--color-fg-muted);
grid-column: 1 / -1;
line-height: 1.5;
}
.sys-toast--success { border-left-color: var(--color-accent); }
.sys-toast--error { border-left-color: var(--color-fail); }
.sys-toast--default { border-left-color: var(--color-fg); }
@keyframes sys-toast-in {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
/* ----- BLINKING CURSOR ----- */
.sys-cursor {
display: inline-block;
width: 8px;
height: 14px;
background: var(--color-accent);
animation: sys-blink 1s step-end infinite;
vertical-align: middle;
margin-left: 4px;
}
.sys-cursor--fg {
background: var(--color-fg);
}
@keyframes sys-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
Step 5 — Wrap the section/page you want themed with <div data-system="system">:
<div data-system="system">
<Button>HELLO SYSTEM</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in System.
Install the Wardrobe System theme in this v0 project. Treat each step below as a concrete file or edit you must make.
Step 1 — Add this to globals.css:
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Space+Grotesk:wght@300;400;500;600&display=swap");
/* =========================================================
Wardrobe — SYSTEM tokens
Scoped via :where() for low specificity.
Activate by adding data-system="system" to a body or wrapper.
========================================================= */
:where([data-system="system"]) {
/* Surface */
--color-bg: #F4F4F4;
--color-card: #FFFFFF;
--color-fg: #0D0D0D;
--color-fg-muted: #888888;
--color-fg-faint: #BBBBBB;
--color-fg-ghost: #EEEEEE;
--color-divider: #F0F0F0;
/* Accents */
--color-accent: #6CEFA0;
--color-accent-blue: #6CDDEF;
--color-accent-purple: #B06CEF;
--color-accent-orange: #EF9B6C;
--color-success: #6CEFA0;
--color-fail: #FF6B6B;
/* Dark inversions (analysis panels in light dashboards) */
--color-dark-bg: #1A1A1A;
--color-dark-fg: #DDDDDD;
--color-dark-muted: #888888;
/* Borders */
--border: 1px solid rgba(0, 0, 0, 0.05);
--border-strong: 1px solid #EEEEEE;
--border-color: rgba(0, 0, 0, 0.05);
/* Radius — sharp! 2px MAX. Never pill. */
--radius-sm: 1px;
--radius-md: 2px;
--radius-lg: 2px;
--radius-full: 9999px;
/* Typography */
--font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, sans-serif;
--font-body: "Space Grotesk", -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-micro: 10px;
--text-xs: 11px;
--text-sm: 12px;
--text-base: 13px;
--text-md: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 48px;
--text-6xl: 64px;
--display-letter-spacing: -2px;
--display-line-height: 1;
--label-letter-spacing: 1.5px;
--button-letter-spacing: 1px;
/* Weights */
--weight-light: 300;
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
/* Spacing */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
/* Shadow — soft only. Borders are nearly invisible. */
--shadow-soft: 0 4px 20px rgba(0, 0, 0, 0.04);
--shadow-flat: 0 1px 2px rgba(0, 0, 0, 0.03);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="system"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
Step 2 — Create components/ui/wardrobe/cn.ts:
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
Step 3 — Create the following 10 component files in components/ui/wardrobe/:
--- components/ui/wardrobe/Button.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive" | "mint";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn("sys-btn", `sys-btn--${variant}`, `sys-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
--- components/ui/wardrobe/Input.tsx ---
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
display?: boolean;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
export function Input({
size = "md",
variant = "default",
label,
hint,
display,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<label className="sys-field" htmlFor={inputId}>
{label && <span className="sys-field__label">{label}</span>}
<input
id={inputId}
className={cn(
"sys-input",
`sys-input--${size}`,
variant === "error" && "sys-input--error",
display && "sys-input--display",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"sys-field__hint",
variant === "error" && "sys-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Textarea.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TextareaProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
} & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">;
export function Textarea({
size = "md",
variant = "default",
label,
hint,
className,
id,
...props
}: TextareaProps) {
const inputId = id ?? React.useId();
return (
<label className="sys-field" htmlFor={inputId}>
{label && <span className="sys-field__label">{label}</span>}
<textarea
id={inputId}
className={cn(
"sys-textarea",
`sys-input--${size}`,
variant === "error" && "sys-textarea--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"sys-field__hint",
variant === "error" && "sys-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Select.tsx ---
import * as React from "react";
import { cn } from "./cn";
type SelectProps = {
options: Array<{ value: string; label: string }>;
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
label?: string;
} & Omit<
React.SelectHTMLAttributes<HTMLSelectElement>,
"value" | "onChange" | "size"
>;
export function Select({
options,
value,
onValueChange,
placeholder,
label,
className,
id,
...props
}: SelectProps) {
const inputId = id ?? React.useId();
return (
<label className="sys-field" htmlFor={inputId}>
{label && <span className="sys-field__label">{label}</span>}
<select
id={inputId}
value={value}
onChange={(e) => onValueChange(e.target.value)}
className={cn("sys-select", className)}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</label>
);
}
--- components/ui/wardrobe/Card.tsx ---
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "dark" | "accent";
size?: "default" | "lg";
codeTexture?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
size = "default",
codeTexture,
className,
children,
...props
}: CardProps) {
return (
<div
className={cn(
"sys-card",
variant !== "default" && `sys-card--${variant}`,
size !== "default" && `sys-card--${size}`,
className,
)}
{...props}
>
{codeTexture && (
<span className="sys-card__code-texture">{codeTexture}</span>
)}
{children}
</div>
);
}
--- components/ui/wardrobe/Badge.tsx ---
import * as React from "react";
import { cn } from "./cn";
type BadgeProps = {
variant?: "default" | "filled";
children: React.ReactNode;
className?: string;
};
export function Badge({ variant = "default", children, className }: BadgeProps) {
return (
<span
className={cn(
"sys-badge",
variant === "filled" && "sys-badge--filled",
className,
)}
>
{children}
</span>
);
}
--- components/ui/wardrobe/Dialog.tsx ---
import * as React from "react";
type DialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
children: React.ReactNode;
};
export function Dialog({ open, onOpenChange, title, children }: DialogProps) {
const dialogRef = React.useRef<HTMLDivElement>(null);
const previousFocus = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
if (!open) return;
previousFocus.current = document.activeElement as HTMLElement;
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
focusable?.[0]?.focus();
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") {
e.preventDefault();
onOpenChange(false);
return;
}
if (e.key !== "Tab" || !focusable || focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
previousFocus.current?.focus();
};
}, [open, onOpenChange]);
if (!open) return null;
return (
<div
className="sys-dialog-backdrop"
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div
ref={dialogRef}
className="sys-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="sys-dialog-title"
>
<button
type="button"
className="sys-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
×
</button>
<h2 id="sys-dialog-title" className="sys-dialog__title">
{title}
</h2>
{children}
</div>
</div>
);
}
--- components/ui/wardrobe/Tabs.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TabsProps = {
tabs: Array<{ id: string; label: string }>;
activeId: string;
onTabChange: (id: string) => void;
className?: string;
};
export function Tabs({ tabs, activeId, onTabChange, className }: TabsProps) {
return (
<div role="tablist" className={cn("sys-tabs", className)}>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={tab.id === activeId}
type="button"
className={cn(
"sys-tabs__btn",
tab.id === activeId && "sys-tabs__btn--active",
)}
onClick={() => onTabChange(tab.id)}
onKeyDown={(e) => {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
e.preventDefault();
const idx = tabs.findIndex((t) => t.id === activeId);
const next =
e.key === "ArrowRight"
? (idx + 1) % tabs.length
: (idx - 1 + tabs.length) % tabs.length;
onTabChange(tabs[next].id);
}}
>
{tab.label}
</button>
))}
</div>
);
}
--- components/ui/wardrobe/Switch.tsx ---
import * as React from "react";
import { cn } from "./cn";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
id?: string;
terminal?: boolean;
};
export function Switch({
checked,
onCheckedChange,
label,
id,
terminal,
}: SwitchProps) {
const inputId = id ?? React.useId();
return (
<label
className={cn("sys-switch", terminal && "sys-switch--terminal")}
htmlFor={inputId}
>
<input
id={inputId}
type="checkbox"
role="switch"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
className="sys-switch__input"
/>
{terminal ? (
<>
<span className="sys-switch__bracket">
{checked ? "[X]" : "[ ]"}
</span>
{label && <span>{label}</span>}
</>
) : (
<>
<span className="sys-switch__box">
<svg
className="sys-switch__check"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
<polyline points="2 6 5 9 10 3" />
</svg>
</span>
{label && <span>{label}</span>}
</>
)}
</label>
);
}
--- components/ui/wardrobe/Toast.tsx ---
import * as React from "react";
import { cn } from "./cn";
export type ToastVariant = "default" | "success" | "error";
export type ToastProps = {
title: string;
description?: string;
variant?: ToastVariant;
};
type ToastEntry = ToastProps & { id: number; time: string };
type ToastContextValue = {
show: (toast: ToastProps) => void;
};
const ToastContext = React.createContext<ToastContextValue | null>(null);
export function useToast() {
const ctx = React.useContext(ToastContext);
if (!ctx) throw new Error("useToast must be used within <ToastProvider>");
return ctx;
}
function timestamp(): string {
const d = new Date();
const pad = (n: number) => String(n).padStart(2, "0");
const ms = String(Math.floor(d.getMilliseconds() / 10)).padStart(2, "0");
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${ms}`;
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<ToastEntry[]>([]);
const idRef = React.useRef(0);
const show = React.useCallback((toast: ToastProps) => {
const id = ++idRef.current;
setToasts((prev) => [...prev, { ...toast, id, time: timestamp() }]);
window.setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 4000);
}, []);
return (
<ToastContext.Provider value={{ show }}>
{children}
<div
className="sys-toast-region"
role="region"
aria-label="Notifications"
aria-live="polite"
>
{toasts.map((t) => (
<Toast key={t.id} {...t} />
))}
</div>
</ToastContext.Provider>
);
}
type ToastViewProps = ToastProps & { time?: string };
export function Toast({
title,
description,
variant = "default",
time,
}: ToastViewProps) {
return (
<div
className={cn(
"sys-toast",
variant === "success" && "sys-toast--success",
variant === "error" && "sys-toast--error",
variant === "default" && "sys-toast--default",
)}
role="status"
>
<span className="sys-toast__title">{title}</span>
{time && <span className="sys-toast__time">{time}</span>}
{description && (
<span className="sys-toast__desc">{description}</span>
)}
</div>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — SYSTEM globals
Loads display + mono fonts and base component styles.
Only active on elements with data-system="system".
========================================================= */
[data-system="system"] *,
[data-system="system"] *::before,
[data-system="system"] *::after {
box-sizing: border-box;
}
[data-system="system"] {
min-height: 100vh;
}
/* ----- LABEL MICRO — the System signature ----- */
.sys-label-micro {
font-family: var(--font-mono);
font-size: var(--text-micro);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
color: var(--color-fg-muted);
line-height: 1.4;
display: block;
}
/* ----- BUTTON ----- */
.sys-btn {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
font-size: var(--text-sm);
border: 1px solid var(--color-fg);
border-radius: var(--radius-md);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing),
opacity var(--duration-fast) var(--easing);
text-decoration: none;
line-height: 1;
background: var(--color-fg);
color: var(--color-card);
padding: 0 18px;
height: 40px;
}
.sys-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.sys-btn--primary {
background: var(--color-fg);
color: var(--color-card);
}
.sys-btn--primary:hover:not(:disabled) {
background: #2a2a2a;
}
.sys-btn--secondary {
background: var(--color-card);
color: var(--color-fg);
border: 1px solid var(--color-fg);
}
.sys-btn--secondary:hover:not(:disabled) {
background: var(--color-fg);
color: var(--color-card);
}
.sys-btn--destructive {
background: var(--color-fail);
color: #FFFFFF;
border-color: var(--color-fail);
}
.sys-btn--mint {
background: var(--color-accent);
color: var(--color-fg);
border-color: var(--color-accent);
}
.sys-btn--sm { height: 32px; padding: 0 14px; font-size: var(--text-xs); }
.sys-btn--md { height: 40px; padding: 0 18px; font-size: var(--text-sm); }
.sys-btn--lg { height: 48px; padding: 0 22px; font-size: var(--text-md); }
.sys-btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ----- INPUT / TEXTAREA — bottom-border only ----- */
.sys-field {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.sys-field__label { /* alias of sys-label-micro for in-field labels */
font-family: var(--font-mono);
font-size: var(--text-micro);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
color: var(--color-fg-muted);
line-height: 1;
}
.sys-field__hint {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-fg-muted);
}
.sys-field__hint--error { color: var(--color-fail); }
.sys-input,
.sys-textarea {
font-family: var(--font-mono);
font-size: var(--text-md);
color: var(--color-fg);
background: transparent;
border: none;
border-bottom: 1px solid #E0E0E0;
border-radius: 0;
padding: 8px 0;
width: 100%;
outline: none;
transition: border-color var(--duration-base) var(--easing);
}
.sys-input:focus,
.sys-textarea:focus { border-bottom-color: var(--color-fg); }
.sys-input--error,
.sys-textarea--error { border-bottom-color: var(--color-fail); }
.sys-input::placeholder,
.sys-textarea::placeholder { color: var(--color-fg-faint); }
.sys-input--sm { font-size: var(--text-sm); padding: 6px 0; }
.sys-input--md { font-size: var(--text-md); }
.sys-input--lg { font-size: var(--text-xl); padding: 10px 0; }
.sys-input--display {
font-family: var(--font-display);
font-weight: var(--weight-light);
font-size: var(--text-5xl);
letter-spacing: var(--display-letter-spacing);
line-height: var(--display-line-height);
padding: 6px 0;
}
.sys-textarea {
min-height: 100px;
resize: vertical;
line-height: 1.55;
}
/* ----- SELECT ----- */
.sys-select {
font-family: var(--font-mono);
font-size: var(--text-sm);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
color: var(--color-fg);
background: var(--color-card);
border: 1px solid #E0E0E0;
border-radius: var(--radius-md);
padding: 10px 36px 10px 14px;
width: 100%;
appearance: none;
-webkit-appearance: none;
cursor: pointer;
transition: border-color var(--duration-base) var(--easing),
box-shadow var(--duration-base) var(--easing);
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%230D0D0D' stroke-width='2' stroke-linecap='square' stroke-linejoin='miter'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 14px center;
outline: none;
}
.sys-select:focus {
border-color: var(--color-fg);
box-shadow: var(--shadow-soft);
}
/* ----- CARD ----- */
.sys-card {
background: var(--color-card);
border-radius: var(--radius-md);
box-shadow: var(--shadow-soft);
padding: var(--space-6);
position: relative;
overflow: hidden;
}
.sys-card--lg { padding: var(--space-8); }
.sys-card--dark {
background: var(--color-dark-bg);
color: var(--color-dark-fg);
}
.sys-card--accent {
border-left: 3px solid var(--color-accent);
}
.sys-card__code-texture {
position: absolute;
top: 20px;
right: 20px;
font-family: var(--font-mono);
font-size: 8px;
line-height: 1.4;
color: var(--color-fg-ghost);
text-align: right;
pointer-events: none;
white-space: pre;
}
.sys-card--dark .sys-card__code-texture {
color: rgba(255, 255, 255, 0.08);
}
/* ----- BADGE ----- */
.sys-badge {
display: inline-flex;
align-items: center;
background: var(--color-card);
color: var(--color-fg-muted);
border: 1px solid #DDDDDD;
border-radius: var(--radius-sm);
padding: 2px 8px;
font-family: var(--font-mono);
font-size: 9px;
line-height: 1.4;
letter-spacing: var(--button-letter-spacing);
text-transform: uppercase;
}
.sys-badge--filled {
background: var(--color-accent);
color: var(--color-fg);
border-color: var(--color-accent);
}
/* ----- DIALOG ----- */
.sys-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(13, 13, 13, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
animation: sys-fade-in 200ms var(--easing);
}
.sys-dialog {
background: var(--color-card);
border-radius: var(--radius-md);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
max-width: 480px;
width: 100%;
padding: var(--space-8);
position: relative;
animation: sys-pop-in 220ms var(--easing);
}
.sys-dialog__title {
font-family: var(--font-display);
font-weight: var(--weight-normal);
font-size: 28px;
line-height: 1.1;
letter-spacing: -0.5px;
margin: 0 0 var(--space-3);
}
.sys-dialog__close {
position: absolute;
top: 16px;
right: 16px;
width: 28px;
height: 28px;
border: 1px solid #DDDDDD;
border-radius: var(--radius-sm);
background: var(--color-card);
cursor: pointer;
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-fg-muted);
line-height: 1;
}
.sys-dialog__close:hover {
background: var(--color-fg);
color: var(--color-card);
border-color: var(--color-fg);
}
@keyframes sys-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes sys-pop-in {
from { opacity: 0; transform: translateY(8px) scale(0.99); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ----- TABS — segmented nav-items ----- */
.sys-tabs {
display: inline-flex;
background: var(--color-card);
border: 1px solid var(--color-divider);
padding: 0;
gap: 0;
}
.sys-tabs__btn {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
font-size: var(--text-sm);
padding: 10px 16px;
border: none;
background: transparent;
color: var(--color-fg);
cursor: pointer;
border-radius: 0;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
line-height: 1;
}
.sys-tabs__btn:not(.sys-tabs__btn--active):hover {
background: var(--color-divider);
}
.sys-tabs__btn--active {
background: var(--color-fg);
color: var(--color-card);
}
.sys-tabs__btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: -2px;
}
/* ----- SWITCH — square checkbox ----- */
.sys-switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
font-family: var(--font-mono);
font-size: var(--text-sm);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
}
.sys-switch__box {
width: 18px;
height: 18px;
border: 2px solid #DDDDDD;
border-radius: var(--radius-md);
background: transparent;
position: relative;
flex-shrink: 0;
transition: background var(--duration-base) var(--easing),
border-color var(--duration-base) var(--easing);
display: flex;
align-items: center;
justify-content: center;
}
.sys-switch__check {
width: 10px;
height: 10px;
color: var(--color-card);
opacity: 0;
transition: opacity var(--duration-fast) var(--easing);
}
.sys-switch__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.sys-switch__input:checked + .sys-switch__box {
background: var(--color-accent);
border-color: var(--color-accent);
}
.sys-switch__input:checked + .sys-switch__box .sys-switch__check {
opacity: 1;
color: var(--color-fg);
}
.sys-switch__input:focus-visible + .sys-switch__box {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* terminal switch variant — "[X]/[ ] LABEL" pattern */
.sys-switch--terminal {
gap: 8px;
}
.sys-switch--terminal .sys-switch__box {
display: none;
}
.sys-switch--terminal .sys-switch__bracket {
font-family: var(--font-mono);
font-size: var(--text-base);
color: var(--color-fg);
line-height: 1;
}
.sys-switch--terminal .sys-switch__input:not(:checked) ~ .sys-switch__bracket,
.sys-switch--terminal .sys-switch__input:not(:checked) ~ * {
opacity: 0.5;
}
.sys-switch--terminal .sys-switch__input:checked ~ .sys-switch__bracket,
.sys-switch--terminal .sys-switch__input:checked ~ * {
opacity: 1;
}
/* ----- TOAST ----- */
.sys-toast-region {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 1100;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
width: max-content;
max-width: calc(100vw - 48px);
}
.sys-toast {
background: var(--color-card);
color: var(--color-fg);
border-radius: var(--radius-md);
box-shadow: var(--shadow-soft);
padding: 14px 18px;
border-left: 3px solid var(--color-accent);
font-family: var(--font-mono);
font-size: var(--text-xs);
pointer-events: auto;
animation: sys-toast-in 280ms var(--easing);
display: grid;
grid-template-columns: 1fr auto;
gap: 8px 16px;
min-width: 280px;
align-items: baseline;
}
.sys-toast__title {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
font-size: var(--text-xs);
}
.sys-toast__time {
font-family: var(--font-mono);
font-size: 10px;
color: var(--color-fg-muted);
text-align: right;
grid-column: 2;
grid-row: 1;
}
.sys-toast__desc {
font-family: var(--font-mono);
font-size: 11px;
color: var(--color-fg-muted);
grid-column: 1 / -1;
line-height: 1.5;
}
.sys-toast--success { border-left-color: var(--color-accent); }
.sys-toast--error { border-left-color: var(--color-fail); }
.sys-toast--default { border-left-color: var(--color-fg); }
@keyframes sys-toast-in {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
/* ----- BLINKING CURSOR ----- */
.sys-cursor {
display: inline-block;
width: 8px;
height: 14px;
background: var(--color-accent);
animation: sys-blink 1s step-end infinite;
vertical-align: middle;
margin-left: 4px;
}
.sys-cursor--fg {
background: var(--color-fg);
}
@keyframes sys-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
Step 5 — Wrap the section/page you want themed with <div data-system="system">:
<div data-system="system">
<Button>HELLO SYSTEM</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in System.
Install the Wardrobe System theme in my Lovable project. Each step below maps to a specific file or change.
Step 1 — Add this to globals.css:
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Space+Grotesk:wght@300;400;500;600&display=swap");
/* =========================================================
Wardrobe — SYSTEM tokens
Scoped via :where() for low specificity.
Activate by adding data-system="system" to a body or wrapper.
========================================================= */
:where([data-system="system"]) {
/* Surface */
--color-bg: #F4F4F4;
--color-card: #FFFFFF;
--color-fg: #0D0D0D;
--color-fg-muted: #888888;
--color-fg-faint: #BBBBBB;
--color-fg-ghost: #EEEEEE;
--color-divider: #F0F0F0;
/* Accents */
--color-accent: #6CEFA0;
--color-accent-blue: #6CDDEF;
--color-accent-purple: #B06CEF;
--color-accent-orange: #EF9B6C;
--color-success: #6CEFA0;
--color-fail: #FF6B6B;
/* Dark inversions (analysis panels in light dashboards) */
--color-dark-bg: #1A1A1A;
--color-dark-fg: #DDDDDD;
--color-dark-muted: #888888;
/* Borders */
--border: 1px solid rgba(0, 0, 0, 0.05);
--border-strong: 1px solid #EEEEEE;
--border-color: rgba(0, 0, 0, 0.05);
/* Radius — sharp! 2px MAX. Never pill. */
--radius-sm: 1px;
--radius-md: 2px;
--radius-lg: 2px;
--radius-full: 9999px;
/* Typography */
--font-display: "Space Grotesk", -apple-system, BlinkMacSystemFont, sans-serif;
--font-body: "Space Grotesk", -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-micro: 10px;
--text-xs: 11px;
--text-sm: 12px;
--text-base: 13px;
--text-md: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 48px;
--text-6xl: 64px;
--display-letter-spacing: -2px;
--display-line-height: 1;
--label-letter-spacing: 1.5px;
--button-letter-spacing: 1px;
/* Weights */
--weight-light: 300;
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
/* Spacing */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
/* Shadow — soft only. Borders are nearly invisible. */
--shadow-soft: 0 4px 20px rgba(0, 0, 0, 0.04);
--shadow-flat: 0 1px 2px rgba(0, 0, 0, 0.03);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="system"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
Step 2 — Create components/ui/wardrobe/cn.ts:
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
Step 3 — Create the following 10 component files in components/ui/wardrobe/:
--- components/ui/wardrobe/Button.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive" | "mint";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn("sys-btn", `sys-btn--${variant}`, `sys-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
--- components/ui/wardrobe/Input.tsx ---
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
display?: boolean;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
export function Input({
size = "md",
variant = "default",
label,
hint,
display,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<label className="sys-field" htmlFor={inputId}>
{label && <span className="sys-field__label">{label}</span>}
<input
id={inputId}
className={cn(
"sys-input",
`sys-input--${size}`,
variant === "error" && "sys-input--error",
display && "sys-input--display",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"sys-field__hint",
variant === "error" && "sys-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Textarea.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TextareaProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
} & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">;
export function Textarea({
size = "md",
variant = "default",
label,
hint,
className,
id,
...props
}: TextareaProps) {
const inputId = id ?? React.useId();
return (
<label className="sys-field" htmlFor={inputId}>
{label && <span className="sys-field__label">{label}</span>}
<textarea
id={inputId}
className={cn(
"sys-textarea",
`sys-input--${size}`,
variant === "error" && "sys-textarea--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"sys-field__hint",
variant === "error" && "sys-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Select.tsx ---
import * as React from "react";
import { cn } from "./cn";
type SelectProps = {
options: Array<{ value: string; label: string }>;
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
label?: string;
} & Omit<
React.SelectHTMLAttributes<HTMLSelectElement>,
"value" | "onChange" | "size"
>;
export function Select({
options,
value,
onValueChange,
placeholder,
label,
className,
id,
...props
}: SelectProps) {
const inputId = id ?? React.useId();
return (
<label className="sys-field" htmlFor={inputId}>
{label && <span className="sys-field__label">{label}</span>}
<select
id={inputId}
value={value}
onChange={(e) => onValueChange(e.target.value)}
className={cn("sys-select", className)}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</label>
);
}
--- components/ui/wardrobe/Card.tsx ---
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "dark" | "accent";
size?: "default" | "lg";
codeTexture?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
size = "default",
codeTexture,
className,
children,
...props
}: CardProps) {
return (
<div
className={cn(
"sys-card",
variant !== "default" && `sys-card--${variant}`,
size !== "default" && `sys-card--${size}`,
className,
)}
{...props}
>
{codeTexture && (
<span className="sys-card__code-texture">{codeTexture}</span>
)}
{children}
</div>
);
}
--- components/ui/wardrobe/Badge.tsx ---
import * as React from "react";
import { cn } from "./cn";
type BadgeProps = {
variant?: "default" | "filled";
children: React.ReactNode;
className?: string;
};
export function Badge({ variant = "default", children, className }: BadgeProps) {
return (
<span
className={cn(
"sys-badge",
variant === "filled" && "sys-badge--filled",
className,
)}
>
{children}
</span>
);
}
--- components/ui/wardrobe/Dialog.tsx ---
import * as React from "react";
type DialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
children: React.ReactNode;
};
export function Dialog({ open, onOpenChange, title, children }: DialogProps) {
const dialogRef = React.useRef<HTMLDivElement>(null);
const previousFocus = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
if (!open) return;
previousFocus.current = document.activeElement as HTMLElement;
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
focusable?.[0]?.focus();
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") {
e.preventDefault();
onOpenChange(false);
return;
}
if (e.key !== "Tab" || !focusable || focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
previousFocus.current?.focus();
};
}, [open, onOpenChange]);
if (!open) return null;
return (
<div
className="sys-dialog-backdrop"
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div
ref={dialogRef}
className="sys-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="sys-dialog-title"
>
<button
type="button"
className="sys-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
×
</button>
<h2 id="sys-dialog-title" className="sys-dialog__title">
{title}
</h2>
{children}
</div>
</div>
);
}
--- components/ui/wardrobe/Tabs.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TabsProps = {
tabs: Array<{ id: string; label: string }>;
activeId: string;
onTabChange: (id: string) => void;
className?: string;
};
export function Tabs({ tabs, activeId, onTabChange, className }: TabsProps) {
return (
<div role="tablist" className={cn("sys-tabs", className)}>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={tab.id === activeId}
type="button"
className={cn(
"sys-tabs__btn",
tab.id === activeId && "sys-tabs__btn--active",
)}
onClick={() => onTabChange(tab.id)}
onKeyDown={(e) => {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
e.preventDefault();
const idx = tabs.findIndex((t) => t.id === activeId);
const next =
e.key === "ArrowRight"
? (idx + 1) % tabs.length
: (idx - 1 + tabs.length) % tabs.length;
onTabChange(tabs[next].id);
}}
>
{tab.label}
</button>
))}
</div>
);
}
--- components/ui/wardrobe/Switch.tsx ---
import * as React from "react";
import { cn } from "./cn";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
id?: string;
terminal?: boolean;
};
export function Switch({
checked,
onCheckedChange,
label,
id,
terminal,
}: SwitchProps) {
const inputId = id ?? React.useId();
return (
<label
className={cn("sys-switch", terminal && "sys-switch--terminal")}
htmlFor={inputId}
>
<input
id={inputId}
type="checkbox"
role="switch"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
className="sys-switch__input"
/>
{terminal ? (
<>
<span className="sys-switch__bracket">
{checked ? "[X]" : "[ ]"}
</span>
{label && <span>{label}</span>}
</>
) : (
<>
<span className="sys-switch__box">
<svg
className="sys-switch__check"
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
<polyline points="2 6 5 9 10 3" />
</svg>
</span>
{label && <span>{label}</span>}
</>
)}
</label>
);
}
--- components/ui/wardrobe/Toast.tsx ---
import * as React from "react";
import { cn } from "./cn";
export type ToastVariant = "default" | "success" | "error";
export type ToastProps = {
title: string;
description?: string;
variant?: ToastVariant;
};
type ToastEntry = ToastProps & { id: number; time: string };
type ToastContextValue = {
show: (toast: ToastProps) => void;
};
const ToastContext = React.createContext<ToastContextValue | null>(null);
export function useToast() {
const ctx = React.useContext(ToastContext);
if (!ctx) throw new Error("useToast must be used within <ToastProvider>");
return ctx;
}
function timestamp(): string {
const d = new Date();
const pad = (n: number) => String(n).padStart(2, "0");
const ms = String(Math.floor(d.getMilliseconds() / 10)).padStart(2, "0");
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${ms}`;
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<ToastEntry[]>([]);
const idRef = React.useRef(0);
const show = React.useCallback((toast: ToastProps) => {
const id = ++idRef.current;
setToasts((prev) => [...prev, { ...toast, id, time: timestamp() }]);
window.setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 4000);
}, []);
return (
<ToastContext.Provider value={{ show }}>
{children}
<div
className="sys-toast-region"
role="region"
aria-label="Notifications"
aria-live="polite"
>
{toasts.map((t) => (
<Toast key={t.id} {...t} />
))}
</div>
</ToastContext.Provider>
);
}
type ToastViewProps = ToastProps & { time?: string };
export function Toast({
title,
description,
variant = "default",
time,
}: ToastViewProps) {
return (
<div
className={cn(
"sys-toast",
variant === "success" && "sys-toast--success",
variant === "error" && "sys-toast--error",
variant === "default" && "sys-toast--default",
)}
role="status"
>
<span className="sys-toast__title">{title}</span>
{time && <span className="sys-toast__time">{time}</span>}
{description && (
<span className="sys-toast__desc">{description}</span>
)}
</div>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — SYSTEM globals
Loads display + mono fonts and base component styles.
Only active on elements with data-system="system".
========================================================= */
[data-system="system"] *,
[data-system="system"] *::before,
[data-system="system"] *::after {
box-sizing: border-box;
}
[data-system="system"] {
min-height: 100vh;
}
/* ----- LABEL MICRO — the System signature ----- */
.sys-label-micro {
font-family: var(--font-mono);
font-size: var(--text-micro);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
color: var(--color-fg-muted);
line-height: 1.4;
display: block;
}
/* ----- BUTTON ----- */
.sys-btn {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
font-size: var(--text-sm);
border: 1px solid var(--color-fg);
border-radius: var(--radius-md);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing),
opacity var(--duration-fast) var(--easing);
text-decoration: none;
line-height: 1;
background: var(--color-fg);
color: var(--color-card);
padding: 0 18px;
height: 40px;
}
.sys-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.sys-btn--primary {
background: var(--color-fg);
color: var(--color-card);
}
.sys-btn--primary:hover:not(:disabled) {
background: #2a2a2a;
}
.sys-btn--secondary {
background: var(--color-card);
color: var(--color-fg);
border: 1px solid var(--color-fg);
}
.sys-btn--secondary:hover:not(:disabled) {
background: var(--color-fg);
color: var(--color-card);
}
.sys-btn--destructive {
background: var(--color-fail);
color: #FFFFFF;
border-color: var(--color-fail);
}
.sys-btn--mint {
background: var(--color-accent);
color: var(--color-fg);
border-color: var(--color-accent);
}
.sys-btn--sm { height: 32px; padding: 0 14px; font-size: var(--text-xs); }
.sys-btn--md { height: 40px; padding: 0 18px; font-size: var(--text-sm); }
.sys-btn--lg { height: 48px; padding: 0 22px; font-size: var(--text-md); }
.sys-btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ----- INPUT / TEXTAREA — bottom-border only ----- */
.sys-field {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.sys-field__label { /* alias of sys-label-micro for in-field labels */
font-family: var(--font-mono);
font-size: var(--text-micro);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
color: var(--color-fg-muted);
line-height: 1;
}
.sys-field__hint {
font-family: var(--font-mono);
font-size: var(--text-xs);
color: var(--color-fg-muted);
}
.sys-field__hint--error { color: var(--color-fail); }
.sys-input,
.sys-textarea {
font-family: var(--font-mono);
font-size: var(--text-md);
color: var(--color-fg);
background: transparent;
border: none;
border-bottom: 1px solid #E0E0E0;
border-radius: 0;
padding: 8px 0;
width: 100%;
outline: none;
transition: border-color var(--duration-base) var(--easing);
}
.sys-input:focus,
.sys-textarea:focus { border-bottom-color: var(--color-fg); }
.sys-input--error,
.sys-textarea--error { border-bottom-color: var(--color-fail); }
.sys-input::placeholder,
.sys-textarea::placeholder { color: var(--color-fg-faint); }
.sys-input--sm { font-size: var(--text-sm); padding: 6px 0; }
.sys-input--md { font-size: var(--text-md); }
.sys-input--lg { font-size: var(--text-xl); padding: 10px 0; }
.sys-input--display {
font-family: var(--font-display);
font-weight: var(--weight-light);
font-size: var(--text-5xl);
letter-spacing: var(--display-letter-spacing);
line-height: var(--display-line-height);
padding: 6px 0;
}
.sys-textarea {
min-height: 100px;
resize: vertical;
line-height: 1.55;
}
/* ----- SELECT ----- */
.sys-select {
font-family: var(--font-mono);
font-size: var(--text-sm);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
color: var(--color-fg);
background: var(--color-card);
border: 1px solid #E0E0E0;
border-radius: var(--radius-md);
padding: 10px 36px 10px 14px;
width: 100%;
appearance: none;
-webkit-appearance: none;
cursor: pointer;
transition: border-color var(--duration-base) var(--easing),
box-shadow var(--duration-base) var(--easing);
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%230D0D0D' stroke-width='2' stroke-linecap='square' stroke-linejoin='miter'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 14px center;
outline: none;
}
.sys-select:focus {
border-color: var(--color-fg);
box-shadow: var(--shadow-soft);
}
/* ----- CARD ----- */
.sys-card {
background: var(--color-card);
border-radius: var(--radius-md);
box-shadow: var(--shadow-soft);
padding: var(--space-6);
position: relative;
overflow: hidden;
}
.sys-card--lg { padding: var(--space-8); }
.sys-card--dark {
background: var(--color-dark-bg);
color: var(--color-dark-fg);
}
.sys-card--accent {
border-left: 3px solid var(--color-accent);
}
.sys-card__code-texture {
position: absolute;
top: 20px;
right: 20px;
font-family: var(--font-mono);
font-size: 8px;
line-height: 1.4;
color: var(--color-fg-ghost);
text-align: right;
pointer-events: none;
white-space: pre;
}
.sys-card--dark .sys-card__code-texture {
color: rgba(255, 255, 255, 0.08);
}
/* ----- BADGE ----- */
.sys-badge {
display: inline-flex;
align-items: center;
background: var(--color-card);
color: var(--color-fg-muted);
border: 1px solid #DDDDDD;
border-radius: var(--radius-sm);
padding: 2px 8px;
font-family: var(--font-mono);
font-size: 9px;
line-height: 1.4;
letter-spacing: var(--button-letter-spacing);
text-transform: uppercase;
}
.sys-badge--filled {
background: var(--color-accent);
color: var(--color-fg);
border-color: var(--color-accent);
}
/* ----- DIALOG ----- */
.sys-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(13, 13, 13, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
animation: sys-fade-in 200ms var(--easing);
}
.sys-dialog {
background: var(--color-card);
border-radius: var(--radius-md);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
max-width: 480px;
width: 100%;
padding: var(--space-8);
position: relative;
animation: sys-pop-in 220ms var(--easing);
}
.sys-dialog__title {
font-family: var(--font-display);
font-weight: var(--weight-normal);
font-size: 28px;
line-height: 1.1;
letter-spacing: -0.5px;
margin: 0 0 var(--space-3);
}
.sys-dialog__close {
position: absolute;
top: 16px;
right: 16px;
width: 28px;
height: 28px;
border: 1px solid #DDDDDD;
border-radius: var(--radius-sm);
background: var(--color-card);
cursor: pointer;
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--color-fg-muted);
line-height: 1;
}
.sys-dialog__close:hover {
background: var(--color-fg);
color: var(--color-card);
border-color: var(--color-fg);
}
@keyframes sys-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes sys-pop-in {
from { opacity: 0; transform: translateY(8px) scale(0.99); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ----- TABS — segmented nav-items ----- */
.sys-tabs {
display: inline-flex;
background: var(--color-card);
border: 1px solid var(--color-divider);
padding: 0;
gap: 0;
}
.sys-tabs__btn {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
font-size: var(--text-sm);
padding: 10px 16px;
border: none;
background: transparent;
color: var(--color-fg);
cursor: pointer;
border-radius: 0;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
line-height: 1;
}
.sys-tabs__btn:not(.sys-tabs__btn--active):hover {
background: var(--color-divider);
}
.sys-tabs__btn--active {
background: var(--color-fg);
color: var(--color-card);
}
.sys-tabs__btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: -2px;
}
/* ----- SWITCH — square checkbox ----- */
.sys-switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
font-family: var(--font-mono);
font-size: var(--text-sm);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
}
.sys-switch__box {
width: 18px;
height: 18px;
border: 2px solid #DDDDDD;
border-radius: var(--radius-md);
background: transparent;
position: relative;
flex-shrink: 0;
transition: background var(--duration-base) var(--easing),
border-color var(--duration-base) var(--easing);
display: flex;
align-items: center;
justify-content: center;
}
.sys-switch__check {
width: 10px;
height: 10px;
color: var(--color-card);
opacity: 0;
transition: opacity var(--duration-fast) var(--easing);
}
.sys-switch__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.sys-switch__input:checked + .sys-switch__box {
background: var(--color-accent);
border-color: var(--color-accent);
}
.sys-switch__input:checked + .sys-switch__box .sys-switch__check {
opacity: 1;
color: var(--color-fg);
}
.sys-switch__input:focus-visible + .sys-switch__box {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* terminal switch variant — "[X]/[ ] LABEL" pattern */
.sys-switch--terminal {
gap: 8px;
}
.sys-switch--terminal .sys-switch__box {
display: none;
}
.sys-switch--terminal .sys-switch__bracket {
font-family: var(--font-mono);
font-size: var(--text-base);
color: var(--color-fg);
line-height: 1;
}
.sys-switch--terminal .sys-switch__input:not(:checked) ~ .sys-switch__bracket,
.sys-switch--terminal .sys-switch__input:not(:checked) ~ * {
opacity: 0.5;
}
.sys-switch--terminal .sys-switch__input:checked ~ .sys-switch__bracket,
.sys-switch--terminal .sys-switch__input:checked ~ * {
opacity: 1;
}
/* ----- TOAST ----- */
.sys-toast-region {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 1100;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
width: max-content;
max-width: calc(100vw - 48px);
}
.sys-toast {
background: var(--color-card);
color: var(--color-fg);
border-radius: var(--radius-md);
box-shadow: var(--shadow-soft);
padding: 14px 18px;
border-left: 3px solid var(--color-accent);
font-family: var(--font-mono);
font-size: var(--text-xs);
pointer-events: auto;
animation: sys-toast-in 280ms var(--easing);
display: grid;
grid-template-columns: 1fr auto;
gap: 8px 16px;
min-width: 280px;
align-items: baseline;
}
.sys-toast__title {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: var(--button-letter-spacing);
font-size: var(--text-xs);
}
.sys-toast__time {
font-family: var(--font-mono);
font-size: 10px;
color: var(--color-fg-muted);
text-align: right;
grid-column: 2;
grid-row: 1;
}
.sys-toast__desc {
font-family: var(--font-mono);
font-size: 11px;
color: var(--color-fg-muted);
grid-column: 1 / -1;
line-height: 1.5;
}
.sys-toast--success { border-left-color: var(--color-accent); }
.sys-toast--error { border-left-color: var(--color-fail); }
.sys-toast--default { border-left-color: var(--color-fg); }
@keyframes sys-toast-in {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
/* ----- BLINKING CURSOR ----- */
.sys-cursor {
display: inline-block;
width: 8px;
height: 14px;
background: var(--color-accent);
animation: sys-blink 1s step-end infinite;
vertical-align: middle;
margin-left: 4px;
}
.sys-cursor--fg {
background: var(--color-fg);
}
@keyframes sys-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
Step 5 — Wrap the section/page you want themed with <div data-system="system">:
<div data-system="system">
<Button>HELLO SYSTEM</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in System.
Once it's installed, wrap whichever section you want themed:
<div data-system="system">
<Button>HELLO SYSTEM</Button>
<Card>...</Card>
</div> Want to see what it looks like in the wild? Sequence →
Got it working?
Drop your email — I'll send you the next theme + occasional builder notes.
No spam. ~1 email per theme drop. Maybe one extra when I ship something interesting.