08Structured-data trail
The visible trail is mirrored once more as BreadcrumbList JSON-LD, and the panel beside it previews the line that snippet turns into on a results page.
Best forProduct and article pages that want a category trail in search results instead of a raw URL
BreadcrumbListapplication/ld+jsonitemListElement

<main class="stage">
<div class="jl is-demo" id="jl">
<div class="jl__serp">
<div class="jl__url">
<span class="jl__glass" aria-hidden="true"><svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true"><g fill="currentColor"><path d="M192 112a80 80 0 1 1-80-80a80 80 0 0 1 80 80" opacity=".2"/><path d="m229.66 218.34l-50.06-50.06a88.21 88.21 0 1 0-11.32 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72"/></g></svg></span>
<nav class="jl__nav" aria-label="Breadcrumb">
<ol class="jl__trail" id="jl-trail">
<li class="jl__li"><a class="jl__crumb" href="#" data-i="0" data-seg="">godrichstory.com</a></li>
<li class="jl__li"><span class="jl__sep" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12" aria-hidden="true"><g fill="currentColor"><path d="m176 128l-80 80V48Z" opacity=".2"/><path d="m181.66 122.34l-80-80A8 8 0 0 0 88 48v160a8 8 0 0 0 13.66 5.66l80-80a8 8 0 0 0 0-11.32M104 188.69V67.31L164.69 128Z"/></g></svg></span><a class="jl__crumb" href="#" data-i="1" data-seg="electronics">Electronics</a></li>
<li class="jl__li"><span class="jl__sep" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12" aria-hidden="true"><g fill="currentColor"><path d="m176 128l-80 80V48Z" opacity=".2"/><path d="m181.66 122.34l-80-80A8 8 0 0 0 88 48v160a8 8 0 0 0 13.66 5.66l80-80a8 8 0 0 0 0-11.32M104 188.69V67.31L164.69 128Z"/></g></svg></span><a class="jl__crumb" href="#" data-i="2" data-seg="electronics/laptops" aria-current="page">Laptops</a></li>
</ol>
</nav>
</div>
<p class="jl__title">13-inch gaming laptop</p>
</div>
<div class="jl__panel">
<p class="jl__head"><svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true"><g fill="currentColor"><path d="m240 128l-48 40H64l-48-40l48-40h128Z" opacity=".2"/><path d="M69.12 94.15L28.5 128l40.62 33.85a8 8 0 1 1-10.24 12.29l-48-40a8 8 0 0 1 0-12.29l48-40a8 8 0 0 1 10.24 12.3m176 27.7l-48-40a8 8 0 1 0-10.24 12.3L227.5 128l-40.62 33.85a8 8 0 1 0 10.24 12.29l48-40a8 8 0 0 0 0-12.29m-82.39-89.37a8 8 0 0 0-10.25 4.79l-64 176a8 8 0 0 0 4.79 10.26A8.1 8.1 0 0 0 96 224a8 8 0 0 0 7.52-5.27l64-176a8 8 0 0 0-4.79-10.25"/></g></svg>application/ld+json</p>
<div class="jl__codewrap">
<span class="jl__bar" aria-hidden="true"></span>
<pre class="jl__code" id="jl-code"><span class="jl__ln">"itemListElement": [</span><span class="jl__ln"> { "position": 1, "name": "godrichstory.com" },</span><span class="jl__ln"> { "position": 2, "name": "Electronics" },</span><span class="jl__ln"> { "position": 3, "name": "Laptops" } ]</span></pre>
</div>
</div>
<button class="jl__btn" type="button" id="jl-make">Build JSON-LD</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $stage-yellow;
@include stage($stage-ink);
.jl {
--line: 16px;
display: flex;
flex-direction: column;
align-items: stretch;
gap: $sp-3;
width: 100%;
max-width: 400px;
}
// ── 위: 검색 결과 미리보기 ──
.jl__serp {
box-sizing: border-box;
padding: $sp-3;
border-radius: $r-card;
background: rgba($subject-cream, .07);
box-shadow: inset 0 0 0 1px rgba($subject-cream, .14);
}
.jl__url { display: flex; align-items: center; gap: $sp-1; }
.jl__glass { display: inline-flex; flex: none; color: rgba($subject-cream, .5); }
.jl__nav { min-width: 0; }
.jl__trail {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: $sp-1;
margin: 0;
padding: 0;
list-style: none;
}
.jl__li { display: flex; align-items: center; gap: $sp-1; flex: 0 0 auto; }
.jl__li[hidden] { display: none; }
.jl__sep { display: inline-flex; flex: none; color: rgba($subject-cream, .34); }
.jl__crumb {
padding: 0 $sp-1;
border-radius: $r-xs;
background: transparent;
color: rgba($subject-cream, .72);
font-size: 12px;
line-height: 18px;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
}
.jl__crumb:hover { background: rgba($subject-cream, .16); color: $subject-cream; }
.jl__crumb:focus-visible { outline: 2px solid $color; outline-offset: 1px; }
.jl__title {
margin: $sp-1 0 0;
color: $subject-sky;
font-size: 15px;
line-height: 22px;
font-weight: 700;
}
// ── 아래: JSON-LD 조각 ──
.jl__panel {
box-sizing: border-box;
padding: $sp-2;
border-radius: $r-sm;
background: rgba($subject-cream, .06);
box-shadow: inset 0 0 0 1px rgba($subject-cream, .12);
}
.jl__head {
display: flex;
align-items: center;
gap: $sp-1;
margin: 0 0 $sp-1;
color: rgba($subject-cream, .5);
font-size: 10px;
line-height: 14px;
font-weight: 700;
letter-spacing: .04em;
}
.jl__codewrap { position: relative; }
.jl__code {
margin: 0;
color: rgba($subject-cream, .86);
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 11px;
line-height: 16px;
white-space: pre;
}
.jl__ln { display: block; height: var(--line); }
.jl__bar {
position: absolute;
left: 0;
right: 0;
top: 0;
height: var(--line);
border-radius: $r-xs;
background: rgba($stage-yellow, .3);
box-shadow: inset 2px 0 0 $stage-yellow;
opacity: 0;
pointer-events: none;
}
.jl__btn {
align-self: flex-start;
padding: $sp-1 $sp-3;
border: 0;
border-radius: $r-pill;
background: $stage-yellow;
color: $subject-ink;
font-family: inherit;
font-size: 12px;
line-height: 16px;
font-weight: 700;
cursor: pointer;
}
.jl__btn:focus-visible { outline: 2px solid $subject-cream; outline-offset: 2px; }
// ── 격자 미리보기용 자동 루프 (2초) ──
// 강조 막대가 네 줄을 차례로 훑고(0~72%) 첫 줄로 연속 복귀(72~88%).
// 그 줄에 해당하는 경로 조각은 steps(1, end) 로 한 프레임에 켜졌다 꺼진다
.jl.is-demo .jl__bar {
opacity: 1;
animation-name: jl-scan;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
@keyframes jl-scan {
0%, 6% { transform: translateY(0); }
22%, 28% { transform: translateY(calc(var(--line) * 1)); }
44%, 50% { transform: translateY(calc(var(--line) * 2)); }
66%, 72% { transform: translateY(calc(var(--line) * 3)); }
88%, 100% { transform: translateY(0); }
}
.jl.is-demo .jl__crumb {
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.jl.is-demo .jl__li:nth-child(1) .jl__crumb { animation-name: jl-hit-1; }
.jl.is-demo .jl__li:nth-child(2) .jl__crumb { animation-name: jl-hit-2; }
.jl.is-demo .jl__li:nth-child(3) .jl__crumb { animation-name: jl-hit-3; }
@keyframes jl-hit-1 {
0% { background: transparent; color: rgba($subject-cream, .72); }
22% { background: $stage-yellow; color: $subject-ink; }
44% { background: transparent; color: rgba($subject-cream, .72); }
100% { background: transparent; color: rgba($subject-cream, .72); }
}
@keyframes jl-hit-2 {
0% { background: transparent; color: rgba($subject-cream, .72); }
44% { background: $stage-yellow; color: $subject-ink; }
66% { background: transparent; color: rgba($subject-cream, .72); }
100% { background: transparent; color: rgba($subject-cream, .72); }
}
@keyframes jl-hit-3 {
0% { background: transparent; color: rgba($subject-cream, .72); }
66% { background: $stage-yellow; color: $subject-ink; }
88% { background: transparent; color: rgba($subject-cream, .72); }
100% { background: transparent; color: rgba($subject-cream, .72); }
}
// 320×200 — 줄 수를 줄이지 않고 글자 크기·줄 높이만 줄인다 (내용을 없애면 데모가 거짓이 된다)
@media (max-width: 340px) {
.jl { --line: 13px; gap: $sp-1; }
.jl__serp { padding: $sp-1; border-radius: $r-sm; }
.jl__crumb { font-size: 10px; line-height: 15px; }
.jl__sep svg { width: 10px; height: 10px; }
.jl__glass svg { width: 12px; height: 12px; }
.jl__title { margin: 0; font-size: 12px; line-height: 17px; }
.jl__panel { padding: $sp-1; }
.jl__head { margin: 0; font-size: 9px; line-height: 12px; }
.jl__head svg { width: 11px; height: 11px; }
.jl__code { font-size: 9px; line-height: 13px; }
.jl__btn { padding: 0 $sp-2; font-size: 11px; line-height: 18px; }
}
@media (prefers-reduced-motion: reduce) {
.jl.is-demo .jl__bar,
.jl.is-demo .jl__crumb { animation: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.jl {
--line: 16px;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 12px;
width: 100%;
max-width: 400px;
}
.jl__serp {
box-sizing: border-box;
padding: 12px;
border-radius: 16px;
background: rgba(255, 247, 230, 0.07);
box-shadow: inset 0 0 0 1px rgba(255, 247, 230, 0.14);
}
.jl__url {
display: flex;
align-items: center;
gap: 4px;
}
.jl__glass {
display: inline-flex;
flex: none;
color: rgba(255, 247, 230, 0.5);
}
.jl__nav {
min-width: 0;
}
.jl__trail {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 4px;
margin: 0;
padding: 0;
list-style: none;
}
.jl__li {
display: flex;
align-items: center;
gap: 4px;
flex: 0 0 auto;
}
.jl__li[hidden] {
display: none;
}
.jl__sep {
display: inline-flex;
flex: none;
color: rgba(255, 247, 230, 0.34);
}
.jl__crumb {
padding: 0 4px;
border-radius: 4px;
background: transparent;
color: rgba(255, 247, 230, 0.72);
font-size: 12px;
line-height: 18px;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
}
.jl__crumb:hover {
background: rgba(255, 247, 230, 0.16);
color: #fff7e6;
}
.jl__crumb:focus-visible {
outline: 2px solid #ffd23f;
outline-offset: 1px;
}
.jl__title {
margin: 4px 0 0;
color: #6ec8ff;
font-size: 15px;
line-height: 22px;
font-weight: 700;
}
.jl__panel {
box-sizing: border-box;
padding: 8px;
border-radius: 8px;
background: rgba(255, 247, 230, 0.06);
box-shadow: inset 0 0 0 1px rgba(255, 247, 230, 0.12);
}
.jl__head {
display: flex;
align-items: center;
gap: 4px;
margin: 0 0 4px;
color: rgba(255, 247, 230, 0.5);
font-size: 10px;
line-height: 14px;
font-weight: 700;
letter-spacing: 0.04em;
}
.jl__codewrap {
position: relative;
}
.jl__code {
margin: 0;
color: rgba(255, 247, 230, 0.86);
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 11px;
line-height: 16px;
white-space: pre;
}
.jl__ln {
display: block;
height: var(--line);
}
.jl__bar {
position: absolute;
left: 0;
right: 0;
top: 0;
height: var(--line);
border-radius: 4px;
background: rgba(255, 210, 63, 0.3);
box-shadow: inset 2px 0 0 #ffd23f;
opacity: 0;
pointer-events: none;
}
.jl__btn {
align-self: flex-start;
padding: 4px 12px;
border: 0;
border-radius: 999px;
background: #ffd23f;
color: #17141a;
font-family: inherit;
font-size: 12px;
line-height: 16px;
font-weight: 700;
cursor: pointer;
}
.jl__btn:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.jl.is-demo .jl__bar {
opacity: 1;
animation-name: jl-scan;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
@keyframes jl-scan {
0%, 6% {
transform: translateY(0);
}
22%, 28% {
transform: translateY(calc(var(--line) * 1));
}
44%, 50% {
transform: translateY(calc(var(--line) * 2));
}
66%, 72% {
transform: translateY(calc(var(--line) * 3));
}
88%, 100% {
transform: translateY(0);
}
}
.jl.is-demo .jl__crumb {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.jl.is-demo .jl__li:nth-child(1) .jl__crumb {
animation-name: jl-hit-1;
}
.jl.is-demo .jl__li:nth-child(2) .jl__crumb {
animation-name: jl-hit-2;
}
.jl.is-demo .jl__li:nth-child(3) .jl__crumb {
animation-name: jl-hit-3;
}
@keyframes jl-hit-1 {
0% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
22% {
background: #ffd23f;
color: #17141a;
}
44% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
100% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
}
@keyframes jl-hit-2 {
0% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
44% {
background: #ffd23f;
color: #17141a;
}
66% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
100% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
}
@keyframes jl-hit-3 {
0% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
66% {
background: #ffd23f;
color: #17141a;
}
88% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
100% {
background: transparent;
color: rgba(255, 247, 230, 0.72);
}
}
@media (max-width: 340px) {
.jl {
--line: 13px;
gap: 4px;
}
.jl__serp {
padding: 4px;
border-radius: 8px;
}
.jl__crumb {
font-size: 10px;
line-height: 15px;
}
.jl__sep svg {
width: 10px;
height: 10px;
}
.jl__glass svg {
width: 12px;
height: 12px;
}
.jl__title {
margin: 0;
font-size: 12px;
line-height: 17px;
}
.jl__panel {
padding: 4px;
}
.jl__head {
margin: 0;
font-size: 9px;
line-height: 12px;
}
.jl__head svg {
width: 11px;
height: 11px;
}
.jl__code {
font-size: 9px;
line-height: 13px;
}
.jl__btn {
padding: 0 8px;
font-size: 11px;
line-height: 18px;
}
}
@media (prefers-reduced-motion: reduce) {
.jl.is-demo .jl__bar,
.jl.is-demo .jl__crumb {
animation: none !important;
}
}{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"godrichstory.com","item":"https://godrichstory.com/"},{"@type":"ListItem","position":2,"name":"Electronics","item":"https://godrichstory.com/electronics"},{"@type":"ListItem","position":3,"name":"Laptops","item":"https://godrichstory.com/electronics/laptops"}]}
// 실제 동작: 화면의 <ol><li> 를 읽어 position·name·item 을 만들고
// <script type="application/ld+json"> 과 <pre> 를 다시 쓴다. 칸을 누르면 경로도 JSON-LD 도 같이 줄어든다.
var root = document.getElementById('jl');
var trail = document.getElementById('jl-trail');
var pre = document.getElementById('jl-code');
var ld = document.getElementById('jl-ld');
var all = [].slice.call(trail.querySelectorAll('.jl__li'));
function build() {
var out = [], n = 0;
for (var i = 0; i < all.length; i++) {
if (all[i].hidden) continue;
var a = all[i].querySelector('.jl__crumb');
n++;
out.push({ "@type": "ListItem", position: n, name: a.textContent.trim(),
item: "https://godrichstory.com/" + a.getAttribute('data-seg') });
}
ld.textContent = JSON.stringify({ "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: out });
var lines = ['"itemListElement": ['];
for (var k = 0; k < out.length; k++) {
lines.push(' { "position": ' + out[k].position + ', "name": "' + out[k].name + '" }' + (k < out.length - 1 ? ',' : ' ]'));
}
pre.textContent = '';
for (var j = 0; j < lines.length; j++) {
var s = document.createElement('span');
s.className = 'jl__ln';
s.textContent = lines[j];
pre.appendChild(s);
}
}
document.getElementById('jl-make').addEventListener('click', function () {
root.classList.remove('is-demo');
build();
});
trail.addEventListener('click', function (e) {
var a = e.target.closest('.jl__crumb');
if (!a) return;
e.preventDefault();
root.classList.remove('is-demo');
var i = +a.getAttribute('data-i');
for (var k = 0; k < all.length; k++) {
all[k].hidden = k > i;
all[k].querySelector('.jl__crumb').setAttribute('aria-current', k === i ? 'page' : 'false');
}
build();
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./JsonLdTrail.module.scss";
export type TrailStep = { name: string; seg: string };
export type JsonLdTrailProps = {
steps?: TrailStep[];
title?: string;
origin?: string;
duration?: number;
easing?: string;
color?: string;
};
// 도구/demo.py 의 ambient.d.ts 는 React 이벤트 제네릭을 안 준다 — 로컬 타입으로 TS7006 을 막는다
type ClickEvt = { preventDefault(): void };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const IconGlass = () => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true"><g fill="currentColor"><path d="M192 112a80 80 0 1 1-80-80a80 80 0 0 1 80 80" opacity=".2"/><path d="m229.66 218.34l-50.06-50.06a88.21 88.21 0 1 0-11.32 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72"/></g></svg>
);
const IconCaretRight = () => (
<svg viewBox="0 0 256 256" width="12" height="12" aria-hidden="true"><g fill="currentColor"><path d="m176 128l-80 80V48Z" opacity=".2"/><path d="m181.66 122.34l-80-80A8 8 0 0 0 88 48v160a8 8 0 0 0 13.66 5.66l80-80a8 8 0 0 0 0-11.32M104 188.69V67.31L164.69 128Z"/></g></svg>
);
const IconCode = () => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true"><g fill="currentColor"><path d="m240 128l-48 40H64l-48-40l48-40h128Z" opacity=".2"/><path d="M69.12 94.15L28.5 128l40.62 33.85a8 8 0 1 1-10.24 12.29l-48-40a8 8 0 0 1 0-12.29l48-40a8 8 0 0 1 10.24 12.3m176 27.7l-48-40a8 8 0 1 0-10.24 12.3L227.5 128l-40.62 33.85a8 8 0 1 0 10.24 12.29l48-40a8 8 0 0 0 0-12.29m-82.39-89.37a8 8 0 0 0-10.25 4.79l-64 176a8 8 0 0 0 4.79 10.26A8.1 8.1 0 0 0 96 224a8 8 0 0 0 7.52-5.27l64-176a8 8 0 0 0-4.79-10.25"/></g></svg>
);
const DEFAULT_STEPS: TrailStep[] = [
{ name: "godrichstory.com", seg: "" },
{ name: "Electronics", seg: "electronics" },
{ name: "Laptops", seg: "electronics/laptops" },
];
export default function JsonLdTrail({
steps = DEFAULT_STEPS,
title = "13-inch gaming laptop",
origin = "https://godrichstory.com/",
duration = 2000,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#ffd23f",
}: JsonLdTrailProps) {
const [shown, setShown] = useState(steps.length);
const [live, setLive] = useState(false); // 사람이 만지면 자동 루프를 뗀다
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const visible = steps.slice(0, shown);
const itemListElement = visible.map((s: TrailStep, i: number) => ({
"@type": "ListItem",
position: i + 1,
name: s.name,
item: origin + s.seg,
}));
const jsonld = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement,
};
const lines = ['"itemListElement": ['].concat(
itemListElement.map((o, i) =>
` { "position": ${o.position}, "name": "${o.name}" }` + (i < itemListElement.length - 1 ? "," : " ]")
)
);
return (
<div className={`${styles.jl} ${live ? "" : styles.demo}`} style={vars}>
<div className={styles.serp}>
<div className={styles.url}>
<span className={styles.glass} aria-hidden="true"><IconGlass /></span>
<nav className={styles.nav} aria-label="Breadcrumb">
<ol className={styles.trail}>
{visible.map((s: TrailStep, i: number) => (
<li key={s.name} className={styles.li}>
{i > 0 && <span className={styles.sep} aria-hidden="true"><IconCaretRight /></span>}
<a
className={styles.crumb}
href={origin + s.seg}
aria-current={i === visible.length - 1 ? "page" : undefined}
onClick={(e: ClickEvt): void => { e.preventDefault(); setLive(true); setShown(i + 1); }}
>
{s.name}
</a>
</li>
))}
</ol>
</nav>
</div>
<p className={styles.title}>{title}</p>
</div>
<div className={styles.panel}>
<p className={styles.head}><IconCode />application/ld+json</p>
<div className={styles.codewrap}>
<span className={styles.bar} aria-hidden="true" />
<pre className={styles.code}>
{lines.map((l: string) => <span key={l} className={styles.ln}>{l}</span>)}
</pre>
</div>
</div>
<button type="button" className={styles.btn} onClick={(): void => { setLive(true); setShown(steps.length); }}>
Build JSON-LD
</button>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonld) }} />
</div>
);
}08 Structured-data trail — Measured area 19.506% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · 9 Breadcrumb UI Patterns That Survive Deep Paths
The post this demo lives in
9 Breadcrumb UI Patterns That Survive Deep PathsBreadcrumb UI is the trail of links at the top of a page showing where you are, from Home to the item in view. Nine work See all demos in this post
If you like this effect - similar demos
Interactions06 Dice That Leave Roll Marks and Dust on the FloorDice Roll Animation: 9 Scenes with Top-Face Math
Interactions04 Tails Drawn From Position History and a Glow That Brightens Where They OverlapCSS Fireworks: 9 Bursts, Trails and Reflections
Interactions01 Cursor shape morph9 Mouse Effects CSS Tricks You Can Copy-Paste