Breadcrumbs
Interactive UI Explorer
아래에서 각 컴포넌트의 다양한 Variants와 States를 문서화된 형태로 테스트해볼 수 있습니다.
Live Preview
Breadcrumbs
페이지 위계 네비게이션. items 배열의 마지막 항목(href 없음)이 현재 페이지로 자동 표시됩니다. maxItems로 중간 항목을 ... 로 축약할 수 있습니다.
Basic
가장 일반적인 형태. 마지막 항목이 현재 페이지로 강조됩니다.
Source Code
Live Preview
Custom separator
separator props로 구분자 교체.
Source Code
Live Preview
Truncated (maxItems)
긴 경로는 maxItems로 축약 — 첫 항목 + ... + 끝쪽 (maxItems-1)개.
Source Code
Live Preview
Implementation
제작 코드
이 컴포넌트가 실제로 어떻게 구현되어 있는지 — 본체 .tsx 파일을 그대로 보여줍니다. variant 매핑·접근성 처리·forwardRef 패턴 등 디테일을 그대로 확인할 수 있어요.
Breadcrumbstypescript
'use client';
/**
* Breadcrumbs — 페이지 위계 네비게이션.
*
* - items 배열 — 마지막 항목(href 없음)이 현재 페이지로 자동 표시
* - separator 커스텀 가능 (default: ›)
* - maxItems 초과 시 중간을 ... 으로 축약
*/
import Link from 'next/link';
import { Fragment, forwardRef, type HTMLAttributes, type ReactNode } from 'react';
import { cn } from '@/lib/cn';
export interface BreadcrumbItem {
label: ReactNode;
/** 없으면 현재 페이지(non-link)로 표시 */
href?: string;
/** 좌측 아이콘 (예: 홈 아이콘) */
icon?: ReactNode;
}
export type BreadcrumbsTheme = 'light' | 'dark';
export interface BreadcrumbsProps extends HTMLAttributes<HTMLElement> {
items: BreadcrumbItem[];
/** 구분자 (default: › ) */
separator?: ReactNode;
/** maxItems 지정 시 중간 항목들을 ... 으로 축약 (첫·마지막은 항상 노출). default 무한 */
maxItems?: number;
/** 색상 토큰 — light(기본, 흰 배경용) / dark(slate-900 이상 어두운 배경용) */
theme?: BreadcrumbsTheme;
}
const themeStyles: Record<
BreadcrumbsTheme,
{
separator: string;
ellipsis: string;
current: string;
link: string;
}
> = {
light: {
separator: 'text-slate-300',
ellipsis: 'text-slate-400',
current: 'text-slate-900',
link: 'text-slate-500 hover:text-slate-900',
},
dark: {
separator: 'text-slate-600',
ellipsis: 'text-slate-500',
current: 'text-slate-100',
link: 'text-slate-400 hover:text-slate-200',
},
};
function DefaultSeparator({ colorClass }: { colorClass: string }) {
return (
<svg
className={cn('size-3.5 shrink-0', colorClass)}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
aria-hidden
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
);
}
export const Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(
({ items, separator, maxItems, theme = 'light', className, ...rest }, ref) => {
const t = themeStyles[theme];
const sep = separator ?? <DefaultSeparator colorClass={t.separator} />;
/** maxItems 축약 — 첫 1개 + ... + 끝쪽 (maxItems-1)개 */
let displayItems: (BreadcrumbItem | { ellipsis: true })[] = items;
if (maxItems && items.length > maxItems && maxItems >= 2) {
const tailCount = maxItems - 1;
displayItems = [items[0], { ellipsis: true }, ...items.slice(items.length - tailCount)];
}
return (
<nav
ref={ref}
aria-label="Breadcrumb"
className={cn('flex w-full items-center text-sm', className)}
{...rest}
>
<ol className="flex flex-wrap items-center gap-1.5">
{displayItems.map((item, idx) => {
const isLast = idx === displayItems.length - 1;
const key = idx;
if ('ellipsis' in item) {
return (
<Fragment key={`ellipsis-${key}`}>
<li className="flex items-center">
<span
aria-hidden
className={cn('inline-flex size-6 items-center justify-center rounded', t.ellipsis)}
>
…
</span>
</li>
{!isLast && <li aria-hidden>{sep}</li>}
</Fragment>
);
}
const isCurrent = isLast || !item.href;
const content = (
<span className="inline-flex items-center gap-1.5">
{item.icon && (
<span className="inline-flex size-3.5 items-center justify-center" aria-hidden>
{item.icon}
</span>
)}
<span className="truncate">{item.label}</span>
</span>
);
return (
<Fragment key={key}>
<li className="flex items-center">
{isCurrent ? (
<span
aria-current="page"
className={cn('inline-flex max-w-[16rem] items-center font-semibold', t.current)}
>
{content}
</span>
) : (
<Link
href={item.href!}
className={cn(
'inline-flex max-w-[12rem] items-center font-medium underline-offset-4 transition-colors hover:underline',
t.link,
)}
>
{content}
</Link>
)}
</li>
{!isLast && <li aria-hidden>{sep}</li>}
</Fragment>
);
})}
</ol>
</nav>
);
},
);
Breadcrumbs.displayName = 'Breadcrumbs';