Mono
A self-contained Swiss-minimal theme preset — a stark black-and-white (or paper-and-ink) panel on a strict grid: tight neo-grotesque heading, numbered hairline spec rows, generous negative space, and exactly one restrained accent. A keyboard-operable radiogroup of accent swatches re-tokens the panel live and a role=switch flips light/dark; the palette is fully exposed via accents and colors. The only motion is an accent rule that draws across the top, pinned to its full frame under reduced motion.
"use client";
import { animate, motion, useMotionValue } from "motion/react";
import * as React from "react";
import { useReducedMotion } from "@/registry/default/hooks/use-reduced-motion/use-reduced-motion";
import { cn } from "@/lib/utils";
export type MonoMode = "light" | "dark";
export interface MonoPalette {
/** Page base behind the card. */
bg: string;
/** Card surface. */
surface: string;
/** Primary foreground text. */
text: string;
/** Muted / secondary text. */
muted: string;
/** Hairline divider / border color. */
line: string;
/** The single restrained accent — index numbers, filled button, draw rule. */
accent: string;
/** Text/ink color that sits on top of the accent fill. */
accentInk: string;
}
export interface MonoAccent {
/** Stable id used for controlled selection + React keys. */
id: string;
/** Chip label shown in the switcher. */
label: string;
/** The accent hue applied across the panel when active. */
color: string;
/** Optional ink color for text on the accent fill. @default "#ffffff" */
ink?: string;
}
export interface MonoSpec {
/** Row caption, e.g. "Leading". */
label: string;
/** Row value, e.g. "1.4". */
value: string;
}
/** Base tokens per mode — accent is layered in from the active preset. */
const DARK_BASE: Omit<MonoPalette, "accent" | "accentInk"> = {
bg: "#0a0a0a",
surface: "#111111",
text: "#fafafa",
muted: "#8a8a8a",
line: "rgba(255,255,255,0.14)",
};
const LIGHT_BASE: Omit<MonoPalette, "accent" | "accentInk"> = {
bg: "#f4f4f2",
surface: "#ffffff",
text: "#0a0a0a",
muted: "#6b6b6b",
line: "rgba(0,0,0,0.14)",
};
/** Built-in accents — one restrained signal color, Swiss-poster primaries. */
const ACCENTS: MonoAccent[] = [
{ id: "red", label: "Red", color: "#e5322b" },
{ id: "blue", label: "Blue", color: "#2563eb" },
{ id: "green", label: "Green", color: "#0e9f6e" },
];
const DEFAULT_SPECS: MonoSpec[] = [
{ label: "Weight", value: "Medium" },
{ label: "Leading", value: "1.40" },
{ label: "Measure", value: "62ch" },
{ label: "Columns", value: "12" },
];
export interface MonoProps
extends Omit<React.ComponentPropsWithoutRef<"section">, "onChange"> {
/** Controlled active accent id (matches an accent's `id`). */
variant?: string;
/** Initial accent id when uncontrolled. @default "red" (first accent) */
defaultVariant?: string;
/** Fires with the next accent id whenever the switcher changes. */
onVariantChange?: (id: string) => void;
/** Override the built-in accent set (chips + colors). @default the three built-in accents */
accents?: MonoAccent[];
/** Controlled light/dark mode. */
mode?: MonoMode;
/** Initial mode when uncontrolled. @default "dark" */
defaultMode?: MonoMode;
/** Fires with the next mode whenever the light/dark switch flips. */
onModeChange?: (mode: MonoMode) => void;
/** Token overrides merged over the resolved palette — makes any palette reachable. */
colors?: Partial<MonoPalette>;
/** Draw-rule animation speed multiplier. @default 1 */
speed?: number;
/** Freeze the draw rule at its resting (full) frame. @default false */
paused?: boolean;
/** Small mono eyebrow above the heading. @default "SPECIMEN — 001" */
eyebrow?: string;
/** Tight neo-grotesque display heading. @default "Form Follows" */
heading?: string;
/** Body line under the heading. @default "A strict grid, hairline rules, and one restrained accent." */
description?: string;
/** Numbered spec rows (auto-indexed 01…N). @default four type-spec rows */
specs?: MonoSpec[];
/** Small square tag label. @default "GRID" */
tag?: string;
/** Filled (accent) button label. @default "Continue" */
primaryLabel?: string;
/** Outline button label. @default "Reset" */
secondaryLabel?: string;
/** Accessible label for the panel region. @default "Monochrome theme preset" */
label?: string;
/** Accessible label for the accent switcher group. @default "Accent color" */
switcherLabel?: string;
/** Accessible label for the light/dark switch. @default "Toggle light mode" */
modeLabel?: string;
}
/**
* Mono — a self-contained Swiss-minimal theme preset. A stark black-and-white
* (or paper-and-ink) panel built on a strict grid: a tight neo-grotesque
* heading, numbered hairline spec rows, generous negative space, and exactly
* one restrained accent. A keyboard-operable radiogroup of accent chips
* re-tokens the whole panel live, and a real `role="switch"` flips light↔dark;
* the palette is fully exposed via `accents` and `colors`. The only motion is a
* single accent rule that draws across the top on change — reduced motion (or
* `paused`) pins it to its full resting frame, so the layout is otherwise
* completely static.
*/
export function Mono({
variant,
defaultVariant,
onVariantChange,
accents,
mode,
defaultMode = "dark",
onModeChange,
colors,
speed = 1,
paused = false,
eyebrow = "SPECIMEN — 001",
heading = "Form Follows",
description = "A strict grid, hairline rules, and one restrained accent.",
specs,
tag = "GRID",
primaryLabel = "Continue",
secondaryLabel = "Reset",
label = "Monochrome theme preset",
switcherLabel = "Accent color",
modeLabel = "Toggle light mode",
className,
style,
...rest
}: MonoProps) {
const reducedMotion = useReducedMotion();
const list = accents && accents.length > 0 ? accents : ACCENTS;
const rows = specs && specs.length > 0 ? specs : DEFAULT_SPECS;
// Accent — controlled or uncontrolled.
const isVarControlled = variant !== undefined;
const [internalVar, setInternalVar] = React.useState(
defaultVariant ?? list[0].id
);
const activeId = isVarControlled ? variant : internalVar;
const activeAccent = list.find((a) => a.id === activeId) ?? list[0];
// Mode — controlled or uncontrolled.
const isModeControlled = mode !== undefined;
const [internalMode, setInternalMode] = React.useState<MonoMode>(defaultMode);
const activeMode = isModeControlled ? mode : internalMode;
const isDark = activeMode === "dark";
const base = isDark ? DARK_BASE : LIGHT_BASE;
const t: MonoPalette = {
...base,
accent: activeAccent.color,
accentInk: activeAccent.ink ?? "#ffffff",
...colors,
};
const spd = Math.max(0.1, speed);
// The single motion: a hairline accent rule that draws across the top of the
// card. It re-runs when the accent or mode changes (change = re-index feel).
// Reduced motion / paused pin it to a full-width resting frame.
const draw = useMotionValue(reducedMotion || paused ? 1 : 0);
React.useEffect(() => {
if (reducedMotion || paused) {
draw.set(1);
return;
}
draw.set(0);
const controls = animate(draw, 1, {
duration: 0.55 / spd,
ease: [0.16, 1, 0.3, 1],
});
return controls.stop;
// Re-draw on accent/mode change.
}, [draw, reducedMotion, paused, spd, activeId, activeMode]);
// Roving-focus radiogroup wiring.
const activeIdx = Math.max(
0,
list.findIndex((a) => a.id === activeId)
);
const chipRefs = React.useRef<(HTMLButtonElement | null)[]>([]);
const select = React.useCallback(
(i: number) => {
const next = list[i];
if (!next) return;
if (!isVarControlled) setInternalVar(next.id);
onVariantChange?.(next.id);
},
[list, isVarControlled, onVariantChange]
);
const onSwitcherKey = React.useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
let next = activeIdx;
switch (e.key) {
case "ArrowRight":
case "ArrowDown":
next = (activeIdx + 1) % list.length;
break;
case "ArrowLeft":
case "ArrowUp":
next = (activeIdx - 1 + list.length) % list.length;
break;
case "Home":
next = 0;
break;
case "End":
next = list.length - 1;
break;
default:
return;
}
e.preventDefault();
select(next);
chipRefs.current[next]?.focus();
},
[activeIdx, list, select]
);
const toggleMode = React.useCallback(() => {
const next: MonoMode = isDark ? "light" : "dark";
if (!isModeControlled) setInternalMode(next);
onModeChange?.(next);
}, [isDark, isModeControlled, onModeChange]);
const vars = {
"--m-bg": t.bg,
"--m-surface": t.surface,
"--m-text": t.text,
"--m-muted": t.muted,
"--m-line": t.line,
"--m-accent": t.accent,
"--m-ink": t.accentInk,
} as React.CSSProperties;
const focusRing =
"outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--m-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--m-surface)]";
return (
<section
aria-label={label}
data-crucible="mono"
data-variant={activeAccent.id}
data-mode={activeMode}
className={cn("relative isolate w-full max-w-sm", className)}
style={{ ...vars, ...style }}
{...rest}
>
<div
className="relative isolate flex w-full flex-col overflow-hidden rounded-lg border"
style={{
background: "var(--m-surface)",
borderColor: "var(--m-line)",
color: "var(--m-text)",
}}
>
{/* The one moving part: an accent rule that draws across the top. */}
<div
aria-hidden
className="absolute inset-x-0 top-0 h-[2px]"
style={{ background: "var(--m-line)" }}
>
<motion.div
className="h-full w-full origin-left"
style={{ background: "var(--m-accent)", scaleX: draw }}
/>
</div>
<div className="flex flex-col gap-6 p-6 pt-7">
{/* Header: eyebrow + light/dark switch. */}
<div className="flex items-start justify-between gap-4">
<p
className="font-mono text-[0.62rem] font-medium uppercase tracking-[0.24em]"
style={{ color: "var(--m-muted)" }}
>
{eyebrow}
</p>
<button
type="button"
role="switch"
aria-checked={isDark}
aria-label={modeLabel}
onClick={toggleMode}
className={cn(
"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border transition-colors",
focusRing
)}
style={{
borderColor: "var(--m-line)",
background: isDark ? "transparent" : "var(--m-accent)",
}}
>
<span
className="ml-0.5 h-3.5 w-3.5 rounded-full transition-transform"
style={{
background: isDark ? "var(--m-text)" : "var(--m-ink)",
transform: isDark ? "translateX(0)" : "translateX(16px)",
}}
/>
</button>
</div>
{/* Heading + description. */}
<div className="flex flex-col gap-2">
<h3 className="text-3xl font-medium leading-[1.02] tracking-[-0.02em]">
{heading}
</h3>
<p
className="max-w-[34ch] text-sm leading-relaxed"
style={{ color: "var(--m-muted)" }}
>
{description}
</p>
</div>
{/* Numbered spec grid — hairline dividers, accent index numbers. */}
<div className="grid grid-cols-2">
{rows.map((row, i) => (
<div
key={`${row.label}-${i}`}
className="flex flex-col gap-1 border-t py-3 pr-4 first:pt-0 [&:nth-child(2)]:pt-0"
style={{ borderColor: "var(--m-line)" }}
>
<span
className="font-mono text-[0.62rem] font-semibold tabular-nums"
style={{ color: "var(--m-accent)" }}
>
{String(i + 1).padStart(2, "0")}
</span>
<span className="text-sm font-medium">{row.value}</span>
<span
className="text-[0.7rem] uppercase tracking-wide"
style={{ color: "var(--m-muted)" }}
>
{row.label}
</span>
</div>
))}
</div>
{/* Tag + buttons. */}
<div className="flex items-center gap-3">
<span
className="rounded-none border px-2 py-1 font-mono text-[0.6rem] font-semibold uppercase tracking-[0.2em]"
style={{ borderColor: "var(--m-line)", color: "var(--m-muted)" }}
>
{tag}
</span>
<div className="ml-auto flex gap-2">
<button
type="button"
className={cn(
"rounded-md px-4 py-1.5 text-sm font-medium transition-transform active:scale-[0.98]",
focusRing
)}
style={{ background: "var(--m-accent)", color: "var(--m-ink)" }}
>
{primaryLabel}
</button>
<button
type="button"
className={cn(
"rounded-md border px-4 py-1.5 text-sm font-medium transition-colors active:scale-[0.98]",
focusRing
)}
style={{ borderColor: "var(--m-line)", color: "var(--m-text)" }}
>
{secondaryLabel}
</button>
</div>
</div>
{/* Hairline divider before the switcher. */}
<div
className="h-px w-full"
style={{ background: "var(--m-line)" }}
aria-hidden
/>
{/* Accent switcher — a real radiogroup with roving focus. */}
<div className="flex items-center justify-between gap-3">
<span
className="font-mono text-[0.6rem] uppercase tracking-[0.22em]"
style={{ color: "var(--m-muted)" }}
>
{switcherLabel}
</span>
<div
role="radiogroup"
aria-label={switcherLabel}
onKeyDown={onSwitcherKey}
className="flex gap-2"
>
{list.map((a, i) => {
const selected = a.id === activeAccent.id;
return (
<button
key={a.id}
ref={(el) => {
chipRefs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={selected}
aria-label={a.label}
tabIndex={selected ? 0 : -1}
onClick={() => select(i)}
className={cn(
"relative h-5 w-5 rounded-full border transition-transform active:scale-95",
focusRing
)}
style={{
background: a.color,
borderColor: selected
? "var(--m-text)"
: "var(--m-line)",
boxShadow: selected
? "inset 0 0 0 2px var(--m-surface)"
: "none",
}}
/>
);
})}
</div>
</div>
</div>
</div>
</section>
);
}
Installation
CLI
npx shadcn@latest add @crucible/monoManual — install dependencies, then copy the source
npm install motionProps
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | string | Controlled active accent id (matches an accent's id). | |
| defaultVariant | string | "red" (first accent) | Initial accent id when uncontrolled. |
| onVariantChange | (id: string) => void | Fires with the next accent id whenever the switcher changes. | |
| accents | MonoAccent[] | the three built-in accents | Override the built-in accent set (chips + colors). |
| mode | MonoMode | Controlled light/dark mode. | |
| defaultMode | MonoMode | "dark" | Initial mode when uncontrolled. |
| onModeChange | (mode: MonoMode) => void | Fires with the next mode whenever the light/dark switch flips. | |
| colors | Partial<MonoPalette> | Token overrides merged over the resolved palette — makes any palette reachable. | |
| speed | number | 1 | Draw-rule animation speed multiplier. |
| paused | boolean | false | Freeze the draw rule at its resting (full) frame. |
| eyebrow | string | "SPECIMEN — 001" | Small mono eyebrow above the heading. |
| heading | string | "Form Follows" | Tight neo-grotesque display heading. |
| description | string | "A strict grid, hairline rules, and one restrained accent." | Body line under the heading. |
| specs | MonoSpec[] | four type-spec rows | Numbered spec rows (auto-indexed 01…N). |
| tag | string | "GRID" | Small square tag label. |
| primaryLabel | string | "Continue" | Filled (accent) button label. |
| secondaryLabel | string | "Reset" | Outline button label. |
| label | string | "Monochrome theme preset" | Accessible label for the panel region. |
| switcherLabel | string | "Accent color" | Accessible label for the accent switcher group. |
| modeLabel | string | "Toggle light mode" | Accessible label for the light/dark switch. |
Also accepts all props of Omit<React.ComponentPropsWithoutRef<"section">, "onChange"> — they pass through to the underlying element. | |||
Honors prefers-reduced-motion with a designed static fallback, and passes className through.