# Brutal — Theme for React

> A self-contained neo-brutalist theme preset — a flat white card with hard 3px black borders, a solid blur-free offset drop-shadow, chunky mono type, a loud accent block, a stat, a rotated corner sticker, and two buttons. Interactive blocks do the classic brutalist nudge toward their shadow on hover and press. A keyboard-operable radiogroup of accent swatches re-tokens the whole panel live; the palette is fully exposed via presets and colors. Pure CSS — reduced motion pins every block to its raised resting pose.

- Page: https://crucibleui.com/components/themes/brutal
- Category: [themes](https://crucibleui.com/components/themes)
- Tier: free (full source)
- Engine: css

## Install

```bash
npx shadcn@latest add @crucible/brutal
```

Registry JSON: https://crucibleui.com/r/brutal.json

Import from `@/components/crucible/brutal`.

## Props (`BrutalProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant?` | `string` | — | Controlled active preset id (matches a preset's `id`). |
| `defaultVariant?` | `string` | `"acid" (first preset)` | Initial preset id when uncontrolled. |
| `onVariantChange?` | `(id: string) => void` | — | Fires with the next preset id whenever the switcher changes. |
| `presets?` | `BrutalPreset[]` | `the three built-in accents` | Override the built-in preset set (chips + palettes). |
| `colors?` | `Partial<BrutalPalette>` | — | Token overrides merged over the active preset — makes any palette reachable. |
| `offset?` | `number` | `6` | Drop-shadow offset in px — the depth of the block. |
| `border?` | `number` | `3` | Border / shadow thickness in px. |
| `paused?` | `boolean` | `false` | Freeze the tactile press-nudge (also auto-frozen under reduced motion). |
| `kicker?` | `string` | `"NO. 03 / SERIES"` | Small mono kicker above the heading. |
| `heading?` | `string` | `"PRESS HARD"` | Chunky display heading. |
| `description?` | `string` | `"Hard edges, honest shadows. Pick your loud."` | Body line under the heading. |
| `statValue?` | `string` | `"24"` | Big stat figure. |
| `statLabel?` | `string` | `"SHIPS / DAY"` | Stat unit / label. |
| `sticker?` | `string` | `"NEW"` | Small corner sticker text. |
| `primaryLabel?` | `string` | `"Get it"` | Filled (accent) button label. |
| `secondaryLabel?` | `string` | `"Later"` | Outline (white) button label. |
| `label?` | `string` | `"Neo-brutalist theme preset"` | Accessible label for the panel region. |
| `switcherLabel?` | `string` | `"Accent"` | Accessible label for the switcher group. |

Also accepts: `Omit<React.ComponentPropsWithoutRef<"section">, "onChange">`.

## Usage example

```tsx
"use client";

import * as React from "react";

import { Brutal } from "@/components/crucible/brutal";

export default function BrutalDemo() {
  const [variant, setVariant] = React.useState("acid");

  return (
    <div className="relative isolate flex h-full min-h-[360px] w-full items-center justify-center overflow-hidden rounded-xl bg-neutral-950 p-6">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(45deg,rgba(255,255,255,0.03)_0_10px,transparent_10px_20px)]"
      />
      <div className="relative z-10 flex flex-col items-center gap-5">
        <Brutal variant={variant} onVariantChange={setVariant} />
        <p className="text-center text-sm text-neutral-500">
          Switch the look — click a swatch, or focus it and use the arrow keys.
          Hover a button to feel the press.
        </p>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.

## Source (`brutal.tsx`)

```tsx
"use client";

import * as React from "react";

import { useReducedMotion } from "@/hooks/use-reduced-motion";
import { cn } from "@/lib/utils";

export interface BrutalPalette {
  /** Card surface — the flat block everything sits on. */
  surface: string;
  /** Panel backdrop behind the card (the mat). */
  base: string;
  /** Foreground text (near-black by default). */
  text: string;
  /** Muted / secondary text. */
  muted: string;
  /** Hard border + drop-shadow color — pure black in classic brutalism. */
  ink: string;
  /** The loud accent — filled blocks, primary button, active chip. */
  accent: string;
  /** Text/ink drawn on top of the accent (kept high-contrast). */
  accentText: string;
}

export interface BrutalPreset {
  /** Stable id used for controlled selection and React keys. */
  id: string;
  /** Chip label shown in the switcher. */
  label: string;
  /** Token set applied when this preset is active. */
  palette: BrutalPalette;
}

/** Near-black ink shared by every built-in preset. */
const INK = "#111111";

/** Loud accents on a white block with black ink — swap the whole vibe with one chip. */
const PRESETS: BrutalPreset[] = [
  {
    id: "acid",
    label: "Acid",
    palette: {
      surface: "#ffffff",
      base: "#f4f1e6",
      text: INK,
      muted: "#5b5b52",
      ink: INK,
      accent: "#ffde17",
      accentText: INK,
    },
  },
  {
    id: "bubble",
    label: "Bubble",
    palette: {
      surface: "#ffffff",
      base: "#ffe9f3",
      text: INK,
      muted: "#6b5560",
      ink: INK,
      accent: "#ff4d97",
      accentText: "#ffffff",
    },
  },
  {
    id: "volt",
    label: "Volt",
    palette: {
      surface: "#ffffff",
      base: "#e2f6fb",
      text: INK,
      muted: "#4d5f63",
      ink: INK,
      accent: "#22d3ee",
      accentText: INK,
    },
  },
];

export interface BrutalProps
  extends Omit<React.ComponentPropsWithoutRef<"section">, "onChange"> {
  /** Controlled active preset id (matches a preset's `id`). */
  variant?: string;
  /** Initial preset id when uncontrolled. @default "acid" (first preset) */
  defaultVariant?: string;
  /** Fires with the next preset id whenever the switcher changes. */
  onVariantChange?: (id: string) => void;
  /** Override the built-in preset set (chips + palettes). @default the three built-in accents */
  presets?: BrutalPreset[];
  /** Token overrides merged over the active preset — makes any palette reachable. */
  colors?: Partial<BrutalPalette>;
  /** Drop-shadow offset in px — the depth of the block. @default 6 */
  offset?: number;
  /** Border / shadow thickness in px. @default 3 */
  border?: number;
  /** Freeze the tactile press-nudge (also auto-frozen under reduced motion). @default false */
  paused?: boolean;
  /** Small mono kicker above the heading. @default "NO. 03 / SERIES" */
  kicker?: string;
  /** Chunky display heading. @default "PRESS HARD" */
  heading?: string;
  /** Body line under the heading. @default "Hard edges, honest shadows. Pick your loud." */
  description?: string;
  /** Big stat figure. @default "24" */
  statValue?: string;
  /** Stat unit / label. @default "SHIPS / DAY" */
  statLabel?: string;
  /** Small corner sticker text. @default "NEW" */
  sticker?: string;
  /** Filled (accent) button label. @default "Get it" */
  primaryLabel?: string;
  /** Outline (white) button label. @default "Later" */
  secondaryLabel?: string;
  /** Accessible label for the panel region. @default "Neo-brutalist theme preset" */
  label?: string;
  /** Accessible label for the switcher group. @default "Accent" */
  switcherLabel?: string;
}

/**
 * Brutal — a self-contained neo-brutalist theme preset. A flat white card with
 * hard 3px black borders and a solid, blur-free offset drop-shadow, chunky mono
 * type, a loud accent block, a stat, a rotated corner sticker, and two buttons.
 * Interactive elements do the classic brutalist nudge on hover/press — they
 * shift toward their shadow until they sit flush. A keyboard-operable radiogroup
 * of accent chips re-tokens the whole panel live; the palette is fully exposed
 * via `presets` and `colors`. The nudge is pure CSS transition, so reduced
 * motion (or `paused`) simply pins every block to its raised resting pose.
 */
export function Brutal({
  variant,
  defaultVariant,
  onVariantChange,
  presets,
  colors,
  offset = 6,
  border = 3,
  paused = false,
  kicker = "NO. 03 / SERIES",
  heading = "PRESS HARD",
  description = "Hard edges, honest shadows. Pick your loud.",
  statValue = "24",
  statLabel = "SHIPS / DAY",
  sticker = "NEW",
  primaryLabel = "Get it",
  secondaryLabel = "Later",
  label = "Neo-brutalist theme preset",
  switcherLabel = "Accent",
  className,
  style,
  ...rest
}: BrutalProps) {
  const reducedMotion = useReducedMotion();

  const list = presets && presets.length > 0 ? presets : PRESETS;
  const isControlled = variant !== undefined;
  const [internal, setInternal] = React.useState(
    defaultVariant ?? list[0].id
  );
  const activeId = isControlled ? variant : internal;
  const active = list.find((p) => p.id === activeId) ?? list[0];
  const t: BrutalPalette = { ...active.palette, ...colors };

  // The nudge is a CSS transition on transform + box-shadow. When motion is off
  // we drop the transition and the hover/active nudge utilities so every block
  // holds its raised resting pose — never a flat, shadowless frame.
  const nudge = !reducedMotion && !paused;

  const off = Math.max(0, offset);
  const bw = Math.max(1, border);

  const activeIdx = Math.max(
    0,
    list.findIndex((p) => p.id === activeId)
  );
  const chipRefs = React.useRef<(HTMLButtonElement | null)[]>([]);

  const select = React.useCallback(
    (i: number) => {
      const next = list[i];
      if (!next) return;
      if (!isControlled) setInternal(next.id);
      onVariantChange?.(next.id);
    },
    [list, isControlled, 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 vars = {
    "--br-surface": t.surface,
    "--br-base": t.base,
    "--br-text": t.text,
    "--br-muted": t.muted,
    "--br-ink": t.ink,
    "--br-accent": t.accent,
    "--br-accent-text": t.accentText,
    "--br-off": `${off}px`,
    "--br-off-hover": `${Math.round(off / 2)}px`,
    "--br-bw": `${bw}px`,
    background: "var(--br-base)",
  } as React.CSSProperties;

  const focusRing =
    "outline-none focus-visible:ring-[3px] focus-visible:ring-[color:var(--br-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--br-surface)]";

  // Raised block: full offset shadow at rest, nudges toward the shadow on
  // hover, sits flush on press. Gated so reduced motion holds the raised pose.
  const raised = cn(
    "border-[length:var(--br-bw)] border-[color:var(--br-ink)]",
    "shadow-[var(--br-off)_var(--br-off)_0_0_var(--br-ink)]",
    nudge &&
      cn(
        "transition-[transform,box-shadow] duration-100 ease-out",
        "hover:translate-x-[var(--br-off-hover)] hover:translate-y-[var(--br-off-hover)]",
        "hover:shadow-[var(--br-off-hover)_var(--br-off-hover)_0_0_var(--br-ink)]",
        "active:translate-x-[var(--br-off)] active:translate-y-[var(--br-off)]",
        "active:shadow-[0_0_0_0_var(--br-ink)]"
      )
  );

  return (
    <section
      aria-label={label}
      data-crucible="brutal"
      data-variant={active.id}
      className={cn(
        "relative isolate w-full max-w-sm p-[calc(var(--br-off)+2px)]",
        className
      )}
      style={{ ...vars, ...style }}
      {...rest}
    >
      {/* The card — a flat block with hard border + solid offset shadow. */}
      <div
        className={cn(
          "relative flex w-full flex-col gap-4 p-5",
          "border-[length:var(--br-bw)] border-[color:var(--br-ink)]",
          "shadow-[var(--br-off)_var(--br-off)_0_0_var(--br-ink)]"
        )}
        style={{ background: "var(--br-surface)", color: "var(--br-text)" }}
      >
        {/* Rotated corner sticker — decorative flair. */}
        {sticker ? (
          <span
            aria-hidden
            className="absolute -right-3 -top-3 -rotate-6 select-none px-2 py-0.5 font-mono text-[0.62rem] font-black uppercase tracking-widest border-[length:var(--br-bw)] border-[color:var(--br-ink)] shadow-[3px_3px_0_0_var(--br-ink)]"
            style={{ background: "var(--br-accent)", color: "var(--br-accent-text)" }}
          >
            {sticker}
          </span>
        ) : null}

        {/* Kicker + heading + body */}
        <div className="flex flex-col gap-1.5">
          <p
            className="font-mono text-[0.62rem] font-bold uppercase tracking-[0.28em]"
            style={{ color: "var(--br-muted)" }}
          >
            {kicker}
          </p>
          <h3
            className="text-4xl font-black uppercase leading-[0.95] tracking-tight"
            style={{ color: "var(--br-text)" }}
          >
            {heading}
          </h3>
          <p className="text-sm font-medium" style={{ color: "var(--br-muted)" }}>
            {description}
          </p>
        </div>

        {/* Accent stat block + tag row */}
        <div className="flex items-stretch gap-3">
          <div
            className="flex items-center gap-2 px-3 py-2 border-[length:var(--br-bw)] border-[color:var(--br-ink)]"
            style={{ background: "var(--br-accent)", color: "var(--br-accent-text)" }}
          >
            <span className="text-2xl font-black tabular-nums leading-none">
              {statValue}
            </span>
            <span className="font-mono text-[0.58rem] font-bold uppercase leading-tight tracking-wider">
              {statLabel}
            </span>
          </div>
        </div>

        {/* Buttons */}
        <div className="flex gap-3">
          <button
            type="button"
            className={cn(
              "px-4 py-2 text-sm font-black uppercase tracking-wide",
              raised,
              focusRing
            )}
            style={{ background: "var(--br-accent)", color: "var(--br-accent-text)" }}
          >
            {primaryLabel}
          </button>
          <button
            type="button"
            className={cn(
              "px-4 py-2 text-sm font-black uppercase tracking-wide",
              raised,
              focusRing
            )}
            style={{ background: "var(--br-surface)", color: "var(--br-text)" }}
          >
            {secondaryLabel}
          </button>
        </div>

        {/* Accent switcher — a real radiogroup with roving focus. Selected chip
            sits flush (pressed in); the rest stay raised on their shadow. */}
        <div
          role="radiogroup"
          aria-label={switcherLabel}
          onKeyDown={onSwitcherKey}
          className="mt-1 flex flex-wrap items-center gap-1 border-t-[length:var(--br-bw)] border-dashed border-[color:color-mix(in_srgb,var(--br-ink)_25%,transparent)] pt-4"
        >
          <span
            aria-hidden
            className="mr-1 font-mono text-[0.6rem] font-bold uppercase tracking-widest"
            style={{ color: "var(--br-muted)" }}
          >
            {switcherLabel}
          </span>
          {list.map((p, i) => {
            const selected = p.id === active.id;
            return (
              <button
                key={p.id}
                ref={(el) => {
                  chipRefs.current[i] = el;
                }}
                type="button"
                role="radio"
                aria-checked={selected}
                aria-label={p.label}
                tabIndex={selected ? 0 : -1}
                onClick={() => select(i)}
                title={p.label}
                className={cn(
                  "h-7 w-7 border-[length:var(--br-bw)] border-[color:var(--br-ink)]",
                  nudge && "transition-[transform,box-shadow] duration-100 ease-out",
                  selected
                    ? "translate-x-[3px] translate-y-[3px] shadow-[0_0_0_0_var(--br-ink)]"
                    : "shadow-[3px_3px_0_0_var(--br-ink)]",
                  focusRing
                )}
                style={{ background: p.palette.accent }}
              >
                {/* High-contrast selected marker that survives any accent. */}
                <span
                  aria-hidden
                  className={cn(
                    "mx-auto block h-2 w-2 rounded-full border border-[color:var(--br-ink)]",
                    selected ? "opacity-100" : "opacity-0"
                  )}
                  style={{ background: "var(--br-ink)" }}
                />
              </button>
            );
          })}
        </div>
      </div>
    </section>
  );
}
```
