09Mobile back-to-parent
Below a breakpoint the whole trail folds into a back row that shows only the parent step, and above it the full path unfolds again.
Best forMobile detail screens where one thumb only ever needs to go up one level
matchMediatranslateXaria-label

<main class="stage">
<div class="mb is-demo" id="mb">
<div class="mb__viewport">
<nav class="mb__nav" aria-label="Breadcrumb">
<ol class="mb__wide" id="mb-wide">
<li class="mb__li"><a class="mb__crumb" href="#">Home</a></li>
<li class="mb__li"><span class="mb__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="mb__crumb" href="#">Electronics</a></li>
<li class="mb__li"><span class="mb__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="mb__crumb" href="#">Laptops</a></li>
<li class="mb__li"><span class="mb__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><span class="mb__crumb mb__crumb--now" aria-current="page">13-inch gaming</span></li>
</ol>
<ol class="mb__narrow">
<li class="mb__li">
<button class="mb__back" type="button" id="mb-back" aria-label="Go back to Laptops">
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true"><g fill="currentColor"><path d="M160 48v160l-80-80Z" opacity=".2"/><path d="M163.06 40.61a8 8 0 0 0-8.72 1.73l-80 80a8 8 0 0 0 0 11.32l80 80A8 8 0 0 0 168 208V48a8 8 0 0 0-4.94-7.39M152 188.69L91.31 128L152 67.31Z"/></g></svg>Laptops
</button>
</li>
</ol>
</nav>
</div>
<div class="mb__chips" role="group" aria-label="Screen width">
<button class="mb__chip" type="button" id="chip-wide" aria-pressed="true">Wide screen</button>
<button class="mb__chip" type="button" id="chip-narrow" aria-pressed="false">Narrow screen</button>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-spring;
$color: $subject-cream;
@include stage($stage-orange);
.mb {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-3;
width: 100%;
max-width: 384px;
}
.mb__viewport {
box-sizing: border-box;
width: 100%;
padding: $sp-1 $sp-2;
border-radius: $r-card;
background: rgba($subject-ink, .92);
overflow: hidden; // 미끄러져 나가는 줄이 문서 폭을 늘리지 않게
}
.mb__nav {
display: grid; // 두 줄을 한 칸에 겹친다 — 절대배치와 달리 부모 높이가 유지된다
}
.mb__wide,
.mb__narrow {
grid-area: 1 / 1;
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: $sp-1;
margin: 0;
padding: 0;
list-style: none;
min-width: 0;
}
.mb__li { display: flex; align-items: center; gap: $sp-1; flex: 0 0 auto; }
.mb__sep { display: inline-flex; flex: none; color: rgba($subject-cream, .45); }
.mb__crumb {
padding: $sp-1 $sp-2;
border-radius: $r-pill;
background: rgba($subject-cream, .18);
color: $color;
font-size: 13px;
line-height: 18px;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
}
a.mb__crumb { cursor: pointer; }
a.mb__crumb:hover { background: rgba($subject-cream, .34); }
a.mb__crumb:focus-visible { outline: 2px solid $subject-cream; outline-offset: 2px; }
.mb__crumb--now {
background: $subject-cream;
color: $subject-ink;
font-weight: 700;
}
.mb__narrow { opacity: 0; pointer-events: none; }
.mb__back {
display: inline-flex;
align-items: center;
gap: $sp-1;
padding: $sp-1 $sp-3;
border: 0;
border-radius: $r-pill;
background: $subject-cream;
color: $subject-ink;
font-family: inherit;
font-size: 13px;
line-height: 18px;
font-weight: 700;
cursor: pointer;
}
.mb__back svg { flex: none; }
.mb__back:focus-visible { outline: 2px solid $subject-cream; outline-offset: 2px; }
// 사람이 전환했을 때 (또는 matchMedia 가 좁은 폭을 알릴 때)
.mb.is-narrow .mb__wide { opacity: 0; pointer-events: none; transform: translateX(-14px); }
.mb.is-narrow .mb__narrow { opacity: 1; pointer-events: auto; transform: translateX(0); }
.mb__wide { transition: transform $duration $easing, opacity $duration $easing; }
.mb__narrow { transition: transform $duration $easing, opacity $duration $easing; }
.mb__chips { display: flex; gap: $sp-1; }
.mb__chip {
padding: 0 $sp-3;
border: 0;
border-radius: $r-pill;
background: rgba($subject-cream, .18);
color: $subject-ink; // 주황 칩 면 위 흐린 크림 2.06:1 → 잉크 6.48:1
box-shadow: inset 0 0 0 1px rgba($subject-cream, .4);
font-family: inherit;
font-size: 12px;
line-height: 24px;
font-weight: 700;
cursor: pointer;
}
.mb__chip[aria-pressed="true"] {
background: $subject-cream;
color: $subject-ink;
box-shadow: none;
}
.mb__chip:focus-visible { outline: 2px solid $subject-cream; outline-offset: 2px; }
// ── 격자 미리보기용 자동 루프 (2초) ──
// 넓은 줄이 왼쪽으로 빠지고 좁은 줄이 오른쪽에서 들어온다(10~34%), 유지(34~62%), 되돌아온다(62~86%)
@keyframes mb-wide {
0%, 10% { transform: translateX(0); opacity: 1; }
34%, 62% { transform: translateX(-14px); opacity: 0; }
86%, 100% { transform: translateX(0); opacity: 1; }
}
@keyframes mb-narrow {
0%, 10% { transform: translateX(14px); opacity: 0; }
34%, 62% { transform: translateX(0); opacity: 1; }
86%, 100% { transform: translateX(14px); opacity: 0; }
}
.mb.is-demo .mb__wide { animation-name: mb-wide; }
.mb.is-demo .mb__narrow { animation-name: mb-narrow; }
.mb.is-demo .mb__wide,
.mb.is-demo .mb__narrow {
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
// 폭 표시 칩의 활성 교체는 한 프레임에 컷 — 반투명 겹침 방지
@keyframes mb-chip {
0%, 33.9% { background: $subject-cream; color: $subject-ink; box-shadow: none; }
34%, 85.9% { background: rgba($subject-cream, .18); color: $subject-ink; box-shadow: inset 0 0 0 1px rgba($subject-cream, .4); }
86%, 100% { background: $subject-cream; color: $subject-ink; box-shadow: none; }
}
@keyframes mb-chip-n {
0%, 33.9% { background: rgba($subject-cream, .18); color: $subject-ink; box-shadow: inset 0 0 0 1px rgba($subject-cream, .4); }
34%, 85.9% { background: $subject-cream; color: $subject-ink; box-shadow: none; }
86%, 100% { background: rgba($subject-cream, .18); color: $subject-ink; box-shadow: inset 0 0 0 1px rgba($subject-cream, .4); }
}
.mb.is-demo .mb__chip {
animation-name: mb-chip;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.mb.is-demo #chip-narrow { animation-name: mb-chip-n; }
@media (max-width: 340px) {
.mb { gap: $sp-2; }
.mb__viewport { padding: 0 $sp-1; border-radius: $r-control; }
.mb__wide, .mb__narrow { gap: 0; }
.mb__li { gap: 0; }
.mb__crumb { padding: 0 $sp-1; font-size: 11px; line-height: 16px; }
.mb__sep svg { width: 10px; height: 10px; }
.mb__back { padding: 0 $sp-2; font-size: 11px; line-height: 20px; }
.mb__back svg { width: 12px; height: 12px; }
.mb__chip { padding: 0 $sp-2; font-size: 11px; line-height: 20px; }
}
@media (prefers-reduced-motion: reduce) {
.mb.is-demo .mb__wide,
.mb.is-demo .mb__narrow,
.mb.is-demo .mb__chip { animation: none !important; }
.mb__wide, .mb__narrow { 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);
}
.mb {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: 100%;
max-width: 384px;
}
.mb__viewport {
box-sizing: border-box;
width: 100%;
padding: 4px 8px;
border-radius: 16px;
background: rgba(23, 20, 26, 0.92);
overflow: hidden;
}
.mb__nav {
display: grid;
}
.mb__wide,
.mb__narrow {
grid-area: 1/1;
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 4px;
margin: 0;
padding: 0;
list-style: none;
min-width: 0;
}
.mb__li {
display: flex;
align-items: center;
gap: 4px;
flex: 0 0 auto;
}
.mb__sep {
display: inline-flex;
flex: none;
color: rgba(255, 247, 230, 0.45);
}
.mb__crumb {
padding: 4px 8px;
border-radius: 999px;
background: rgba(255, 247, 230, 0.18);
color: #fff7e6;
font-size: 13px;
line-height: 18px;
font-weight: 600;
text-decoration: none;
white-space: nowrap;
}
a.mb__crumb {
cursor: pointer;
}
a.mb__crumb:hover {
background: rgba(255, 247, 230, 0.34);
}
a.mb__crumb:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.mb__crumb--now {
background: #fff7e6;
color: #17141a;
font-weight: 700;
}
.mb__narrow {
opacity: 0;
pointer-events: none;
}
.mb__back {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border: 0;
border-radius: 999px;
background: #fff7e6;
color: #17141a;
font-family: inherit;
font-size: 13px;
line-height: 18px;
font-weight: 700;
cursor: pointer;
}
.mb__back svg {
flex: none;
}
.mb__back:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.mb.is-narrow .mb__wide {
opacity: 0;
pointer-events: none;
transform: translateX(-14px);
}
.mb.is-narrow .mb__narrow {
opacity: 1;
pointer-events: auto;
transform: translateX(0);
}
.mb__wide {
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mb__narrow {
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mb__chips {
display: flex;
gap: 4px;
}
.mb__chip {
padding: 0 12px;
border: 0;
border-radius: 999px;
background: rgba(255, 247, 230, 0.18);
color: #17141a;
box-shadow: inset 0 0 0 1px rgba(255, 247, 230, 0.4);
font-family: inherit;
font-size: 12px;
line-height: 24px;
font-weight: 700;
cursor: pointer;
}
.mb__chip[aria-pressed=true] {
background: #fff7e6;
color: #17141a;
box-shadow: none;
}
.mb__chip:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
@keyframes mb-wide {
0%, 10% {
transform: translateX(0);
opacity: 1;
}
34%, 62% {
transform: translateX(-14px);
opacity: 0;
}
86%, 100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes mb-narrow {
0%, 10% {
transform: translateX(14px);
opacity: 0;
}
34%, 62% {
transform: translateX(0);
opacity: 1;
}
86%, 100% {
transform: translateX(14px);
opacity: 0;
}
}
.mb.is-demo .mb__wide {
animation-name: mb-wide;
}
.mb.is-demo .mb__narrow {
animation-name: mb-narrow;
}
.mb.is-demo .mb__wide,
.mb.is-demo .mb__narrow {
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
@keyframes mb-chip {
0%, 33.9% {
background: #fff7e6;
color: #17141a;
box-shadow: none;
}
34%, 85.9% {
background: rgba(255, 247, 230, 0.18);
color: #17141a;
box-shadow: inset 0 0 0 1px rgba(255, 247, 230, 0.4);
}
86%, 100% {
background: #fff7e6;
color: #17141a;
box-shadow: none;
}
}
@keyframes mb-chip-n {
0%, 33.9% {
background: rgba(255, 247, 230, 0.18);
color: #17141a;
box-shadow: inset 0 0 0 1px rgba(255, 247, 230, 0.4);
}
34%, 85.9% {
background: #fff7e6;
color: #17141a;
box-shadow: none;
}
86%, 100% {
background: rgba(255, 247, 230, 0.18);
color: #17141a;
box-shadow: inset 0 0 0 1px rgba(255, 247, 230, 0.4);
}
}
.mb.is-demo .mb__chip {
animation-name: mb-chip;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
.mb.is-demo #chip-narrow {
animation-name: mb-chip-n;
}
@media (max-width: 340px) {
.mb {
gap: 8px;
}
.mb__viewport {
padding: 0 4px;
border-radius: 12px;
}
.mb__wide, .mb__narrow {
gap: 0;
}
.mb__li {
gap: 0;
}
.mb__crumb {
padding: 0 4px;
font-size: 11px;
line-height: 16px;
}
.mb__sep svg {
width: 10px;
height: 10px;
}
.mb__back {
padding: 0 8px;
font-size: 11px;
line-height: 20px;
}
.mb__back svg {
width: 12px;
height: 12px;
}
.mb__chip {
padding: 0 8px;
font-size: 11px;
line-height: 20px;
}
}
@media (prefers-reduced-motion: reduce) {
.mb.is-demo .mb__wide,
.mb.is-demo .mb__narrow,
.mb.is-demo .mb__chip {
animation: none !important;
}
.mb__wide, .mb__narrow {
transition: none;
}
}// 실제 동작: matchMedia('(max-width: 420px)') 를 상시 구독해 실제 폭을 따르고,
// 폭 칩을 누르면 수동 모드로 덮어쓴다. 뒤로 가기 버튼은 aria-label 그대로 Enter 로도 동작.
var root = document.getElementById('mb');
var chips = { wide: document.getElementById('chip-wide'), narrow: document.getElementById('chip-narrow') };
var manual = null;
var mq = window.matchMedia('(max-width: 420px)');
function apply(narrow) {
root.classList.remove('is-demo');
root.classList.toggle('is-narrow', narrow);
chips.wide.setAttribute('aria-pressed', String(!narrow));
chips.narrow.setAttribute('aria-pressed', String(narrow));
}
function reflect() { apply(manual === null ? mq.matches : manual); }
if (mq.addEventListener) { mq.addEventListener('change', function () { manual = null; reflect(); }); }
chips.wide.addEventListener('click', function () { manual = false; reflect(); });
chips.narrow.addEventListener('click', function () { manual = true; reflect(); });
document.getElementById('mb-back').addEventListener('click', function () {
root.classList.remove('is-demo', 'is-narrow');
});import { useEffect, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./MobileBackParent.module.scss";
export type BackCrumb = { name: string };
export type MobileBackParentProps = {
crumbs?: BackCrumb[];
duration?: number;
easing?: string;
color?: string;
};
// 도구/demo.py 의 ambient.d.ts 는 React 이벤트 제네릭을 안 준다 — 로컬 타입으로 TS7006 을 막는다
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type Mq = { matches: boolean; addEventListener(t: string, fn: () => void): void; removeEventListener(t: string, fn: () => void): void };
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 IconCaretLeft = () => (
<svg viewBox="0 0 256 256" width="14" height="14" aria-hidden="true"><g fill="currentColor"><path d="M160 48v160l-80-80Z" opacity=".2"/><path d="M163.06 40.61a8 8 0 0 0-8.72 1.73l-80 80a8 8 0 0 0 0 11.32l80 80A8 8 0 0 0 168 208V48a8 8 0 0 0-4.94-7.39M152 188.69L91.31 128L152 67.31Z"/></g></svg>
);
const DEFAULT_CRUMBS: BackCrumb[] = [
{ name: "Home" },
{ name: "Electronics" },
{ name: "Laptops" },
{ name: "13-inch gaming" },
];
export default function MobileBackParent({
crumbs = DEFAULT_CRUMBS,
duration = 300,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#fff7e6",
}: MobileBackParentProps) {
const [narrow, setNarrow] = useState(false);
const [manual, setManual] = useState<boolean | null>(null); // null = 미디어 질의를 따른다
const [demo, setDemo] = useState(true); // 격자 미리보기용 자동 루프
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
useEffect(() => {
const mq = (window as { matchMedia(q: string): Mq }).matchMedia("(max-width: 420px)");
const onChange = (): void => { setManual(null); setNarrow(mq.matches); setDemo(false); };
if (mq.addEventListener) mq.addEventListener("change", onChange);
return () => { if (mq.removeEventListener) mq.removeEventListener("change", onChange); };
}, []);
function force(v: boolean) {
setManual(v);
setNarrow(v);
setDemo(false);
}
const parent = crumbs[crumbs.length - 2];
return (
<div className={`${styles.mb} ${demo ? styles.demo : ""} ${narrow ? styles.isNarrow : ""}`} style={vars}>
<div className={styles.viewport}>
<nav className={styles.nav} aria-label="Breadcrumb">
<ol className={styles.wide}>
{crumbs.map((c: BackCrumb, i: number) => (
<li key={c.name} className={styles.li}>
{i > 0 && <span className={styles.sep} aria-hidden="true"><IconCaretRight /></span>}
{i === crumbs.length - 1 ? (
<span className={`${styles.crumb} ${styles.now}`} aria-current="page">{c.name}</span>
) : (
<a className={styles.crumb} href="#">{c.name}</a>
)}
</li>
))}
</ol>
<ol className={styles.narrowOl}>
<li className={styles.li}>
<button
type="button"
className={styles.back}
aria-label={`Go back to ${parent ? parent.name : ""}`}
onClick={(): void => { setNarrow(false); setDemo(false); }}
>
<IconCaretLeft />{parent ? parent.name : ""}
</button>
</li>
</ol>
</nav>
</div>
<div className={styles.chips} role="group" aria-label="Screen width">
<button type="button" className={styles.chip} aria-pressed={!narrow} onClick={(): void => force(false)}>Wide screen</button>
<button type="button" className={styles.chip} aria-pressed={narrow} onClick={(): void => force(true)}>Narrow screen</button>
</div>
</div>
);
}09 Mobile back-to-parent — Measured area 7.014% · moving frames 14/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

