WARDROBE / HYPER / INSTALL
INSTALL HYPER
IN 30 SECONDS.
Pick your AI. Hit copy. Paste it into a fresh chat in your project. Watch the theme arrive.
Install the Wardrobe Hyper 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=Anton&display=swap");
/* =========================================================
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;
}
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";
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>
);
}
--- 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;
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>
);
}
--- 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="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>
);
}
--- 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="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>
);
}
--- components/ui/wardrobe/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>
);
}
--- 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(
"hyp-badge",
variant === "filled" && "hyp-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="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>
);
}
--- 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("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>
);
}
--- components/ui/wardrobe/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>
);
}
--- 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 };
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>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — HYPER globals
Loads display font + base component styles.
Only active on elements with data-system="hyper".
========================================================= */
[data-system="hyper"] *,
[data-system="hyper"] *::before,
[data-system="hyper"] *::after {
box-sizing: border-box;
}
[data-system="hyper"] {
min-height: 100vh;
}
/* ----- BUTTON ----- */
.hyp-btn {
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
border: var(--border);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: transform var(--duration-fast) var(--easing),
box-shadow var(--duration-fast) var(--easing),
background var(--duration-base) var(--easing),
color var(--duration-base) var(--easing);
text-decoration: none;
line-height: 1;
}
.hyp-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.hyp-btn--primary {
background: var(--color-fg);
color: var(--color-bg);
border-radius: var(--radius-pill);
}
.hyp-btn--secondary {
background: var(--color-surface);
color: var(--color-fg);
border-radius: var(--radius-md);
}
.hyp-btn--destructive {
background: var(--color-danger);
color: #FFFFFF;
border-radius: var(--radius-pill);
}
.hyp-btn--sm { height: 32px; padding: 0 16px; font-size: var(--text-sm); }
.hyp-btn--md { height: 48px; padding: 0 24px; font-size: var(--text-md); }
.hyp-btn--lg { height: 60px; padding: 0 32px; font-size: var(--text-xl); }
.hyp-btn:not(:disabled):hover {
transform: translate(-1px, -1px);
box-shadow: var(--shadow-hard-sm);
}
.hyp-btn:not(:disabled):active {
transform: translate(-2px, -2px);
box-shadow: var(--shadow-hard);
}
.hyp-btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 3px;
}
/* ----- INPUT / TEXTAREA ----- */
.hyp-field {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
position: relative;
}
.hyp-field__label {
font-family: var(--font-display);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1;
}
.hyp-field__hint {
font-family: var(--font-body);
font-size: var(--text-xs);
opacity: 0.7;
}
.hyp-field__hint--error { color: var(--color-danger); opacity: 1; }
.hyp-input,
.hyp-textarea,
.hyp-select {
font-family: var(--font-body);
font-size: var(--text-md);
color: var(--color-fg);
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
padding: 12px 16px;
width: 100%;
outline: none;
transition: box-shadow var(--duration-fast) var(--easing),
transform var(--duration-fast) var(--easing);
}
.hyp-input:focus,
.hyp-textarea:focus,
.hyp-select:focus {
box-shadow: var(--shadow-hard-sm);
transform: translate(-1px, -1px);
}
.hyp-input--error,
.hyp-textarea--error { border-color: var(--color-danger); }
.hyp-input::placeholder,
.hyp-textarea::placeholder { color: rgba(58, 30, 30, 0.5); }
.hyp-input--sm { height: 36px; padding: 8px 12px; font-size: var(--text-sm); }
.hyp-input--md { height: 48px; }
.hyp-input--lg { height: 56px; font-size: var(--text-lg); }
.hyp-input--display {
font-family: var(--font-display);
font-size: var(--text-5xl);
line-height: 1;
letter-spacing: -0.02em;
text-transform: uppercase;
height: auto;
padding: 16px;
}
.hyp-textarea {
min-height: 120px;
resize: vertical;
font-family: var(--font-body);
}
/* ----- SELECT ----- */
.hyp-select {
appearance: none;
-webkit-appearance: none;
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
padding-right: 40px;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%233A1E1E' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 16px center;
cursor: pointer;
}
/* ----- CARD ----- */
.hyp-card {
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
padding: var(--space-5);
position: relative;
}
.hyp-card--pink { background: var(--color-pink); }
.hyp-card--blue { background: var(--color-blue); }
.hyp-card--green { background: var(--color-green); }
.hyp-badge-circle {
position: absolute;
top: -10px;
right: 20px;
width: 24px;
height: 24px;
background: var(--color-surface);
border: var(--border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-body);
font-size: var(--text-xs);
z-index: 2;
}
/* ----- BADGE (inline pill) ----- */
.hyp-badge {
display: inline-flex;
align-items: center;
background: var(--color-surface);
color: var(--color-fg);
border: var(--border);
border-radius: var(--radius-pill);
padding: 4px 10px;
font-family: var(--font-body);
font-size: var(--text-xs);
line-height: 1.2;
letter-spacing: 0.02em;
}
.hyp-badge--filled {
background: var(--color-fg);
color: var(--color-bg);
}
/* ----- DIALOG ----- */
.hyp-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(58, 30, 30, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
animation: hyp-fade-in 200ms var(--easing);
}
.hyp-dialog {
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-hard);
max-width: 520px;
width: 100%;
padding: var(--space-8);
position: relative;
animation: hyp-pop-in 220ms var(--easing);
}
.hyp-dialog__title {
font-family: var(--font-display);
text-transform: uppercase;
font-size: var(--text-3xl);
line-height: var(--display-line-height);
letter-spacing: var(--display-letter-spacing);
margin: 0 0 var(--space-4);
}
.hyp-dialog__close {
position: absolute;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
border: var(--border);
border-radius: 50%;
background: var(--color-surface);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-body);
font-size: var(--text-md);
color: var(--color-fg);
line-height: 1;
}
.hyp-dialog__close:hover {
background: var(--color-fg);
color: var(--color-bg);
}
@keyframes hyp-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes hyp-pop-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ----- TABS ----- */
.hyp-tabs {
display: inline-flex;
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
padding: 4px;
gap: 0;
}
.hyp-tabs__btn {
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: var(--text-sm);
padding: 8px 16px;
border: none;
background: transparent;
color: var(--color-fg);
cursor: pointer;
border-radius: var(--radius-sm);
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
line-height: 1;
}
.hyp-tabs__btn--active {
background: var(--color-fg);
color: var(--color-bg);
}
.hyp-tabs__btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ----- SWITCH ----- */
.hyp-switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
font-family: var(--font-body);
font-size: var(--text-md);
}
.hyp-switch__track {
width: 50px;
height: 26px;
border: var(--border);
border-radius: var(--radius-pill);
background: transparent;
position: relative;
transition: background var(--duration-base) var(--easing);
flex-shrink: 0;
}
.hyp-switch__thumb {
position: absolute;
top: 3px;
left: 3px;
width: 18px;
height: 18px;
background: var(--color-fg);
border-radius: 50%;
transition: transform var(--duration-base) var(--easing),
background var(--duration-base) var(--easing);
}
.hyp-switch__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.hyp-switch__input:checked + .hyp-switch__track {
background: var(--color-fg);
}
.hyp-switch__input:checked + .hyp-switch__track .hyp-switch__thumb {
transform: translateX(24px);
background: var(--color-bg);
}
.hyp-switch__input:focus-visible + .hyp-switch__track {
outline: 2px solid var(--color-fg);
outline-offset: 3px;
}
/* ----- TOAST ----- */
.hyp-toast-region {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 1100;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
width: max-content;
max-width: calc(100vw - 48px);
}
.hyp-toast {
background: var(--color-fg);
color: var(--color-bg);
border: var(--border);
border-radius: var(--radius-pill);
padding: 14px 22px;
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: var(--text-sm);
pointer-events: auto;
box-shadow: var(--shadow-hard-sm);
animation: hyp-toast-in 280ms var(--easing);
display: inline-flex;
align-items: center;
gap: 10px;
}
.hyp-toast__desc {
font-family: var(--font-body);
text-transform: none;
letter-spacing: 0;
font-size: var(--text-sm);
opacity: 0.85;
}
.hyp-toast--success {
background: var(--color-success);
color: var(--color-fg);
}
.hyp-toast--error {
background: var(--color-danger);
color: #FFFFFF;
}
@keyframes hyp-toast-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ----- SECTION TITLE pattern ----- */
.hyp-section-title {
font-family: var(--font-display);
text-transform: uppercase;
font-size: var(--text-3xl);
letter-spacing: var(--display-letter-spacing);
line-height: 1;
display: flex;
align-items: center;
margin: 0 0 var(--space-6);
}
.hyp-section-title::after {
content: "";
flex-grow: 1;
height: var(--border-width);
background: var(--color-fg);
margin-left: 16px;
}
/* ----- BOTTOM NAV pattern ----- */
.hyp-bottom-nav {
position: fixed;
bottom: 20px;
left: 20px;
right: 20px;
height: 64px;
background: var(--color-fg);
border-radius: var(--radius-pill);
display: flex;
justify-content: space-around;
align-items: center;
padding: 0 10px;
box-shadow: var(--shadow-soft);
z-index: 100;
max-width: 480px;
margin: 0 auto;
}
.hyp-bottom-nav__item {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-bg);
text-decoration: none;
border-radius: 50%;
font-family: var(--font-display);
text-transform: uppercase;
font-size: var(--text-xs);
letter-spacing: 0.5px;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
}
.hyp-bottom-nav__item--active {
background: var(--color-bg);
color: var(--color-fg);
}
/* ----- DISPLAY headings ----- */
.hyp-display {
font-family: var(--font-display);
text-transform: uppercase;
line-height: var(--display-line-height);
letter-spacing: var(--display-letter-spacing);
margin: 0;
}
Step 5 — Wrap the section/page you want themed with <div data-system="hyper">:
<div data-system="hyper">
<Button>HELLO HYPER</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in Hyper.
Install the Wardrobe Hyper 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=Anton&display=swap");
/* =========================================================
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;
}
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";
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>
);
}
--- 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;
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>
);
}
--- 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="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>
);
}
--- 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="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>
);
}
--- components/ui/wardrobe/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>
);
}
--- 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(
"hyp-badge",
variant === "filled" && "hyp-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="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>
);
}
--- 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("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>
);
}
--- components/ui/wardrobe/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>
);
}
--- 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 };
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>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — HYPER globals
Loads display font + base component styles.
Only active on elements with data-system="hyper".
========================================================= */
[data-system="hyper"] *,
[data-system="hyper"] *::before,
[data-system="hyper"] *::after {
box-sizing: border-box;
}
[data-system="hyper"] {
min-height: 100vh;
}
/* ----- BUTTON ----- */
.hyp-btn {
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
border: var(--border);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: transform var(--duration-fast) var(--easing),
box-shadow var(--duration-fast) var(--easing),
background var(--duration-base) var(--easing),
color var(--duration-base) var(--easing);
text-decoration: none;
line-height: 1;
}
.hyp-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.hyp-btn--primary {
background: var(--color-fg);
color: var(--color-bg);
border-radius: var(--radius-pill);
}
.hyp-btn--secondary {
background: var(--color-surface);
color: var(--color-fg);
border-radius: var(--radius-md);
}
.hyp-btn--destructive {
background: var(--color-danger);
color: #FFFFFF;
border-radius: var(--radius-pill);
}
.hyp-btn--sm { height: 32px; padding: 0 16px; font-size: var(--text-sm); }
.hyp-btn--md { height: 48px; padding: 0 24px; font-size: var(--text-md); }
.hyp-btn--lg { height: 60px; padding: 0 32px; font-size: var(--text-xl); }
.hyp-btn:not(:disabled):hover {
transform: translate(-1px, -1px);
box-shadow: var(--shadow-hard-sm);
}
.hyp-btn:not(:disabled):active {
transform: translate(-2px, -2px);
box-shadow: var(--shadow-hard);
}
.hyp-btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 3px;
}
/* ----- INPUT / TEXTAREA ----- */
.hyp-field {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
position: relative;
}
.hyp-field__label {
font-family: var(--font-display);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1;
}
.hyp-field__hint {
font-family: var(--font-body);
font-size: var(--text-xs);
opacity: 0.7;
}
.hyp-field__hint--error { color: var(--color-danger); opacity: 1; }
.hyp-input,
.hyp-textarea,
.hyp-select {
font-family: var(--font-body);
font-size: var(--text-md);
color: var(--color-fg);
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
padding: 12px 16px;
width: 100%;
outline: none;
transition: box-shadow var(--duration-fast) var(--easing),
transform var(--duration-fast) var(--easing);
}
.hyp-input:focus,
.hyp-textarea:focus,
.hyp-select:focus {
box-shadow: var(--shadow-hard-sm);
transform: translate(-1px, -1px);
}
.hyp-input--error,
.hyp-textarea--error { border-color: var(--color-danger); }
.hyp-input::placeholder,
.hyp-textarea::placeholder { color: rgba(58, 30, 30, 0.5); }
.hyp-input--sm { height: 36px; padding: 8px 12px; font-size: var(--text-sm); }
.hyp-input--md { height: 48px; }
.hyp-input--lg { height: 56px; font-size: var(--text-lg); }
.hyp-input--display {
font-family: var(--font-display);
font-size: var(--text-5xl);
line-height: 1;
letter-spacing: -0.02em;
text-transform: uppercase;
height: auto;
padding: 16px;
}
.hyp-textarea {
min-height: 120px;
resize: vertical;
font-family: var(--font-body);
}
/* ----- SELECT ----- */
.hyp-select {
appearance: none;
-webkit-appearance: none;
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
padding-right: 40px;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%233A1E1E' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 16px center;
cursor: pointer;
}
/* ----- CARD ----- */
.hyp-card {
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
padding: var(--space-5);
position: relative;
}
.hyp-card--pink { background: var(--color-pink); }
.hyp-card--blue { background: var(--color-blue); }
.hyp-card--green { background: var(--color-green); }
.hyp-badge-circle {
position: absolute;
top: -10px;
right: 20px;
width: 24px;
height: 24px;
background: var(--color-surface);
border: var(--border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-body);
font-size: var(--text-xs);
z-index: 2;
}
/* ----- BADGE (inline pill) ----- */
.hyp-badge {
display: inline-flex;
align-items: center;
background: var(--color-surface);
color: var(--color-fg);
border: var(--border);
border-radius: var(--radius-pill);
padding: 4px 10px;
font-family: var(--font-body);
font-size: var(--text-xs);
line-height: 1.2;
letter-spacing: 0.02em;
}
.hyp-badge--filled {
background: var(--color-fg);
color: var(--color-bg);
}
/* ----- DIALOG ----- */
.hyp-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(58, 30, 30, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
animation: hyp-fade-in 200ms var(--easing);
}
.hyp-dialog {
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-hard);
max-width: 520px;
width: 100%;
padding: var(--space-8);
position: relative;
animation: hyp-pop-in 220ms var(--easing);
}
.hyp-dialog__title {
font-family: var(--font-display);
text-transform: uppercase;
font-size: var(--text-3xl);
line-height: var(--display-line-height);
letter-spacing: var(--display-letter-spacing);
margin: 0 0 var(--space-4);
}
.hyp-dialog__close {
position: absolute;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
border: var(--border);
border-radius: 50%;
background: var(--color-surface);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-body);
font-size: var(--text-md);
color: var(--color-fg);
line-height: 1;
}
.hyp-dialog__close:hover {
background: var(--color-fg);
color: var(--color-bg);
}
@keyframes hyp-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes hyp-pop-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ----- TABS ----- */
.hyp-tabs {
display: inline-flex;
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
padding: 4px;
gap: 0;
}
.hyp-tabs__btn {
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: var(--text-sm);
padding: 8px 16px;
border: none;
background: transparent;
color: var(--color-fg);
cursor: pointer;
border-radius: var(--radius-sm);
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
line-height: 1;
}
.hyp-tabs__btn--active {
background: var(--color-fg);
color: var(--color-bg);
}
.hyp-tabs__btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ----- SWITCH ----- */
.hyp-switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
font-family: var(--font-body);
font-size: var(--text-md);
}
.hyp-switch__track {
width: 50px;
height: 26px;
border: var(--border);
border-radius: var(--radius-pill);
background: transparent;
position: relative;
transition: background var(--duration-base) var(--easing);
flex-shrink: 0;
}
.hyp-switch__thumb {
position: absolute;
top: 3px;
left: 3px;
width: 18px;
height: 18px;
background: var(--color-fg);
border-radius: 50%;
transition: transform var(--duration-base) var(--easing),
background var(--duration-base) var(--easing);
}
.hyp-switch__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.hyp-switch__input:checked + .hyp-switch__track {
background: var(--color-fg);
}
.hyp-switch__input:checked + .hyp-switch__track .hyp-switch__thumb {
transform: translateX(24px);
background: var(--color-bg);
}
.hyp-switch__input:focus-visible + .hyp-switch__track {
outline: 2px solid var(--color-fg);
outline-offset: 3px;
}
/* ----- TOAST ----- */
.hyp-toast-region {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 1100;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
width: max-content;
max-width: calc(100vw - 48px);
}
.hyp-toast {
background: var(--color-fg);
color: var(--color-bg);
border: var(--border);
border-radius: var(--radius-pill);
padding: 14px 22px;
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: var(--text-sm);
pointer-events: auto;
box-shadow: var(--shadow-hard-sm);
animation: hyp-toast-in 280ms var(--easing);
display: inline-flex;
align-items: center;
gap: 10px;
}
.hyp-toast__desc {
font-family: var(--font-body);
text-transform: none;
letter-spacing: 0;
font-size: var(--text-sm);
opacity: 0.85;
}
.hyp-toast--success {
background: var(--color-success);
color: var(--color-fg);
}
.hyp-toast--error {
background: var(--color-danger);
color: #FFFFFF;
}
@keyframes hyp-toast-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ----- SECTION TITLE pattern ----- */
.hyp-section-title {
font-family: var(--font-display);
text-transform: uppercase;
font-size: var(--text-3xl);
letter-spacing: var(--display-letter-spacing);
line-height: 1;
display: flex;
align-items: center;
margin: 0 0 var(--space-6);
}
.hyp-section-title::after {
content: "";
flex-grow: 1;
height: var(--border-width);
background: var(--color-fg);
margin-left: 16px;
}
/* ----- BOTTOM NAV pattern ----- */
.hyp-bottom-nav {
position: fixed;
bottom: 20px;
left: 20px;
right: 20px;
height: 64px;
background: var(--color-fg);
border-radius: var(--radius-pill);
display: flex;
justify-content: space-around;
align-items: center;
padding: 0 10px;
box-shadow: var(--shadow-soft);
z-index: 100;
max-width: 480px;
margin: 0 auto;
}
.hyp-bottom-nav__item {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-bg);
text-decoration: none;
border-radius: 50%;
font-family: var(--font-display);
text-transform: uppercase;
font-size: var(--text-xs);
letter-spacing: 0.5px;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
}
.hyp-bottom-nav__item--active {
background: var(--color-bg);
color: var(--color-fg);
}
/* ----- DISPLAY headings ----- */
.hyp-display {
font-family: var(--font-display);
text-transform: uppercase;
line-height: var(--display-line-height);
letter-spacing: var(--display-letter-spacing);
margin: 0;
}
Step 5 — Wrap the section/page you want themed with <div data-system="hyper">:
<div data-system="hyper">
<Button>HELLO HYPER</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in Hyper.
Install the Wardrobe Hyper 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=Anton&display=swap");
/* =========================================================
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;
}
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";
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>
);
}
--- 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;
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>
);
}
--- 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="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>
);
}
--- 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="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>
);
}
--- components/ui/wardrobe/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>
);
}
--- 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(
"hyp-badge",
variant === "filled" && "hyp-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="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>
);
}
--- 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("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>
);
}
--- components/ui/wardrobe/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>
);
}
--- 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 };
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>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — HYPER globals
Loads display font + base component styles.
Only active on elements with data-system="hyper".
========================================================= */
[data-system="hyper"] *,
[data-system="hyper"] *::before,
[data-system="hyper"] *::after {
box-sizing: border-box;
}
[data-system="hyper"] {
min-height: 100vh;
}
/* ----- BUTTON ----- */
.hyp-btn {
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
border: var(--border);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: transform var(--duration-fast) var(--easing),
box-shadow var(--duration-fast) var(--easing),
background var(--duration-base) var(--easing),
color var(--duration-base) var(--easing);
text-decoration: none;
line-height: 1;
}
.hyp-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.hyp-btn--primary {
background: var(--color-fg);
color: var(--color-bg);
border-radius: var(--radius-pill);
}
.hyp-btn--secondary {
background: var(--color-surface);
color: var(--color-fg);
border-radius: var(--radius-md);
}
.hyp-btn--destructive {
background: var(--color-danger);
color: #FFFFFF;
border-radius: var(--radius-pill);
}
.hyp-btn--sm { height: 32px; padding: 0 16px; font-size: var(--text-sm); }
.hyp-btn--md { height: 48px; padding: 0 24px; font-size: var(--text-md); }
.hyp-btn--lg { height: 60px; padding: 0 32px; font-size: var(--text-xl); }
.hyp-btn:not(:disabled):hover {
transform: translate(-1px, -1px);
box-shadow: var(--shadow-hard-sm);
}
.hyp-btn:not(:disabled):active {
transform: translate(-2px, -2px);
box-shadow: var(--shadow-hard);
}
.hyp-btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 3px;
}
/* ----- INPUT / TEXTAREA ----- */
.hyp-field {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
position: relative;
}
.hyp-field__label {
font-family: var(--font-display);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1;
}
.hyp-field__hint {
font-family: var(--font-body);
font-size: var(--text-xs);
opacity: 0.7;
}
.hyp-field__hint--error { color: var(--color-danger); opacity: 1; }
.hyp-input,
.hyp-textarea,
.hyp-select {
font-family: var(--font-body);
font-size: var(--text-md);
color: var(--color-fg);
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
padding: 12px 16px;
width: 100%;
outline: none;
transition: box-shadow var(--duration-fast) var(--easing),
transform var(--duration-fast) var(--easing);
}
.hyp-input:focus,
.hyp-textarea:focus,
.hyp-select:focus {
box-shadow: var(--shadow-hard-sm);
transform: translate(-1px, -1px);
}
.hyp-input--error,
.hyp-textarea--error { border-color: var(--color-danger); }
.hyp-input::placeholder,
.hyp-textarea::placeholder { color: rgba(58, 30, 30, 0.5); }
.hyp-input--sm { height: 36px; padding: 8px 12px; font-size: var(--text-sm); }
.hyp-input--md { height: 48px; }
.hyp-input--lg { height: 56px; font-size: var(--text-lg); }
.hyp-input--display {
font-family: var(--font-display);
font-size: var(--text-5xl);
line-height: 1;
letter-spacing: -0.02em;
text-transform: uppercase;
height: auto;
padding: 16px;
}
.hyp-textarea {
min-height: 120px;
resize: vertical;
font-family: var(--font-body);
}
/* ----- SELECT ----- */
.hyp-select {
appearance: none;
-webkit-appearance: none;
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
padding-right: 40px;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%233A1E1E' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 16px center;
cursor: pointer;
}
/* ----- CARD ----- */
.hyp-card {
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
padding: var(--space-5);
position: relative;
}
.hyp-card--pink { background: var(--color-pink); }
.hyp-card--blue { background: var(--color-blue); }
.hyp-card--green { background: var(--color-green); }
.hyp-badge-circle {
position: absolute;
top: -10px;
right: 20px;
width: 24px;
height: 24px;
background: var(--color-surface);
border: var(--border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-body);
font-size: var(--text-xs);
z-index: 2;
}
/* ----- BADGE (inline pill) ----- */
.hyp-badge {
display: inline-flex;
align-items: center;
background: var(--color-surface);
color: var(--color-fg);
border: var(--border);
border-radius: var(--radius-pill);
padding: 4px 10px;
font-family: var(--font-body);
font-size: var(--text-xs);
line-height: 1.2;
letter-spacing: 0.02em;
}
.hyp-badge--filled {
background: var(--color-fg);
color: var(--color-bg);
}
/* ----- DIALOG ----- */
.hyp-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(58, 30, 30, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
animation: hyp-fade-in 200ms var(--easing);
}
.hyp-dialog {
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-hard);
max-width: 520px;
width: 100%;
padding: var(--space-8);
position: relative;
animation: hyp-pop-in 220ms var(--easing);
}
.hyp-dialog__title {
font-family: var(--font-display);
text-transform: uppercase;
font-size: var(--text-3xl);
line-height: var(--display-line-height);
letter-spacing: var(--display-letter-spacing);
margin: 0 0 var(--space-4);
}
.hyp-dialog__close {
position: absolute;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
border: var(--border);
border-radius: 50%;
background: var(--color-surface);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-body);
font-size: var(--text-md);
color: var(--color-fg);
line-height: 1;
}
.hyp-dialog__close:hover {
background: var(--color-fg);
color: var(--color-bg);
}
@keyframes hyp-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes hyp-pop-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ----- TABS ----- */
.hyp-tabs {
display: inline-flex;
background: var(--color-surface);
border: var(--border);
border-radius: var(--radius-md);
padding: 4px;
gap: 0;
}
.hyp-tabs__btn {
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: var(--text-sm);
padding: 8px 16px;
border: none;
background: transparent;
color: var(--color-fg);
cursor: pointer;
border-radius: var(--radius-sm);
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
line-height: 1;
}
.hyp-tabs__btn--active {
background: var(--color-fg);
color: var(--color-bg);
}
.hyp-tabs__btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ----- SWITCH ----- */
.hyp-switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
font-family: var(--font-body);
font-size: var(--text-md);
}
.hyp-switch__track {
width: 50px;
height: 26px;
border: var(--border);
border-radius: var(--radius-pill);
background: transparent;
position: relative;
transition: background var(--duration-base) var(--easing);
flex-shrink: 0;
}
.hyp-switch__thumb {
position: absolute;
top: 3px;
left: 3px;
width: 18px;
height: 18px;
background: var(--color-fg);
border-radius: 50%;
transition: transform var(--duration-base) var(--easing),
background var(--duration-base) var(--easing);
}
.hyp-switch__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.hyp-switch__input:checked + .hyp-switch__track {
background: var(--color-fg);
}
.hyp-switch__input:checked + .hyp-switch__track .hyp-switch__thumb {
transform: translateX(24px);
background: var(--color-bg);
}
.hyp-switch__input:focus-visible + .hyp-switch__track {
outline: 2px solid var(--color-fg);
outline-offset: 3px;
}
/* ----- TOAST ----- */
.hyp-toast-region {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 1100;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
width: max-content;
max-width: calc(100vw - 48px);
}
.hyp-toast {
background: var(--color-fg);
color: var(--color-bg);
border: var(--border);
border-radius: var(--radius-pill);
padding: 14px 22px;
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: var(--text-sm);
pointer-events: auto;
box-shadow: var(--shadow-hard-sm);
animation: hyp-toast-in 280ms var(--easing);
display: inline-flex;
align-items: center;
gap: 10px;
}
.hyp-toast__desc {
font-family: var(--font-body);
text-transform: none;
letter-spacing: 0;
font-size: var(--text-sm);
opacity: 0.85;
}
.hyp-toast--success {
background: var(--color-success);
color: var(--color-fg);
}
.hyp-toast--error {
background: var(--color-danger);
color: #FFFFFF;
}
@keyframes hyp-toast-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ----- SECTION TITLE pattern ----- */
.hyp-section-title {
font-family: var(--font-display);
text-transform: uppercase;
font-size: var(--text-3xl);
letter-spacing: var(--display-letter-spacing);
line-height: 1;
display: flex;
align-items: center;
margin: 0 0 var(--space-6);
}
.hyp-section-title::after {
content: "";
flex-grow: 1;
height: var(--border-width);
background: var(--color-fg);
margin-left: 16px;
}
/* ----- BOTTOM NAV pattern ----- */
.hyp-bottom-nav {
position: fixed;
bottom: 20px;
left: 20px;
right: 20px;
height: 64px;
background: var(--color-fg);
border-radius: var(--radius-pill);
display: flex;
justify-content: space-around;
align-items: center;
padding: 0 10px;
box-shadow: var(--shadow-soft);
z-index: 100;
max-width: 480px;
margin: 0 auto;
}
.hyp-bottom-nav__item {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-bg);
text-decoration: none;
border-radius: 50%;
font-family: var(--font-display);
text-transform: uppercase;
font-size: var(--text-xs);
letter-spacing: 0.5px;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
}
.hyp-bottom-nav__item--active {
background: var(--color-bg);
color: var(--color-fg);
}
/* ----- DISPLAY headings ----- */
.hyp-display {
font-family: var(--font-display);
text-transform: uppercase;
line-height: var(--display-line-height);
letter-spacing: var(--display-letter-spacing);
margin: 0;
}
Step 5 — Wrap the section/page you want themed with <div data-system="hyper">:
<div data-system="hyper">
<Button>HELLO HYPER</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in Hyper.
Once it's installed, wrap whichever section you want themed:
<div data-system="hyper">
<Button>HELLO HYPER</Button>
<Card variant="pink">…</Card>
</div> Want to see what it looks like in the wild? Marathon Club →
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.