404 / Not Found
Animated 404 pages in five styles: glitch scramble, magnetic digits, cursor spotlight, a fanning card stack and a typed terminal.
Glitch
glitch.tsxDigits scramble through random glyphs before resolving, with a chromatic split on hover.
404
Page not found
The page you are looking for moved, vanished, or never existed.
"use client";
import { NotFoundGlitch } from "@/components/motion/not-found/glitch";
export function NotFoundGlitchPreview() {
return (
<div className="w-full">
<NotFoundGlitch />
</div>
);
}
"use client";
// beui.dev/components/blocks/not-found
import { useEffect, useState } from "react";
import { useReducedMotion } from "motion/react";
import {
NOT_FOUND_DEFAULTS,
NotFoundActions,
NotFoundStage,
type NotFoundProps,
} from "./shared";
const GLYPHS = "ABCDEFGHJKLMNPQRSTUVWXYZ0123456789#%&@$?/\\";
const SCRAMBLE_MS = 700;
const TICK_MS = 45;
/**
* Renders the code, scrambling each character on mount before it settles.
* SSR and the first paint show the real code, so the scramble is a pure
* client-side enhancement and reduced-motion users see the code immediately.
*/
function Scramble({ text }: { text: string }) {
const reduce = useReducedMotion();
const [display, setDisplay] = useState(text);
useEffect(() => {
if (reduce) {
setDisplay(text);
return;
}
const chars = text.split("");
const start = performance.now();
let raf = 0;
let last = 0;
const loop = (now: number) => {
if (now - last >= TICK_MS) {
last = now;
const progress = Math.min((now - start) / SCRAMBLE_MS, 1);
const settled = Math.floor(progress * chars.length);
setDisplay(
chars
.map((ch, i) =>
i < settled || ch === " "
? ch
: GLYPHS[Math.floor(Math.random() * GLYPHS.length)],
)
.join(""),
);
}
if (now - start < SCRAMBLE_MS) {
raf = requestAnimationFrame(loop);
} else {
setDisplay(text);
}
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [text, reduce]);
return <span className="tabular-nums">{display}</span>;
}
export function NotFoundGlitch({
className,
code = NOT_FOUND_DEFAULTS.code,
title = NOT_FOUND_DEFAULTS.title,
description = NOT_FOUND_DEFAULTS.description,
homeHref,
homeLabel,
browseHref,
browseLabel,
}: NotFoundProps) {
return (
<NotFoundStage className={className}>
<div className="group relative select-none font-mono font-bold leading-none tracking-tighter text-foreground [font-size:clamp(5rem,18vw,11rem)]">
{/* Chromatic ghost layers, nudged apart on hover. */}
<span
aria-hidden
className="pointer-events-none absolute inset-0 text-[#ff0040] opacity-0 mix-blend-screen transition-[transform,opacity] duration-150 ease-out group-hover:translate-x-[3px] group-hover:opacity-70 motion-reduce:hidden"
>
<Scramble text={code} />
</span>
<span
aria-hidden
className="pointer-events-none absolute inset-0 text-[#00e5ff] opacity-0 mix-blend-screen transition-[transform,opacity] duration-150 ease-out group-hover:-translate-x-[3px] group-hover:opacity-70 motion-reduce:hidden"
>
<Scramble text={code} />
</span>
<h1 className="relative">
<Scramble text={code} />
</h1>
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-semibold text-foreground">{title}</p>
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
</div>
<NotFoundActions
homeHref={homeHref}
homeLabel={homeLabel}
browseHref={browseHref}
browseLabel={browseLabel}
/>
</NotFoundStage>
);
}
Install
shadcn init? You are set. Theme setupInstall dependencies
npm i clsx motion tailwind-mergeAdd util files
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.
export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.6,
} as const;
/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
stiffness: 460,
damping: 30,
mass: 0.55,
} as const;
/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
stiffness: 420,
damping: 40,
mass: 0.5,
} as const;
/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
type: "spring",
stiffness: 360,
damping: 32,
mass: 0.6,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
stiffness: 200,
damping: 15,
mass: 0.3,
} as const;
/** Dragged handles and fills (sliders) — critically damped `useSpring` config,
* so the value follows the pointer butterily and never rebounds off an end. */
export const SPRING_GLIDE = {
stiffness: 700,
damping: 50,
mass: 0.5,
} as const;
"use client";
import { useEffect, useState } from "react";
/**
* Returns true only on devices that have a true hover (mouse / trackpad).
* Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere
* — gate hover-only effects (scale lifts, magnetic pulls) behind this.
*/
export function useHoverCapable() {
const [canHover, setCanHover] = useState(false);
useEffect(() => {
if (typeof window === "undefined" || !window.matchMedia) return;
const mq = window.matchMedia("(hover: hover) and (pointer: fine)");
const update = () => setCanHover(mq.matches);
update();
mq.addEventListener?.("change", update);
return () => mq.removeEventListener?.("change", update);
}, []);
return canHover;
}
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy the source code
"use client";
// beui.dev/components/blocks/not-found
import { useEffect, useState } from "react";
import { useReducedMotion } from "motion/react";
import {
NOT_FOUND_DEFAULTS,
NotFoundActions,
NotFoundStage,
type NotFoundProps,
} from "./shared";
const GLYPHS = "ABCDEFGHJKLMNPQRSTUVWXYZ0123456789#%&@$?/\\";
const SCRAMBLE_MS = 700;
const TICK_MS = 45;
/**
* Renders the code, scrambling each character on mount before it settles.
* SSR and the first paint show the real code, so the scramble is a pure
* client-side enhancement and reduced-motion users see the code immediately.
*/
function Scramble({ text }: { text: string }) {
const reduce = useReducedMotion();
const [display, setDisplay] = useState(text);
useEffect(() => {
if (reduce) {
setDisplay(text);
return;
}
const chars = text.split("");
const start = performance.now();
let raf = 0;
let last = 0;
const loop = (now: number) => {
if (now - last >= TICK_MS) {
last = now;
const progress = Math.min((now - start) / SCRAMBLE_MS, 1);
const settled = Math.floor(progress * chars.length);
setDisplay(
chars
.map((ch, i) =>
i < settled || ch === " "
? ch
: GLYPHS[Math.floor(Math.random() * GLYPHS.length)],
)
.join(""),
);
}
if (now - start < SCRAMBLE_MS) {
raf = requestAnimationFrame(loop);
} else {
setDisplay(text);
}
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [text, reduce]);
return <span className="tabular-nums">{display}</span>;
}
export function NotFoundGlitch({
className,
code = NOT_FOUND_DEFAULTS.code,
title = NOT_FOUND_DEFAULTS.title,
description = NOT_FOUND_DEFAULTS.description,
homeHref,
homeLabel,
browseHref,
browseLabel,
}: NotFoundProps) {
return (
<NotFoundStage className={className}>
<div className="group relative select-none font-mono font-bold leading-none tracking-tighter text-foreground [font-size:clamp(5rem,18vw,11rem)]">
{/* Chromatic ghost layers, nudged apart on hover. */}
<span
aria-hidden
className="pointer-events-none absolute inset-0 text-[#ff0040] opacity-0 mix-blend-screen transition-[transform,opacity] duration-150 ease-out group-hover:translate-x-[3px] group-hover:opacity-70 motion-reduce:hidden"
>
<Scramble text={code} />
</span>
<span
aria-hidden
className="pointer-events-none absolute inset-0 text-[#00e5ff] opacity-0 mix-blend-screen transition-[transform,opacity] duration-150 ease-out group-hover:-translate-x-[3px] group-hover:opacity-70 motion-reduce:hidden"
>
<Scramble text={code} />
</span>
<h1 className="relative">
<Scramble text={code} />
</h1>
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-semibold text-foreground">{title}</p>
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
</div>
<NotFoundActions
homeHref={homeHref}
homeLabel={homeLabel}
browseHref={browseHref}
browseLabel={browseLabel}
/>
</NotFoundStage>
);
}
"use client";
import { motion, useReducedMotion } from "motion/react";
import { SPRING_PRESS } from "@/lib/ease";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import { cn } from "@/lib/utils";
export interface NotFoundProps {
className?: string;
/** The big status code. */
code?: string;
title?: string;
description?: string;
homeHref?: string;
homeLabel?: string;
browseHref?: string;
browseLabel?: string;
}
export const NOT_FOUND_DEFAULTS = {
code: "404",
title: "Page not found",
description:
"The page you are looking for moved, vanished, or never existed.",
homeHref: "/",
homeLabel: "Back home",
browseHref: "/components/motion",
browseLabel: "Browse components",
} as const;
type ActionsProps = Pick<
NotFoundProps,
"homeHref" | "homeLabel" | "browseHref" | "browseLabel" | "className"
>;
/** The shared dual CTA: a primary "Back home" and a secondary "Browse". */
export function NotFoundActions({
homeHref = NOT_FOUND_DEFAULTS.homeHref,
homeLabel = NOT_FOUND_DEFAULTS.homeLabel,
browseHref = NOT_FOUND_DEFAULTS.browseHref,
browseLabel = NOT_FOUND_DEFAULTS.browseLabel,
className,
}: ActionsProps) {
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const whileTap = reduce ? undefined : { scale: 0.96 };
const whileHover = reduce || !canHover ? undefined : { scale: 1.02 };
return (
<div
className={cn(
"flex flex-wrap items-center justify-center gap-3",
className,
)}
>
<motion.a
href={homeHref}
whileTap={whileTap}
whileHover={whileHover}
transition={SPRING_PRESS}
className="inline-flex h-11 select-none items-center justify-center rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
{homeLabel}
</motion.a>
<motion.a
href={browseHref}
whileTap={whileTap}
whileHover={whileHover}
transition={SPRING_PRESS}
className="inline-flex h-11 select-none items-center justify-center rounded-full border border-border bg-card px-6 text-sm font-medium text-foreground transition-colors hover:bg-primary/5"
>
{browseLabel}
</motion.a>
</div>
);
}
/** Centers a variant and gives it a consistent minimum stage height. */
export function NotFoundStage({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
return (
<div
className={cn(
"flex min-h-[420px] w-full flex-col items-center justify-center gap-8 px-4 text-center",
className,
)}
>
{children}
</div>
);
}
API Reference
className?string—code?stringThe big status code.
404title?stringPage not founddescription?stringThe page you are looking for moved, vanished, or never existed.homeHref?string—homeLabel?string—browseHref?string—browseLabel?string—Magnetic
magnetic.tsxEach digit is cursor-attracted via the Magnetic wrapper and springs back on leave.
Page not found
The page you are looking for moved, vanished, or never existed.
"use client";
import { NotFoundMagnetic } from "@/components/motion/not-found/magnetic";
export function NotFoundMagneticPreview() {
return (
<div className="w-full">
<NotFoundMagnetic />
</div>
);
}
"use client";
// beui.dev/components/blocks/not-found
import { Magnetic } from "@/components/motion/magnetic";
import { cn } from "@/lib/utils";
import {
NOT_FOUND_DEFAULTS,
NotFoundActions,
NotFoundStage,
type NotFoundProps,
} from "./shared";
export function NotFoundMagnetic({
className,
code = NOT_FOUND_DEFAULTS.code,
title = NOT_FOUND_DEFAULTS.title,
description = NOT_FOUND_DEFAULTS.description,
homeHref,
homeLabel,
browseHref,
browseLabel,
}: NotFoundProps) {
const chars = code.split("");
return (
<NotFoundStage className={className}>
<h1
aria-label={code}
className="flex select-none items-center justify-center font-bold leading-none tracking-tighter text-foreground [font-size:clamp(5rem,18vw,12rem)]"
>
{chars.map((ch, i) => (
<Magnetic
// biome-ignore lint/suspicious/noArrayIndexKey: fixed positional glyphs
key={i}
strength={0.6}
className={cn(i > 0 && "-ml-2")}
>
<span aria-hidden className="inline-block px-1 tabular-nums">
{ch}
</span>
</Magnetic>
))}
</h1>
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-semibold text-foreground">{title}</p>
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
</div>
<NotFoundActions
homeHref={homeHref}
homeLabel={homeLabel}
browseHref={browseHref}
browseLabel={browseLabel}
/>
</NotFoundStage>
);
}
Install
shadcn init? You are set. Theme setupInstall dependencies
npm i clsx motion tailwind-mergeAdd util files
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.
export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.6,
} as const;
/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
stiffness: 460,
damping: 30,
mass: 0.55,
} as const;
/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
stiffness: 420,
damping: 40,
mass: 0.5,
} as const;
/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
type: "spring",
stiffness: 360,
damping: 32,
mass: 0.6,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
stiffness: 200,
damping: 15,
mass: 0.3,
} as const;
/** Dragged handles and fills (sliders) — critically damped `useSpring` config,
* so the value follows the pointer butterily and never rebounds off an end. */
export const SPRING_GLIDE = {
stiffness: 700,
damping: 50,
mass: 0.5,
} as const;
"use client";
import { useEffect, useState } from "react";
/**
* Returns true only on devices that have a true hover (mouse / trackpad).
* Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere
* — gate hover-only effects (scale lifts, magnetic pulls) behind this.
*/
export function useHoverCapable() {
const [canHover, setCanHover] = useState(false);
useEffect(() => {
if (typeof window === "undefined" || !window.matchMedia) return;
const mq = window.matchMedia("(hover: hover) and (pointer: fine)");
const update = () => setCanHover(mq.matches);
update();
mq.addEventListener?.("change", update);
return () => mq.removeEventListener?.("change", update);
}, []);
return canHover;
}
Copy the source code
"use client";
// beui.dev/components/blocks/not-found
import { Magnetic } from "@/components/motion/magnetic";
import { cn } from "@/lib/utils";
import {
NOT_FOUND_DEFAULTS,
NotFoundActions,
NotFoundStage,
type NotFoundProps,
} from "./shared";
export function NotFoundMagnetic({
className,
code = NOT_FOUND_DEFAULTS.code,
title = NOT_FOUND_DEFAULTS.title,
description = NOT_FOUND_DEFAULTS.description,
homeHref,
homeLabel,
browseHref,
browseLabel,
}: NotFoundProps) {
const chars = code.split("");
return (
<NotFoundStage className={className}>
<h1
aria-label={code}
className="flex select-none items-center justify-center font-bold leading-none tracking-tighter text-foreground [font-size:clamp(5rem,18vw,12rem)]"
>
{chars.map((ch, i) => (
<Magnetic
// biome-ignore lint/suspicious/noArrayIndexKey: fixed positional glyphs
key={i}
strength={0.6}
className={cn(i > 0 && "-ml-2")}
>
<span aria-hidden className="inline-block px-1 tabular-nums">
{ch}
</span>
</Magnetic>
))}
</h1>
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-semibold text-foreground">{title}</p>
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
</div>
<NotFoundActions
homeHref={homeHref}
homeLabel={homeLabel}
browseHref={browseHref}
browseLabel={browseLabel}
/>
</NotFoundStage>
);
}
"use client";
import { motion, useReducedMotion } from "motion/react";
import { SPRING_PRESS } from "@/lib/ease";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import { cn } from "@/lib/utils";
export interface NotFoundProps {
className?: string;
/** The big status code. */
code?: string;
title?: string;
description?: string;
homeHref?: string;
homeLabel?: string;
browseHref?: string;
browseLabel?: string;
}
export const NOT_FOUND_DEFAULTS = {
code: "404",
title: "Page not found",
description:
"The page you are looking for moved, vanished, or never existed.",
homeHref: "/",
homeLabel: "Back home",
browseHref: "/components/motion",
browseLabel: "Browse components",
} as const;
type ActionsProps = Pick<
NotFoundProps,
"homeHref" | "homeLabel" | "browseHref" | "browseLabel" | "className"
>;
/** The shared dual CTA: a primary "Back home" and a secondary "Browse". */
export function NotFoundActions({
homeHref = NOT_FOUND_DEFAULTS.homeHref,
homeLabel = NOT_FOUND_DEFAULTS.homeLabel,
browseHref = NOT_FOUND_DEFAULTS.browseHref,
browseLabel = NOT_FOUND_DEFAULTS.browseLabel,
className,
}: ActionsProps) {
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const whileTap = reduce ? undefined : { scale: 0.96 };
const whileHover = reduce || !canHover ? undefined : { scale: 1.02 };
return (
<div
className={cn(
"flex flex-wrap items-center justify-center gap-3",
className,
)}
>
<motion.a
href={homeHref}
whileTap={whileTap}
whileHover={whileHover}
transition={SPRING_PRESS}
className="inline-flex h-11 select-none items-center justify-center rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
{homeLabel}
</motion.a>
<motion.a
href={browseHref}
whileTap={whileTap}
whileHover={whileHover}
transition={SPRING_PRESS}
className="inline-flex h-11 select-none items-center justify-center rounded-full border border-border bg-card px-6 text-sm font-medium text-foreground transition-colors hover:bg-primary/5"
>
{browseLabel}
</motion.a>
</div>
);
}
/** Centers a variant and gives it a consistent minimum stage height. */
export function NotFoundStage({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
return (
<div
className={cn(
"flex min-h-[420px] w-full flex-col items-center justify-center gap-8 px-4 text-center",
className,
)}
>
{children}
</div>
);
}
"use client";
import { motion, useMotionValue, useReducedMotion, useSpring } from "motion/react";
import { useRef, type ReactNode } from "react";
import { SPRING_MOUSE } from "@/lib/ease";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import { cn } from "@/lib/utils";
export interface MagneticProps {
children: ReactNode;
strength?: number;
className?: string;
}
export function Magnetic({ children, strength = 0.35, className }: MagneticProps) {
const ref = useRef<HTMLDivElement>(null);
const reduce = useReducedMotion();
const canHover = useHoverCapable();
// Decorative cursor-follow: skip on touch (phantom hover) and reduced motion.
const enabled = !reduce && canHover;
const x = useMotionValue(0);
const y = useMotionValue(0);
const sx = useSpring(x, SPRING_MOUSE);
const sy = useSpring(y, SPRING_MOUSE);
const onMove = (e: React.MouseEvent<HTMLDivElement>) => {
const el = ref.current;
if (!el || !enabled) return;
const rect = el.getBoundingClientRect();
x.set((e.clientX - rect.left - rect.width / 2) * strength);
y.set((e.clientY - rect.top - rect.height / 2) * strength);
};
const onLeave = () => {
x.set(0);
y.set(0);
};
return (
<motion.div
ref={ref}
onMouseMove={onMove}
onMouseLeave={onLeave}
style={{ x: sx, y: sy }}
className={cn("inline-block", className)}
>
{children}
</motion.div>
);
}
API Reference
className?string—code?stringThe big status code.
404title?stringPage not founddescription?stringThe page you are looking for moved, vanished, or never existed.homeHref?string—homeLabel?string—browseHref?string—browseLabel?string—Spotlight
spotlight.tsxA dark panel where a cursor-tracked spotlight reveals the bright code from a dim base.
Page not found
The page you are looking for moved, vanished, or never existed.
"use client";
import { NotFoundSpotlight } from "@/components/motion/not-found/spotlight";
export function NotFoundSpotlightPreview() {
return (
<div className="w-full">
<NotFoundSpotlight />
</div>
);
}
"use client";
// beui.dev/components/blocks/not-found
import { useRef } from "react";
import {
motion,
useMotionTemplate,
useMotionValue,
useReducedMotion,
} from "motion/react";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import { cn } from "@/lib/utils";
import {
NOT_FOUND_DEFAULTS,
NotFoundActions,
NotFoundStage,
type NotFoundProps,
} from "./shared";
export function NotFoundSpotlight({
className,
code = NOT_FOUND_DEFAULTS.code,
title = NOT_FOUND_DEFAULTS.title,
description = NOT_FOUND_DEFAULTS.description,
homeHref,
homeLabel,
browseHref,
browseLabel,
}: NotFoundProps) {
const ref = useRef<HTMLDivElement>(null);
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const enabled = !reduce && canHover;
const mx = useMotionValue(50);
const my = useMotionValue(50);
const mask = useMotionTemplate`radial-gradient(220px circle at ${mx}% ${my}%, #000 25%, transparent 72%)`;
const onMove = (e: React.MouseEvent<HTMLDivElement>) => {
const el = ref.current;
if (!el || !enabled) return;
const rect = el.getBoundingClientRect();
mx.set(((e.clientX - rect.left) / rect.width) * 100);
my.set(((e.clientY - rect.top) / rect.height) * 100);
};
return (
<NotFoundStage className={className}>
<motion.div
ref={ref}
onMouseMove={onMove}
className="relative isolate flex aspect-[16/9] w-full max-w-xl items-center justify-center overflow-hidden rounded-3xl border border-border bg-neutral-950"
>
{/* Dim base layer. */}
<span
aria-hidden
className="select-none font-bold leading-none tracking-tighter text-white/10 [font-size:clamp(5rem,16vw,10rem)]"
>
{code}
</span>
{/* Bright layer, revealed only under the spotlight. */}
<motion.h1
aria-label={code}
style={enabled ? { WebkitMaskImage: mask, maskImage: mask } : undefined}
className={cn(
"absolute select-none font-bold leading-none tracking-tighter text-white [font-size:clamp(5rem,16vw,10rem)]",
!enabled && "text-white/90",
)}
>
<span aria-hidden>{code}</span>
</motion.h1>
</motion.div>
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-semibold text-foreground">{title}</p>
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
</div>
<NotFoundActions
homeHref={homeHref}
homeLabel={homeLabel}
browseHref={browseHref}
browseLabel={browseLabel}
/>
</NotFoundStage>
);
}
Install
shadcn init? You are set. Theme setupInstall dependencies
npm i clsx motion tailwind-mergeAdd util files
"use client";
import { useEffect, useState } from "react";
/**
* Returns true only on devices that have a true hover (mouse / trackpad).
* Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere
* — gate hover-only effects (scale lifts, magnetic pulls) behind this.
*/
export function useHoverCapable() {
const [canHover, setCanHover] = useState(false);
useEffect(() => {
if (typeof window === "undefined" || !window.matchMedia) return;
const mq = window.matchMedia("(hover: hover) and (pointer: fine)");
const update = () => setCanHover(mq.matches);
update();
mq.addEventListener?.("change", update);
return () => mq.removeEventListener?.("change", update);
}, []);
return canHover;
}
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.
export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.6,
} as const;
/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
stiffness: 460,
damping: 30,
mass: 0.55,
} as const;
/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
stiffness: 420,
damping: 40,
mass: 0.5,
} as const;
/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
type: "spring",
stiffness: 360,
damping: 32,
mass: 0.6,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
stiffness: 200,
damping: 15,
mass: 0.3,
} as const;
/** Dragged handles and fills (sliders) — critically damped `useSpring` config,
* so the value follows the pointer butterily and never rebounds off an end. */
export const SPRING_GLIDE = {
stiffness: 700,
damping: 50,
mass: 0.5,
} as const;
Copy the source code
"use client";
// beui.dev/components/blocks/not-found
import { useRef } from "react";
import {
motion,
useMotionTemplate,
useMotionValue,
useReducedMotion,
} from "motion/react";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import { cn } from "@/lib/utils";
import {
NOT_FOUND_DEFAULTS,
NotFoundActions,
NotFoundStage,
type NotFoundProps,
} from "./shared";
export function NotFoundSpotlight({
className,
code = NOT_FOUND_DEFAULTS.code,
title = NOT_FOUND_DEFAULTS.title,
description = NOT_FOUND_DEFAULTS.description,
homeHref,
homeLabel,
browseHref,
browseLabel,
}: NotFoundProps) {
const ref = useRef<HTMLDivElement>(null);
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const enabled = !reduce && canHover;
const mx = useMotionValue(50);
const my = useMotionValue(50);
const mask = useMotionTemplate`radial-gradient(220px circle at ${mx}% ${my}%, #000 25%, transparent 72%)`;
const onMove = (e: React.MouseEvent<HTMLDivElement>) => {
const el = ref.current;
if (!el || !enabled) return;
const rect = el.getBoundingClientRect();
mx.set(((e.clientX - rect.left) / rect.width) * 100);
my.set(((e.clientY - rect.top) / rect.height) * 100);
};
return (
<NotFoundStage className={className}>
<motion.div
ref={ref}
onMouseMove={onMove}
className="relative isolate flex aspect-[16/9] w-full max-w-xl items-center justify-center overflow-hidden rounded-3xl border border-border bg-neutral-950"
>
{/* Dim base layer. */}
<span
aria-hidden
className="select-none font-bold leading-none tracking-tighter text-white/10 [font-size:clamp(5rem,16vw,10rem)]"
>
{code}
</span>
{/* Bright layer, revealed only under the spotlight. */}
<motion.h1
aria-label={code}
style={enabled ? { WebkitMaskImage: mask, maskImage: mask } : undefined}
className={cn(
"absolute select-none font-bold leading-none tracking-tighter text-white [font-size:clamp(5rem,16vw,10rem)]",
!enabled && "text-white/90",
)}
>
<span aria-hidden>{code}</span>
</motion.h1>
</motion.div>
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-semibold text-foreground">{title}</p>
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
</div>
<NotFoundActions
homeHref={homeHref}
homeLabel={homeLabel}
browseHref={browseHref}
browseLabel={browseLabel}
/>
</NotFoundStage>
);
}
"use client";
import { motion, useReducedMotion } from "motion/react";
import { SPRING_PRESS } from "@/lib/ease";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import { cn } from "@/lib/utils";
export interface NotFoundProps {
className?: string;
/** The big status code. */
code?: string;
title?: string;
description?: string;
homeHref?: string;
homeLabel?: string;
browseHref?: string;
browseLabel?: string;
}
export const NOT_FOUND_DEFAULTS = {
code: "404",
title: "Page not found",
description:
"The page you are looking for moved, vanished, or never existed.",
homeHref: "/",
homeLabel: "Back home",
browseHref: "/components/motion",
browseLabel: "Browse components",
} as const;
type ActionsProps = Pick<
NotFoundProps,
"homeHref" | "homeLabel" | "browseHref" | "browseLabel" | "className"
>;
/** The shared dual CTA: a primary "Back home" and a secondary "Browse". */
export function NotFoundActions({
homeHref = NOT_FOUND_DEFAULTS.homeHref,
homeLabel = NOT_FOUND_DEFAULTS.homeLabel,
browseHref = NOT_FOUND_DEFAULTS.browseHref,
browseLabel = NOT_FOUND_DEFAULTS.browseLabel,
className,
}: ActionsProps) {
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const whileTap = reduce ? undefined : { scale: 0.96 };
const whileHover = reduce || !canHover ? undefined : { scale: 1.02 };
return (
<div
className={cn(
"flex flex-wrap items-center justify-center gap-3",
className,
)}
>
<motion.a
href={homeHref}
whileTap={whileTap}
whileHover={whileHover}
transition={SPRING_PRESS}
className="inline-flex h-11 select-none items-center justify-center rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
{homeLabel}
</motion.a>
<motion.a
href={browseHref}
whileTap={whileTap}
whileHover={whileHover}
transition={SPRING_PRESS}
className="inline-flex h-11 select-none items-center justify-center rounded-full border border-border bg-card px-6 text-sm font-medium text-foreground transition-colors hover:bg-primary/5"
>
{browseLabel}
</motion.a>
</div>
);
}
/** Centers a variant and gives it a consistent minimum stage height. */
export function NotFoundStage({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
return (
<div
className={cn(
"flex min-h-[420px] w-full flex-col items-center justify-center gap-8 px-4 text-center",
className,
)}
>
{children}
</div>
);
}
API Reference
className?string—code?stringThe big status code.
404title?stringPage not founddescription?stringThe page you are looking for moved, vanished, or never existed.homeHref?string—homeLabel?string—browseHref?string—browseLabel?string—Stacked
stacked.tsxA code card over a hidden stack that fans out with a spring on hover.
404
out of the deckPage not found
The page you are looking for moved, vanished, or never existed.
"use client";
import { NotFoundStacked } from "@/components/motion/not-found/stacked";
export function NotFoundStackedPreview() {
return (
<div className="w-full">
<NotFoundStacked />
</div>
);
}
"use client";
// beui.dev/components/blocks/not-found
import { motion, useReducedMotion } from "motion/react";
import { SPRING_PANEL } from "@/lib/ease";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import {
NOT_FOUND_DEFAULTS,
NotFoundActions,
NotFoundStage,
type NotFoundProps,
} from "./shared";
const CARD =
"absolute inset-0 rounded-3xl border border-border bg-card shadow-sm";
export function NotFoundStacked({
className,
code = NOT_FOUND_DEFAULTS.code,
title = NOT_FOUND_DEFAULTS.title,
description = NOT_FOUND_DEFAULTS.description,
homeHref,
homeLabel,
browseHref,
browseLabel,
}: NotFoundProps) {
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const interactive = !reduce && canHover;
return (
<NotFoundStage className={className}>
<motion.div
initial="rest"
animate="rest"
whileHover={interactive ? "hover" : undefined}
className="relative h-44 w-64"
>
<motion.div
aria-hidden
variants={{ rest: { rotate: 0, x: 0, y: 0 }, hover: { rotate: -9, x: -28, y: 8 } }}
transition={SPRING_PANEL}
className={CARD}
/>
<motion.div
aria-hidden
variants={{ rest: { rotate: 0, x: 0, y: 0 }, hover: { rotate: 9, x: 28, y: 8 } }}
transition={SPRING_PANEL}
className={CARD}
/>
<motion.div
variants={{ rest: { y: 0 }, hover: { y: -6 } }}
transition={SPRING_PANEL}
className="absolute inset-0 flex flex-col items-center justify-center gap-1 rounded-3xl border border-border bg-card shadow-md"
>
<h1 className="select-none font-bold leading-none tracking-tighter text-foreground [font-size:clamp(3.5rem,9vw,5rem)]">
{code}
</h1>
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
out of the deck
</span>
</motion.div>
</motion.div>
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-semibold text-foreground">{title}</p>
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
</div>
<NotFoundActions
homeHref={homeHref}
homeLabel={homeLabel}
browseHref={browseHref}
browseLabel={browseLabel}
/>
</NotFoundStage>
);
}
Install
shadcn init? You are set. Theme setupInstall dependencies
npm i clsx motion tailwind-mergeAdd util files
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.
export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.6,
} as const;
/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
stiffness: 460,
damping: 30,
mass: 0.55,
} as const;
/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
stiffness: 420,
damping: 40,
mass: 0.5,
} as const;
/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
type: "spring",
stiffness: 360,
damping: 32,
mass: 0.6,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
stiffness: 200,
damping: 15,
mass: 0.3,
} as const;
/** Dragged handles and fills (sliders) — critically damped `useSpring` config,
* so the value follows the pointer butterily and never rebounds off an end. */
export const SPRING_GLIDE = {
stiffness: 700,
damping: 50,
mass: 0.5,
} as const;
"use client";
import { useEffect, useState } from "react";
/**
* Returns true only on devices that have a true hover (mouse / trackpad).
* Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere
* — gate hover-only effects (scale lifts, magnetic pulls) behind this.
*/
export function useHoverCapable() {
const [canHover, setCanHover] = useState(false);
useEffect(() => {
if (typeof window === "undefined" || !window.matchMedia) return;
const mq = window.matchMedia("(hover: hover) and (pointer: fine)");
const update = () => setCanHover(mq.matches);
update();
mq.addEventListener?.("change", update);
return () => mq.removeEventListener?.("change", update);
}, []);
return canHover;
}
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy the source code
"use client";
// beui.dev/components/blocks/not-found
import { motion, useReducedMotion } from "motion/react";
import { SPRING_PANEL } from "@/lib/ease";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import {
NOT_FOUND_DEFAULTS,
NotFoundActions,
NotFoundStage,
type NotFoundProps,
} from "./shared";
const CARD =
"absolute inset-0 rounded-3xl border border-border bg-card shadow-sm";
export function NotFoundStacked({
className,
code = NOT_FOUND_DEFAULTS.code,
title = NOT_FOUND_DEFAULTS.title,
description = NOT_FOUND_DEFAULTS.description,
homeHref,
homeLabel,
browseHref,
browseLabel,
}: NotFoundProps) {
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const interactive = !reduce && canHover;
return (
<NotFoundStage className={className}>
<motion.div
initial="rest"
animate="rest"
whileHover={interactive ? "hover" : undefined}
className="relative h-44 w-64"
>
<motion.div
aria-hidden
variants={{ rest: { rotate: 0, x: 0, y: 0 }, hover: { rotate: -9, x: -28, y: 8 } }}
transition={SPRING_PANEL}
className={CARD}
/>
<motion.div
aria-hidden
variants={{ rest: { rotate: 0, x: 0, y: 0 }, hover: { rotate: 9, x: 28, y: 8 } }}
transition={SPRING_PANEL}
className={CARD}
/>
<motion.div
variants={{ rest: { y: 0 }, hover: { y: -6 } }}
transition={SPRING_PANEL}
className="absolute inset-0 flex flex-col items-center justify-center gap-1 rounded-3xl border border-border bg-card shadow-md"
>
<h1 className="select-none font-bold leading-none tracking-tighter text-foreground [font-size:clamp(3.5rem,9vw,5rem)]">
{code}
</h1>
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
out of the deck
</span>
</motion.div>
</motion.div>
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-semibold text-foreground">{title}</p>
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
</div>
<NotFoundActions
homeHref={homeHref}
homeLabel={homeLabel}
browseHref={browseHref}
browseLabel={browseLabel}
/>
</NotFoundStage>
);
}
"use client";
import { motion, useReducedMotion } from "motion/react";
import { SPRING_PRESS } from "@/lib/ease";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import { cn } from "@/lib/utils";
export interface NotFoundProps {
className?: string;
/** The big status code. */
code?: string;
title?: string;
description?: string;
homeHref?: string;
homeLabel?: string;
browseHref?: string;
browseLabel?: string;
}
export const NOT_FOUND_DEFAULTS = {
code: "404",
title: "Page not found",
description:
"The page you are looking for moved, vanished, or never existed.",
homeHref: "/",
homeLabel: "Back home",
browseHref: "/components/motion",
browseLabel: "Browse components",
} as const;
type ActionsProps = Pick<
NotFoundProps,
"homeHref" | "homeLabel" | "browseHref" | "browseLabel" | "className"
>;
/** The shared dual CTA: a primary "Back home" and a secondary "Browse". */
export function NotFoundActions({
homeHref = NOT_FOUND_DEFAULTS.homeHref,
homeLabel = NOT_FOUND_DEFAULTS.homeLabel,
browseHref = NOT_FOUND_DEFAULTS.browseHref,
browseLabel = NOT_FOUND_DEFAULTS.browseLabel,
className,
}: ActionsProps) {
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const whileTap = reduce ? undefined : { scale: 0.96 };
const whileHover = reduce || !canHover ? undefined : { scale: 1.02 };
return (
<div
className={cn(
"flex flex-wrap items-center justify-center gap-3",
className,
)}
>
<motion.a
href={homeHref}
whileTap={whileTap}
whileHover={whileHover}
transition={SPRING_PRESS}
className="inline-flex h-11 select-none items-center justify-center rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
{homeLabel}
</motion.a>
<motion.a
href={browseHref}
whileTap={whileTap}
whileHover={whileHover}
transition={SPRING_PRESS}
className="inline-flex h-11 select-none items-center justify-center rounded-full border border-border bg-card px-6 text-sm font-medium text-foreground transition-colors hover:bg-primary/5"
>
{browseLabel}
</motion.a>
</div>
);
}
/** Centers a variant and gives it a consistent minimum stage height. */
export function NotFoundStage({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
return (
<div
className={cn(
"flex min-h-[420px] w-full flex-col items-center justify-center gap-8 px-4 text-center",
className,
)}
>
{children}
</div>
);
}
API Reference
className?string—code?stringThe big status code.
404title?stringPage not founddescription?stringThe page you are looking for moved, vanished, or never existed.homeHref?string—homeLabel?string—browseHref?string—browseLabel?string—Terminal
terminal.tsxA terminal window that types a failed cd command and a 404 status, with a blinking caret.
$ cd /page
cd: no such file or directory: /page
$ status 404
Page not found
The page you are looking for moved, vanished, or never existed.
"use client";
import { NotFoundTerminal } from "@/components/motion/not-found/terminal";
export function NotFoundTerminalPreview() {
return (
<div className="w-full">
<NotFoundTerminal />
</div>
);
}
"use client";
// beui.dev/components/blocks/not-found
import { TextReveal } from "@/components/motion/text-reveal";
import {
NOT_FOUND_DEFAULTS,
NotFoundActions,
NotFoundStage,
type NotFoundProps,
} from "./shared";
const TYPE_SPRING = { stiffness: 320, damping: 30, mass: 0.6 };
export function NotFoundTerminal({
className,
code = NOT_FOUND_DEFAULTS.code,
title = NOT_FOUND_DEFAULTS.title,
description = NOT_FOUND_DEFAULTS.description,
homeHref,
homeLabel,
browseHref,
browseLabel,
}: NotFoundProps) {
return (
<NotFoundStage className={className}>
<div className="w-full max-w-md overflow-hidden rounded-xl border border-border bg-neutral-950 text-left shadow-lg">
<div className="flex items-center gap-1.5 border-b border-white/10 px-4 py-3">
<span className="h-3 w-3 rounded-full bg-[#ff5f57]" />
<span className="h-3 w-3 rounded-full bg-[#febc2e]" />
<span className="h-3 w-3 rounded-full bg-[#28c840]" />
<span className="ml-2 text-xs text-white/40">~/beui</span>
</div>
<div className="space-y-1.5 p-4 font-mono text-sm leading-relaxed">
<TextReveal
as="p"
split="char"
stagger={0.018}
blur={6}
yOffset={0}
spring={TYPE_SPRING}
className="text-white/80"
text="$ cd /page"
/>
<TextReveal
as="p"
split="char"
stagger={0.012}
delay={0.45}
blur={6}
yOffset={0}
spring={TYPE_SPRING}
className="text-[#ff5f57]"
text="cd: no such file or directory: /page"
/>
<p className="flex items-center text-white/80">
<TextReveal
as="span"
split="char"
stagger={0.018}
delay={1.1}
blur={6}
yOffset={0}
spring={TYPE_SPRING}
text={`$ status ${code}`}
/>
<span className="ml-1 inline-block h-[1.1em] w-[0.55ch] translate-y-[0.12em] bg-white/80 motion-safe:animate-pulse" />
</p>
</div>
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-semibold text-foreground">{title}</p>
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
</div>
<NotFoundActions
homeHref={homeHref}
homeLabel={homeLabel}
browseHref={browseHref}
browseLabel={browseLabel}
/>
</NotFoundStage>
);
}
Install
shadcn init? You are set. Theme setupInstall dependencies
npm i clsx motion tailwind-mergeAdd util files
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.
export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.6,
} as const;
/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
stiffness: 460,
damping: 30,
mass: 0.55,
} as const;
/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
stiffness: 420,
damping: 40,
mass: 0.5,
} as const;
/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
type: "spring",
stiffness: 360,
damping: 32,
mass: 0.6,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
stiffness: 200,
damping: 15,
mass: 0.3,
} as const;
/** Dragged handles and fills (sliders) — critically damped `useSpring` config,
* so the value follows the pointer butterily and never rebounds off an end. */
export const SPRING_GLIDE = {
stiffness: 700,
damping: 50,
mass: 0.5,
} as const;
"use client";
import { useEffect, useState } from "react";
/**
* Returns true only on devices that have a true hover (mouse / trackpad).
* Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere
* — gate hover-only effects (scale lifts, magnetic pulls) behind this.
*/
export function useHoverCapable() {
const [canHover, setCanHover] = useState(false);
useEffect(() => {
if (typeof window === "undefined" || !window.matchMedia) return;
const mq = window.matchMedia("(hover: hover) and (pointer: fine)");
const update = () => setCanHover(mq.matches);
update();
mq.addEventListener?.("change", update);
return () => mq.removeEventListener?.("change", update);
}, []);
return canHover;
}
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy the source code
"use client";
// beui.dev/components/blocks/not-found
import { TextReveal } from "@/components/motion/text-reveal";
import {
NOT_FOUND_DEFAULTS,
NotFoundActions,
NotFoundStage,
type NotFoundProps,
} from "./shared";
const TYPE_SPRING = { stiffness: 320, damping: 30, mass: 0.6 };
export function NotFoundTerminal({
className,
code = NOT_FOUND_DEFAULTS.code,
title = NOT_FOUND_DEFAULTS.title,
description = NOT_FOUND_DEFAULTS.description,
homeHref,
homeLabel,
browseHref,
browseLabel,
}: NotFoundProps) {
return (
<NotFoundStage className={className}>
<div className="w-full max-w-md overflow-hidden rounded-xl border border-border bg-neutral-950 text-left shadow-lg">
<div className="flex items-center gap-1.5 border-b border-white/10 px-4 py-3">
<span className="h-3 w-3 rounded-full bg-[#ff5f57]" />
<span className="h-3 w-3 rounded-full bg-[#febc2e]" />
<span className="h-3 w-3 rounded-full bg-[#28c840]" />
<span className="ml-2 text-xs text-white/40">~/beui</span>
</div>
<div className="space-y-1.5 p-4 font-mono text-sm leading-relaxed">
<TextReveal
as="p"
split="char"
stagger={0.018}
blur={6}
yOffset={0}
spring={TYPE_SPRING}
className="text-white/80"
text="$ cd /page"
/>
<TextReveal
as="p"
split="char"
stagger={0.012}
delay={0.45}
blur={6}
yOffset={0}
spring={TYPE_SPRING}
className="text-[#ff5f57]"
text="cd: no such file or directory: /page"
/>
<p className="flex items-center text-white/80">
<TextReveal
as="span"
split="char"
stagger={0.018}
delay={1.1}
blur={6}
yOffset={0}
spring={TYPE_SPRING}
text={`$ status ${code}`}
/>
<span className="ml-1 inline-block h-[1.1em] w-[0.55ch] translate-y-[0.12em] bg-white/80 motion-safe:animate-pulse" />
</p>
</div>
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-semibold text-foreground">{title}</p>
<p className="max-w-sm text-sm text-muted-foreground">{description}</p>
</div>
<NotFoundActions
homeHref={homeHref}
homeLabel={homeLabel}
browseHref={browseHref}
browseLabel={browseLabel}
/>
</NotFoundStage>
);
}
"use client";
import { motion, useReducedMotion } from "motion/react";
import { SPRING_PRESS } from "@/lib/ease";
import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
import { cn } from "@/lib/utils";
export interface NotFoundProps {
className?: string;
/** The big status code. */
code?: string;
title?: string;
description?: string;
homeHref?: string;
homeLabel?: string;
browseHref?: string;
browseLabel?: string;
}
export const NOT_FOUND_DEFAULTS = {
code: "404",
title: "Page not found",
description:
"The page you are looking for moved, vanished, or never existed.",
homeHref: "/",
homeLabel: "Back home",
browseHref: "/components/motion",
browseLabel: "Browse components",
} as const;
type ActionsProps = Pick<
NotFoundProps,
"homeHref" | "homeLabel" | "browseHref" | "browseLabel" | "className"
>;
/** The shared dual CTA: a primary "Back home" and a secondary "Browse". */
export function NotFoundActions({
homeHref = NOT_FOUND_DEFAULTS.homeHref,
homeLabel = NOT_FOUND_DEFAULTS.homeLabel,
browseHref = NOT_FOUND_DEFAULTS.browseHref,
browseLabel = NOT_FOUND_DEFAULTS.browseLabel,
className,
}: ActionsProps) {
const reduce = useReducedMotion();
const canHover = useHoverCapable();
const whileTap = reduce ? undefined : { scale: 0.96 };
const whileHover = reduce || !canHover ? undefined : { scale: 1.02 };
return (
<div
className={cn(
"flex flex-wrap items-center justify-center gap-3",
className,
)}
>
<motion.a
href={homeHref}
whileTap={whileTap}
whileHover={whileHover}
transition={SPRING_PRESS}
className="inline-flex h-11 select-none items-center justify-center rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
{homeLabel}
</motion.a>
<motion.a
href={browseHref}
whileTap={whileTap}
whileHover={whileHover}
transition={SPRING_PRESS}
className="inline-flex h-11 select-none items-center justify-center rounded-full border border-border bg-card px-6 text-sm font-medium text-foreground transition-colors hover:bg-primary/5"
>
{browseLabel}
</motion.a>
</div>
);
}
/** Centers a variant and gives it a consistent minimum stage height. */
export function NotFoundStage({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
return (
<div
className={cn(
"flex min-h-[420px] w-full flex-col items-center justify-center gap-8 px-4 text-center",
className,
)}
>
{children}
</div>
);
}
"use client";
import { motion, type Transition, useInView, useReducedMotion } from "motion/react";
import { useRef, type ElementType, type ReactNode } from "react";
import { EASE_OUT } from "@/lib/ease";
import { cn } from "@/lib/utils";
type SplitMode = "word" | "char";
export interface TextRevealProps {
text: string | string[];
as?: ElementType;
className?: string;
split?: SplitMode;
stagger?: number;
delay?: number;
blur?: number;
yOffset?: string | number;
spring?: { stiffness?: number; damping?: number; mass?: number };
once?: boolean;
whileInView?: boolean;
children?: ReactNode;
}
const DEFAULT_SPRING = { stiffness: 140, damping: 26, mass: 1.2 };
export function TextReveal({
text,
as: Comp = "span",
className,
split = "word",
stagger = 0.09,
delay = 0,
blur = 12,
yOffset = "40%",
spring,
once = true,
whileInView = false,
children,
}: TextRevealProps) {
const ref = useRef<HTMLElement>(null);
const inView = useInView(ref, { once, amount: 0.4 });
const reduce = useReducedMotion();
const shouldAnimate = whileInView ? inView : true;
const lines = Array.isArray(text) ? text : [text];
const s = { ...DEFAULT_SPRING, ...spring };
let unitIndex = 0;
const lineCounts = new Map<string, number>();
return (
<Comp ref={ref} className={cn("block", className)}>
{lines.map((line) => {
const units = split === "word" ? line.split(" ") : Array.from(line);
const lineCount = lineCounts.get(line) ?? 0;
lineCounts.set(line, lineCount + 1);
const lineKey = `${line}-${lineCount}`;
const unitCounts = new Map<string, number>();
return (
<span key={lineKey} className="block">
{units.map((unit, i) => {
const d = delay + unitIndex * stagger;
unitIndex += 1;
const unitCount = unitCounts.get(unit) ?? 0;
unitCounts.set(unit, unitCount + 1);
const unitKey = `${unit}-${unitCount}`;
const initial = reduce
? { opacity: 0 }
: { y: yOffset, opacity: 0, filter: `blur(${blur}px)` };
const animate = shouldAnimate
? reduce
? { opacity: 1 }
: { y: 0, opacity: 1, filter: "blur(0px)" }
: initial;
const transition: Transition = reduce
? { opacity: { duration: 0.25, ease: EASE_OUT, delay: d * 0.3 } }
: {
y: { type: "spring" as const, ...s, delay: d },
opacity: { duration: 0.7, ease: EASE_OUT, delay: d },
filter: { duration: 0.9, ease: EASE_OUT, delay: d },
};
return (
<motion.span
key={unitKey}
initial={initial}
animate={animate}
transition={transition}
className="inline-block will-change-transform"
>
{unit}
{split === "word" && i < units.length - 1 ? (
<span className="inline-block"> </span>
) : null}
</motion.span>
);
})}
</span>
);
})}
{children}
</Comp>
);
}
API Reference
className?string—code?stringThe big status code.
404title?stringPage not founddescription?stringThe page you are looking for moved, vanished, or never existed.homeHref?string—homeLabel?string—browseHref?string—browseLabel?string—Keep in mind
Some components on this site are inspired by or recreated from existing work across the web. I'm not here to take credit; just to learn, experiment, and sometimes push things a bit further. If something looks familiar and I forgot to mention you, reach out and I'll fix that right away.
Updated