WARDROBE / HYPER
HYPER.
Acid yellow. Brown borders. CAPS forever.
Stop building purple-gradient apps.
TOKENS.
Colors
acid yellow · #EFFF71
deep brown · #3A1E1E
pure white · #FFFFFF
pastel pink · #FFC1E3
pastel blue · #BCEFFF
pastel green · #C3FF8B
red flag · #FF4B4B
Typography
IS A TRACK.
Hyper is loud on purpose. The body copy is Georgia — old-school serif — so the display can scream while the reading stays grown-up. Don't replace it with Inter.
Radius & shadow
COMPONENTS.
Button.
Pill-shaped, CAPS forever. Hard offset shadow when you press.
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive";
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("hyp-btn", `hyp-btn--${variant}`, `hyp-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
Add this Wardrobe Hyper Button to my project.
1. Create components/ui/wardrobe/Button.tsx with the code below.
2. Make sure components/ui/wardrobe/cn.ts exists (utility shown at the bottom).
3. Add these CSS variables to your globals.css under [data-system="hyper"]:
--color-fg
--color-bg
--color-danger
--font-display
--radius-pill
--shadow-hard
--shadow-hard-sm
--border
--border-color
4. Add the font import to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
5. Wrap any section using this component with <div data-system="hyper">.
--- Button.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive";
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("hyp-btn", `hyp-btn--${variant}`, `hyp-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
--- cn.ts ---
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
/* =========================================================
Wardrobe — HYPER tokens
Scoped via :where() for low specificity.
Activate by adding data-system="hyper" to a body or wrapper.
========================================================= */
:where([data-system="hyper"]) {
/* Surface */
--color-bg: #EFFF71;
--color-fg: #3A1E1E;
--color-surface: #FFFFFF;
/* Pastels */
--color-pink: #FFC1E3;
--color-blue: #BCEFFF;
--color-green: #C3FF8B;
/* Status */
--color-success: #C3FF8B;
--color-warning: #FFC1E3;
--color-danger: #FF4B4B;
--color-info: #BCEFFF;
/* Border */
--border-width: 1px;
--border-color: #3A1E1E;
--border: var(--border-width) solid var(--border-color);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 32px;
--radius-full: 9999px;
/* Typography */
--font-display: "Anton", "Impact", "Arial Narrow", sans-serif;
--font-body: "Georgia", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 56px;
--text-6xl: 80px;
--text-7xl: 120px;
--display-line-height: 0.85;
--display-letter-spacing: -0.02em;
/* 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;
--space-16: 64px;
/* Shadows */
--shadow-hard: 4px 4px 0px var(--color-fg);
--shadow-hard-sm: 2px 2px 0px var(--color-fg);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.1);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--duration-slow: 400ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="hyper"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
Input.
White surface, brown border, focus shadows hard. Optional numeric badge.
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
numericBadge?: string;
display?: boolean;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
export function Input({
size = "md",
variant = "default",
label,
hint,
numericBadge,
display,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<label className="hyp-field" htmlFor={inputId}>
{label && <span className="hyp-field__label">{label}</span>}
{numericBadge && <span className="hyp-badge-circle">{numericBadge}</span>}
<input
id={inputId}
className={cn(
"hyp-input",
`hyp-input--${size}`,
variant === "error" && "hyp-input--error",
display && "hyp-input--display",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"hyp-field__hint",
variant === "error" && "hyp-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
Add this Wardrobe Hyper Input to my project.
1. Create components/ui/wardrobe/Input.tsx with the code below.
2. Make sure components/ui/wardrobe/cn.ts exists (utility shown at the bottom).
3. Add these CSS variables to your globals.css under [data-system="hyper"]:
--color-surface
--color-fg
--font-display
--font-body
--radius-md
--shadow-hard-sm
--border
4. Add the font import to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
5. Wrap any section using this component with <div data-system="hyper">.
--- Input.tsx ---
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
numericBadge?: string;
display?: boolean;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
export function Input({
size = "md",
variant = "default",
label,
hint,
numericBadge,
display,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<label className="hyp-field" htmlFor={inputId}>
{label && <span className="hyp-field__label">{label}</span>}
{numericBadge && <span className="hyp-badge-circle">{numericBadge}</span>}
<input
id={inputId}
className={cn(
"hyp-input",
`hyp-input--${size}`,
variant === "error" && "hyp-input--error",
display && "hyp-input--display",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"hyp-field__hint",
variant === "error" && "hyp-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- cn.ts ---
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
/* =========================================================
Wardrobe — HYPER tokens
Scoped via :where() for low specificity.
Activate by adding data-system="hyper" to a body or wrapper.
========================================================= */
:where([data-system="hyper"]) {
/* Surface */
--color-bg: #EFFF71;
--color-fg: #3A1E1E;
--color-surface: #FFFFFF;
/* Pastels */
--color-pink: #FFC1E3;
--color-blue: #BCEFFF;
--color-green: #C3FF8B;
/* Status */
--color-success: #C3FF8B;
--color-warning: #FFC1E3;
--color-danger: #FF4B4B;
--color-info: #BCEFFF;
/* Border */
--border-width: 1px;
--border-color: #3A1E1E;
--border: var(--border-width) solid var(--border-color);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 32px;
--radius-full: 9999px;
/* Typography */
--font-display: "Anton", "Impact", "Arial Narrow", sans-serif;
--font-body: "Georgia", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 56px;
--text-6xl: 80px;
--text-7xl: 120px;
--display-line-height: 0.85;
--display-letter-spacing: -0.02em;
/* 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;
--space-16: 64px;
/* Shadows */
--shadow-hard: 4px 4px 0px var(--color-fg);
--shadow-hard-sm: 2px 2px 0px var(--color-fg);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.1);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--duration-slow: 400ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="hyper"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
Textarea.
Same energy as Input, just taller. Body font for what you write.
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="hyp-field" htmlFor={inputId}>
{label && <span className="hyp-field__label">{label}</span>}
<textarea
id={inputId}
className={cn(
"hyp-textarea",
`hyp-input--${size}`,
variant === "error" && "hyp-textarea--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"hyp-field__hint",
variant === "error" && "hyp-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
Add this Wardrobe Hyper Textarea to my project.
1. Create components/ui/wardrobe/Textarea.tsx with the code below.
2. Make sure components/ui/wardrobe/cn.ts exists (utility shown at the bottom).
3. Add these CSS variables to your globals.css under [data-system="hyper"]:
--color-surface
--color-fg
--font-body
--radius-md
--border
4. Add the font import to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
5. Wrap any section using this component with <div data-system="hyper">.
--- 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="hyp-field" htmlFor={inputId}>
{label && <span className="hyp-field__label">{label}</span>}
<textarea
id={inputId}
className={cn(
"hyp-textarea",
`hyp-input--${size}`,
variant === "error" && "hyp-textarea--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"hyp-field__hint",
variant === "error" && "hyp-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- cn.ts ---
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
/* =========================================================
Wardrobe — HYPER tokens
Scoped via :where() for low specificity.
Activate by adding data-system="hyper" to a body or wrapper.
========================================================= */
:where([data-system="hyper"]) {
/* Surface */
--color-bg: #EFFF71;
--color-fg: #3A1E1E;
--color-surface: #FFFFFF;
/* Pastels */
--color-pink: #FFC1E3;
--color-blue: #BCEFFF;
--color-green: #C3FF8B;
/* Status */
--color-success: #C3FF8B;
--color-warning: #FFC1E3;
--color-danger: #FF4B4B;
--color-info: #BCEFFF;
/* Border */
--border-width: 1px;
--border-color: #3A1E1E;
--border: var(--border-width) solid var(--border-color);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 32px;
--radius-full: 9999px;
/* Typography */
--font-display: "Anton", "Impact", "Arial Narrow", sans-serif;
--font-body: "Georgia", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 56px;
--text-6xl: 80px;
--text-7xl: 120px;
--display-line-height: 0.85;
--display-letter-spacing: -0.02em;
/* 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;
--space-16: 64px;
/* Shadows */
--shadow-hard: 4px 4px 0px var(--color-fg);
--shadow-hard-sm: 2px 2px 0px var(--color-fg);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.1);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--duration-slow: 400ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="hyper"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
Select.
Caps display value, custom chevron, border-color matches everything.
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="hyp-field" htmlFor={inputId}>
{label && <span className="hyp-field__label">{label}</span>}
<select
id={inputId}
value={value}
onChange={(e) => onValueChange(e.target.value)}
className={cn("hyp-select", className)}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</label>
);
}
Add this Wardrobe Hyper Select to my project.
1. Create components/ui/wardrobe/Select.tsx with the code below.
2. Make sure components/ui/wardrobe/cn.ts exists (utility shown at the bottom).
3. Add these CSS variables to your globals.css under [data-system="hyper"]:
--color-surface
--color-fg
--font-display
--radius-md
--border
4. Add the font import to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
5. Wrap any section using this component with <div data-system="hyper">.
--- 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="hyp-field" htmlFor={inputId}>
{label && <span className="hyp-field__label">{label}</span>}
<select
id={inputId}
value={value}
onChange={(e) => onValueChange(e.target.value)}
className={cn("hyp-select", className)}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</label>
);
}
--- cn.ts ---
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
/* =========================================================
Wardrobe — HYPER tokens
Scoped via :where() for low specificity.
Activate by adding data-system="hyper" to a body or wrapper.
========================================================= */
:where([data-system="hyper"]) {
/* Surface */
--color-bg: #EFFF71;
--color-fg: #3A1E1E;
--color-surface: #FFFFFF;
/* Pastels */
--color-pink: #FFC1E3;
--color-blue: #BCEFFF;
--color-green: #C3FF8B;
/* Status */
--color-success: #C3FF8B;
--color-warning: #FFC1E3;
--color-danger: #FF4B4B;
--color-info: #BCEFFF;
/* Border */
--border-width: 1px;
--border-color: #3A1E1E;
--border: var(--border-width) solid var(--border-color);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 32px;
--radius-full: 9999px;
/* Typography */
--font-display: "Anton", "Impact", "Arial Narrow", sans-serif;
--font-body: "Georgia", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 56px;
--text-6xl: 80px;
--text-7xl: 120px;
--display-line-height: 0.85;
--display-letter-spacing: -0.02em;
/* 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;
--space-16: 64px;
/* Shadows */
--shadow-hard: 4px 4px 0px var(--color-fg);
--shadow-hard-sm: 2px 2px 0px var(--color-fg);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.1);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--duration-slow: 400ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="hyper"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
Card.
Pastel pink, blue, green, or white. Numeric badge lapping the corner.
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "pink" | "blue" | "green";
numericBadge?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
numericBadge,
className,
children,
...props
}: CardProps) {
return (
<div
className={cn(
"hyp-card",
variant !== "default" && `hyp-card--${variant}`,
className,
)}
{...props}
>
{numericBadge && <span className="hyp-badge-circle">{numericBadge}</span>}
{children}
</div>
);
}
Add this Wardrobe Hyper Card to my project.
1. Create components/ui/wardrobe/Card.tsx with the code below.
2. Make sure components/ui/wardrobe/cn.ts exists (utility shown at the bottom).
3. Add these CSS variables to your globals.css under [data-system="hyper"]:
--color-surface
--color-pink
--color-blue
--color-green
--radius-md
--border
4. Add the font import to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
5. Wrap any section using this component with <div data-system="hyper">.
--- Card.tsx ---
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "pink" | "blue" | "green";
numericBadge?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
numericBadge,
className,
children,
...props
}: CardProps) {
return (
<div
className={cn(
"hyp-card",
variant !== "default" && `hyp-card--${variant}`,
className,
)}
{...props}
>
{numericBadge && <span className="hyp-badge-circle">{numericBadge}</span>}
{children}
</div>
);
}
--- cn.ts ---
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
/* =========================================================
Wardrobe — HYPER tokens
Scoped via :where() for low specificity.
Activate by adding data-system="hyper" to a body or wrapper.
========================================================= */
:where([data-system="hyper"]) {
/* Surface */
--color-bg: #EFFF71;
--color-fg: #3A1E1E;
--color-surface: #FFFFFF;
/* Pastels */
--color-pink: #FFC1E3;
--color-blue: #BCEFFF;
--color-green: #C3FF8B;
/* Status */
--color-success: #C3FF8B;
--color-warning: #FFC1E3;
--color-danger: #FF4B4B;
--color-info: #BCEFFF;
/* Border */
--border-width: 1px;
--border-color: #3A1E1E;
--border: var(--border-width) solid var(--border-color);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 32px;
--radius-full: 9999px;
/* Typography */
--font-display: "Anton", "Impact", "Arial Narrow", sans-serif;
--font-body: "Georgia", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 56px;
--text-6xl: 80px;
--text-7xl: 120px;
--display-line-height: 0.85;
--display-letter-spacing: -0.02em;
/* 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;
--space-16: 64px;
/* Shadows */
--shadow-hard: 4px 4px 0px var(--color-fg);
--shadow-hard-sm: 2px 2px 0px var(--color-fg);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.1);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--duration-slow: 400ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="hyper"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
Badge.
Inline pill. Outline by default, brown-filled for live status.
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(
"hyp-badge",
variant === "filled" && "hyp-badge--filled",
className,
)}
>
{children}
</span>
);
}
Add this Wardrobe Hyper Badge to my project.
1. Create components/ui/wardrobe/Badge.tsx with the code below.
2. Make sure components/ui/wardrobe/cn.ts exists (utility shown at the bottom).
3. Add these CSS variables to your globals.css under [data-system="hyper"]:
--color-surface
--color-fg
--color-bg
--radius-pill
--border
4. Add the font import to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
5. Wrap any section using this component with <div data-system="hyper">.
--- 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(
"hyp-badge",
variant === "filled" && "hyp-badge--filled",
className,
)}
>
{children}
</span>
);
}
--- cn.ts ---
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
/* =========================================================
Wardrobe — HYPER tokens
Scoped via :where() for low specificity.
Activate by adding data-system="hyper" to a body or wrapper.
========================================================= */
:where([data-system="hyper"]) {
/* Surface */
--color-bg: #EFFF71;
--color-fg: #3A1E1E;
--color-surface: #FFFFFF;
/* Pastels */
--color-pink: #FFC1E3;
--color-blue: #BCEFFF;
--color-green: #C3FF8B;
/* Status */
--color-success: #C3FF8B;
--color-warning: #FFC1E3;
--color-danger: #FF4B4B;
--color-info: #BCEFFF;
/* Border */
--border-width: 1px;
--border-color: #3A1E1E;
--border: var(--border-width) solid var(--border-color);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 32px;
--radius-full: 9999px;
/* Typography */
--font-display: "Anton", "Impact", "Arial Narrow", sans-serif;
--font-body: "Georgia", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 56px;
--text-6xl: 80px;
--text-7xl: 120px;
--display-line-height: 0.85;
--display-letter-spacing: -0.02em;
/* 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;
--space-16: 64px;
/* Shadows */
--shadow-hard: 4px 4px 0px var(--color-fg);
--shadow-hard-sm: 2px 2px 0px var(--color-fg);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.1);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--duration-slow: 400ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="hyper"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
Dialog.
Centered, hard shadow, brown veil. Escape to close. Focus trapped inside.
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="hyp-dialog-backdrop"
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div
ref={dialogRef}
className="hyp-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="hyp-dialog-title"
>
<button
type="button"
className="hyp-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
×
</button>
<h2 id="hyp-dialog-title" className="hyp-dialog__title">
{title}.
</h2>
{children}
</div>
</div>
);
}
Add this Wardrobe Hyper Dialog to my project.
1. Create components/ui/wardrobe/Dialog.tsx with the code below.
2. Make sure components/ui/wardrobe/cn.ts exists (utility shown at the bottom).
3. Add these CSS variables to your globals.css under [data-system="hyper"]:
--color-surface
--color-fg
--font-display
--radius-md
--shadow-hard
--border
4. Add the font import to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
5. Wrap any section using this component with <div data-system="hyper">.
--- 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="hyp-dialog-backdrop"
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div
ref={dialogRef}
className="hyp-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="hyp-dialog-title"
>
<button
type="button"
className="hyp-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
×
</button>
<h2 id="hyp-dialog-title" className="hyp-dialog__title">
{title}.
</h2>
{children}
</div>
</div>
);
}
--- cn.ts ---
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
/* =========================================================
Wardrobe — HYPER tokens
Scoped via :where() for low specificity.
Activate by adding data-system="hyper" to a body or wrapper.
========================================================= */
:where([data-system="hyper"]) {
/* Surface */
--color-bg: #EFFF71;
--color-fg: #3A1E1E;
--color-surface: #FFFFFF;
/* Pastels */
--color-pink: #FFC1E3;
--color-blue: #BCEFFF;
--color-green: #C3FF8B;
/* Status */
--color-success: #C3FF8B;
--color-warning: #FFC1E3;
--color-danger: #FF4B4B;
--color-info: #BCEFFF;
/* Border */
--border-width: 1px;
--border-color: #3A1E1E;
--border: var(--border-width) solid var(--border-color);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 32px;
--radius-full: 9999px;
/* Typography */
--font-display: "Anton", "Impact", "Arial Narrow", sans-serif;
--font-body: "Georgia", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 56px;
--text-6xl: 80px;
--text-7xl: 120px;
--display-line-height: 0.85;
--display-letter-spacing: -0.02em;
/* 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;
--space-16: 64px;
/* Shadows */
--shadow-hard: 4px 4px 0px var(--color-fg);
--shadow-hard-sm: 2px 2px 0px var(--color-fg);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.1);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--duration-slow: 400ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="hyper"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
Tabs.
Segmented control. Active tab fills brown. Arrow keys move you.
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("hyp-tabs", className)}>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={tab.id === activeId}
type="button"
className={cn(
"hyp-tabs__btn",
tab.id === activeId && "hyp-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>
);
}
Add this Wardrobe Hyper Tabs to my project.
1. Create components/ui/wardrobe/Tabs.tsx with the code below.
2. Make sure components/ui/wardrobe/cn.ts exists (utility shown at the bottom).
3. Add these CSS variables to your globals.css under [data-system="hyper"]:
--color-surface
--color-fg
--color-bg
--font-display
--radius-md
--radius-sm
--border
4. Add the font import to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
5. Wrap any section using this component with <div data-system="hyper">.
--- 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("hyp-tabs", className)}>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={tab.id === activeId}
type="button"
className={cn(
"hyp-tabs__btn",
tab.id === activeId && "hyp-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>
);
}
--- cn.ts ---
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
/* =========================================================
Wardrobe — HYPER tokens
Scoped via :where() for low specificity.
Activate by adding data-system="hyper" to a body or wrapper.
========================================================= */
:where([data-system="hyper"]) {
/* Surface */
--color-bg: #EFFF71;
--color-fg: #3A1E1E;
--color-surface: #FFFFFF;
/* Pastels */
--color-pink: #FFC1E3;
--color-blue: #BCEFFF;
--color-green: #C3FF8B;
/* Status */
--color-success: #C3FF8B;
--color-warning: #FFC1E3;
--color-danger: #FF4B4B;
--color-info: #BCEFFF;
/* Border */
--border-width: 1px;
--border-color: #3A1E1E;
--border: var(--border-width) solid var(--border-color);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 32px;
--radius-full: 9999px;
/* Typography */
--font-display: "Anton", "Impact", "Arial Narrow", sans-serif;
--font-body: "Georgia", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 56px;
--text-6xl: 80px;
--text-7xl: 120px;
--display-line-height: 0.85;
--display-letter-spacing: -0.02em;
/* 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;
--space-16: 64px;
/* Shadows */
--shadow-hard: 4px 4px 0px var(--color-fg);
--shadow-hard-sm: 2px 2px 0px var(--color-fg);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.1);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--duration-slow: 400ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="hyper"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
Switch.
Pill track, brown thumb. Flips to brown-filled with acid thumb.
import * as React from "react";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
id?: string;
};
export function Switch({ checked, onCheckedChange, label, id }: SwitchProps) {
const inputId = id ?? React.useId();
return (
<label className="hyp-switch" htmlFor={inputId}>
<input
id={inputId}
type="checkbox"
role="switch"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
className="hyp-switch__input"
/>
<span className="hyp-switch__track">
<span className="hyp-switch__thumb" />
</span>
{label && <span>{label}</span>}
</label>
);
}
Add this Wardrobe Hyper Switch to my project.
1. Create components/ui/wardrobe/Switch.tsx with the code below.
2. Make sure components/ui/wardrobe/cn.ts exists (utility shown at the bottom).
3. Add these CSS variables to your globals.css under [data-system="hyper"]:
--color-fg
--color-bg
--radius-pill
--border
4. Add the font import to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
5. Wrap any section using this component with <div data-system="hyper">.
--- Switch.tsx ---
import * as React from "react";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
id?: string;
};
export function Switch({ checked, onCheckedChange, label, id }: SwitchProps) {
const inputId = id ?? React.useId();
return (
<label className="hyp-switch" htmlFor={inputId}>
<input
id={inputId}
type="checkbox"
role="switch"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
className="hyp-switch__input"
/>
<span className="hyp-switch__track">
<span className="hyp-switch__thumb" />
</span>
{label && <span>{label}</span>}
</label>
);
}
--- cn.ts ---
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
/* =========================================================
Wardrobe — HYPER tokens
Scoped via :where() for low specificity.
Activate by adding data-system="hyper" to a body or wrapper.
========================================================= */
:where([data-system="hyper"]) {
/* Surface */
--color-bg: #EFFF71;
--color-fg: #3A1E1E;
--color-surface: #FFFFFF;
/* Pastels */
--color-pink: #FFC1E3;
--color-blue: #BCEFFF;
--color-green: #C3FF8B;
/* Status */
--color-success: #C3FF8B;
--color-warning: #FFC1E3;
--color-danger: #FF4B4B;
--color-info: #BCEFFF;
/* Border */
--border-width: 1px;
--border-color: #3A1E1E;
--border: var(--border-width) solid var(--border-color);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 32px;
--radius-full: 9999px;
/* Typography */
--font-display: "Anton", "Impact", "Arial Narrow", sans-serif;
--font-body: "Georgia", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 56px;
--text-6xl: 80px;
--text-7xl: 120px;
--display-line-height: 0.85;
--display-letter-spacing: -0.02em;
/* 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;
--space-16: 64px;
/* Shadows */
--shadow-hard: 4px 4px 0px var(--color-fg);
--shadow-hard-sm: 2px 2px 0px var(--color-fg);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.1);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--duration-slow: 400ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="hyper"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
Toast.
Slides in from below. Auto-dismisses after 4s. Success swaps to green.
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 };
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;
}
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 }]);
window.setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 4000);
}, []);
return (
<ToastContext.Provider value={{ show }}>
{children}
<div
className="hyp-toast-region"
role="region"
aria-label="Notifications"
aria-live="polite"
>
{toasts.map((t) => (
<Toast key={t.id} {...t} />
))}
</div>
</ToastContext.Provider>
);
}
export function Toast({ title, description, variant = "default" }: ToastProps) {
return (
<div
className={cn(
"hyp-toast",
variant === "success" && "hyp-toast--success",
variant === "error" && "hyp-toast--error",
)}
role="status"
>
<span>{title}</span>
{description && <span className="hyp-toast__desc">{description}</span>}
</div>
);
}
Add this Wardrobe Hyper Toast to my project.
1. Create components/ui/wardrobe/Toast.tsx with the code below.
2. Make sure components/ui/wardrobe/cn.ts exists (utility shown at the bottom).
3. Add these CSS variables to your globals.css under [data-system="hyper"]:
--color-fg
--color-bg
--color-success
--color-danger
--font-display
--radius-pill
--shadow-hard-sm
--border
4. Add the font import to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
5. Wrap any section using this component with <div data-system="hyper">.
--- 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 };
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;
}
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 }]);
window.setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 4000);
}, []);
return (
<ToastContext.Provider value={{ show }}>
{children}
<div
className="hyp-toast-region"
role="region"
aria-label="Notifications"
aria-live="polite"
>
{toasts.map((t) => (
<Toast key={t.id} {...t} />
))}
</div>
</ToastContext.Provider>
);
}
export function Toast({ title, description, variant = "default" }: ToastProps) {
return (
<div
className={cn(
"hyp-toast",
variant === "success" && "hyp-toast--success",
variant === "error" && "hyp-toast--error",
)}
role="status"
>
<span>{title}</span>
{description && <span className="hyp-toast__desc">{description}</span>}
</div>
);
}
--- cn.ts ---
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
/* =========================================================
Wardrobe — HYPER tokens
Scoped via :where() for low specificity.
Activate by adding data-system="hyper" to a body or wrapper.
========================================================= */
:where([data-system="hyper"]) {
/* Surface */
--color-bg: #EFFF71;
--color-fg: #3A1E1E;
--color-surface: #FFFFFF;
/* Pastels */
--color-pink: #FFC1E3;
--color-blue: #BCEFFF;
--color-green: #C3FF8B;
/* Status */
--color-success: #C3FF8B;
--color-warning: #FFC1E3;
--color-danger: #FF4B4B;
--color-info: #BCEFFF;
/* Border */
--border-width: 1px;
--border-color: #3A1E1E;
--border: var(--border-width) solid var(--border-color);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 32px;
--radius-full: 9999px;
/* Typography */
--font-display: "Anton", "Impact", "Arial Narrow", sans-serif;
--font-body: "Georgia", serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
/* Type scale */
--text-xs: 10px;
--text-sm: 12px;
--text-base: 14px;
--text-md: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
--text-3xl: 32px;
--text-4xl: 42px;
--text-5xl: 56px;
--text-6xl: 80px;
--text-7xl: 120px;
--display-line-height: 0.85;
--display-letter-spacing: -0.02em;
/* 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;
--space-16: 64px;
/* Shadows */
--shadow-hard: 4px 4px 0px var(--color-fg);
--shadow-hard-sm: 2px 2px 0px var(--color-fg);
--shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.1);
/* Motion */
--duration-fast: 100ms;
--duration-base: 200ms;
--duration-slow: 400ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="hyper"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@import url("https://fonts.googleapis.com/css2?family=Anton&display=swap");
INSTALL.
One paste, one prompt, your project is wearing Hyper. No npm install, no config files, no purple gradients.
BIG GREEN INSTALL BUTTON →MCP COMING SOON.
Wardrobe MCP server is in the works. Soon you'll just say "install hyper" to your AI and it'll be done. Sign up below for the drop.
SEE IT LIVE.
Marathon Club — a fictional running club using Hyper end-to-end.