07404 card with a wandering needle
A compass rocks left and right with a shrinking angle instead of settling on a bearing, and the last cell of the path line below it blinks as a dashed blank.
Best forFor a wrong or deleted URL, where a picture of being lost works better than a giant error code
rotateborder: dashedease-out

<main class="stage">
<section class="nf is-demo" aria-labelledby="nf-t">
<span class="nf__icon" aria-hidden="true"><svg viewBox="0 0 256 256" width="1em" height="1em"><g fill="currentColor"><path d="M128 32a96 96 0 1 0 96 96a96 96 0 0 0-96-96m16 112l-64 32l32-64l64-32Z" opacity=".2"/><path d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m0 192a88 88 0 1 1 88-88a88.1 88.1 0 0 1-88 88m44.42-143.16l-64 32a8.05 8.05 0 0 0-3.58 3.58l-32 64A8 8 0 0 0 80 184a8.1 8.1 0 0 0 3.58-.84l64-32a8.05 8.05 0 0 0 3.58-3.58l32-64a8 8 0 0 0-10.74-10.74M138 138l-40.11 20.11L118 118l40.15-20.07Z"/></g></svg></span>
<h2 class="nf__title" id="nf-t">There is nothing here</h2>
<ol class="nf__path" id="nf-path" aria-live="polite">
<li class="nf__seg">Home</li>
<li class="nf__seg">Files</li>
<li class="nf__seg is-gone">?</li>
</ol>
<button class="nf__btn" type="button" id="nf-b" data-home="This is home">Up one level</button>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-out;
$color: $subject-cream;
@include stage($stage-orange);
.nf {
box-sizing: border-box;
width: min(248px, 86vw);
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-align: center;
color: $subject-ink; // AA 4.5: cream on orange 3.11 -> ink 5.50
}
.nf__icon {
display: block;
font-size: clamp(40px, 13vw, 54px);
line-height: 0;
color: $subject-ink;
}
.nf__title {
margin: 0;
font-size: clamp(12px, 4vw, 14px);
line-height: 1.3;
font-weight: 800;
}
.nf__path {
margin: 0;
padding: 0;
list-style: none;
display: flex;
align-items: center;
gap: $sp-1;
}
.nf__seg {
height: 20px;
padding: 0 $sp-2;
display: flex;
align-items: center;
border-radius: $r-sm;
background: rgba(255, 247, 230, .18);
color: $subject-ink;
font-size: 10px;
font-weight: 700;
}
.nf__seg.is-gone {
border: 1px dashed rgba(23, 20, 26, .55);
background: none;
color: $subject-ink;
}
.nf__btn {
height: 28px;
margin: 0;
padding: 0 $sp-4;
border: 1px solid rgba(255, 247, 230, .42);
border-radius: $r-pill;
background: none;
color: $subject-ink;
font-family: inherit;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: background $dur-quick $ease-out;
}
.nf__btn:hover { background: rgba(255, 247, 230, .16); }
.nf__btn:disabled { opacity: .5; cursor: default; }
.nf__btn:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
// ── 자동 루프: 바늘이 한 바퀴를 못 돌고 좌우로 헤맨다. 0% 와 100% 가 같은 각도 ──
.nf.is-demo .nf__icon { animation: nf-wander $duration $easing infinite; }
@keyframes nf-wander {
0%, 100% { transform: rotate(-26deg); }
28% { transform: rotate(22deg); }
54% { transform: rotate(-14deg); }
78% { transform: rotate(10deg); }
}
.nf.is-demo .nf__seg.is-gone { animation: nf-blink $duration $easing infinite; }
@keyframes nf-blink {
0%, 100% { opacity: .42; }
50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.nf.is-demo .nf__icon,
.nf.is-demo .nf__seg.is-gone { animation: none !important; }
.nf__btn { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.nf {
box-sizing: border-box;
width: min(248px, 86vw);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-align: center;
color: #17141a;
}
.nf__icon {
display: block;
font-size: clamp(40px, 13vw, 54px);
line-height: 0;
color: #17141a;
}
.nf__title {
margin: 0;
font-size: clamp(12px, 4vw, 14px);
line-height: 1.3;
font-weight: 800;
}
.nf__path {
margin: 0;
padding: 0;
list-style: none;
display: flex;
align-items: center;
gap: 4px;
}
.nf__seg {
height: 20px;
padding: 0 8px;
display: flex;
align-items: center;
border-radius: 8px;
background: rgba(255, 247, 230, 0.18);
color: #17141a;
font-size: 10px;
font-weight: 700;
}
.nf__seg.is-gone {
border: 1px dashed rgba(23, 20, 26, 0.55);
background: none;
color: #17141a;
}
.nf__btn {
height: 28px;
margin: 0;
padding: 0 16px;
border: 1px solid rgba(255, 247, 230, 0.42);
border-radius: 999px;
background: none;
color: #17141a;
font-family: inherit;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: background 220ms ease-out;
}
.nf__btn:hover {
background: rgba(255, 247, 230, 0.16);
}
.nf__btn:disabled {
opacity: 0.5;
cursor: default;
}
.nf__btn:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.nf.is-demo .nf__icon {
animation: nf-wander 2s ease-out infinite;
}
@keyframes nf-wander {
0%, 100% {
transform: rotate(-26deg);
}
28% {
transform: rotate(22deg);
}
54% {
transform: rotate(-14deg);
}
78% {
transform: rotate(10deg);
}
}
.nf.is-demo .nf__seg.is-gone {
animation: nf-blink 2s ease-out infinite;
}
@keyframes nf-blink {
0%, 100% {
opacity: 0.42;
}
50% {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.nf.is-demo .nf__icon,
.nf.is-demo .nf__seg.is-gone {
animation: none !important;
}
.nf__btn {
transition: none;
}
}// 실제 동작: 한 단계 위로를 누를 때마다 주소 조각이 실제로 하나씩 지워지고, 홈만 남으면 버튼이 잠긴다.
var card = document.querySelector('.nf');
var path = document.getElementById('nf-path');
var btn = document.getElementById('nf-b');
btn.addEventListener('click', function () {
card.classList.remove('is-demo');
var segs = [].slice.call(path.querySelectorAll('.nf__seg'));
if (segs.length > 1) path.removeChild(segs[segs.length - 1]);
if (path.querySelectorAll('.nf__seg').length <= 1) {
btn.disabled = true;
btn.textContent = btn.dataset.home;
}
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./NotFoundCard.module.scss";
export type NotFoundCardProps = {
title?: string;
path?: string[];
action?: string;
homeLabel?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function NotFoundCard({
title = "There is nothing here",
path = ["Home", "Files", "?"],
action = "Up one level",
homeLabel = "This is home",
duration = 2000,
easing = "ease-out",
color = "#fff7e6",
}: NotFoundCardProps) {
const [segs, setSegs] = useState(path);
const [loop, setLoop] = useState(true);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const atHome = segs.length <= 1;
return (
<section className={`${styles.nf} ${loop ? styles.isDemo : ""}`} style={vars}>
<span className={styles.icon} aria-hidden="true">
<svg viewBox="0 0 256 256" width="1em" height="1em">
<g fill="currentColor">
<path d="M128 32a96 96 0 1 0 96 96a96 96 0 0 0-96-96m16 112l-64 32l32-64l64-32Z" opacity=".2" />
<path d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m0 192a88 88 0 1 1 88-88a88.1 88.1 0 0 1-88 88m44.42-143.16l-64 32a8.05 8.05 0 0 0-3.58 3.58l-32 64A8 8 0 0 0 80 184a8.1 8.1 0 0 0 3.58-.84l64-32a8.05 8.05 0 0 0 3.58-3.58l32-64a8 8 0 0 0-10.74-10.74M138 138l-40.11 20.11L118 118l40.15-20.07Z" />
</g>
</svg>
</span>
<h2 className={styles.title}>{title}</h2>
<ol className={styles.path} aria-live="polite">
{segs.map((s, i) => (
<li className={`${styles.seg} ${i === segs.length - 1 && segs.length === path.length ? styles.isGone : ""}`} key={s}>{s}</li>
))}
</ol>
<button
className={styles.btn}
type="button"
disabled={atHome}
onClick={() => { setLoop(false); setSegs(segs.slice(0, -1)); }}
>
{atHome ? homeLabel : action}
</button>
</section>
);
}07 404 card with a wandering needle — Measured area 0.709% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · 9 Empty State UI Design Cards Drawn in Pure CSS
The post this demo lives in
9 Empty State UI Design Cards Drawn in Pure CSSEmpty state UI design fills the space a list would occupy when there is no data to put in it. One of these nine is blank See all demos in this post
If you like this effect - similar demos
Lists & tables01 Priority sort that holds two columns at once9 Data Table UI Patterns — Drag, Resize, Select
Lists & tables01 Dial-rotating marker — stacking an angle value per item so each bullet turns like a clockCSS List Style: 9 Marker Design Patterns
Lists & tables02 Type icons with indent guides9 File Tree CSS Views — Folders That Really Open