컴포넌트
관련 항목 줄 목록
한 줄 전체가 링크인 목록이에요. 카드보다 조용해서 길어도 읽기 편해요.
- #arc
- #component
- #list
- #link
- #related
- #navigation
- #rows
1280px
설치
이 명령 한 줄이면 필요한 파일과 패키지가 함께 설치돼요.
터미널터미널터미널bash
npx shadcn@latest add https://dsui.vercel.app/r/related-list.json파일
components/arc/related-list/related-list.module.csscomponents/arc/related-list/related-list.tsx
함께 오는 항목
related-list.module.cssrelated-list.module.cssrelated-list.module.csscss
.list { display: flex; margin: 0; padding: 0; flex-direction: column; overflow: hidden; border: 1px solid var(--border-subtle); border-radius: var(--radius-panel); background: var(--surface); list-style: none; }
.item + .item { border-top: 1px solid var(--border-subtle); }
.row { display: flex; min-height: var(--control-height-lg); align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-5); color: var(--foreground); text-decoration: none; -webkit-tap-highlight-color: transparent; transition: background var(--duration-fast) var(--ease-standard); }
.title { flex: none; font-size: var(--text-base); font-weight: 500; }
/* One line only: a row that wraps stops being scannable, which is the whole point of a list over cards. */
.description { min-width: 0; flex: 1 1 auto; overflow: hidden; color: var(--text-secondary); font-size: var(--text-sm); text-overflow: ellipsis; white-space: nowrap; }
.meta { flex: none; }
.chevron { flex: none; margin-left: auto; color: var(--text-secondary); transition: color var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard); }
.description + .chevron { margin-left: 0; }
.row[aria-current="page"] { background: var(--accent-subtle); }
.row[aria-current="page"] .chevron { color: var(--accent); }
@media (hover: hover) and (pointer: fine) {
.row:hover { background: var(--surface-muted); }
.row:hover .chevron { color: var(--accent); transform: translateX(2px); }
.row[aria-current="page"]:hover { background: var(--accent-subtle); }
}
.row:active { background: var(--surface-sunken); }
/* On a phone the description would leave nothing for the title, so it steps under it. */
@media (max-width: 599px) {
.row { flex-wrap: wrap; padding-block: var(--space-3); }
.title { flex: 1 1 100%; }
.description { flex: 1 1 auto; }
}
@media (prefers-reduced-motion: reduce) { .row, .chevron { transition: none; } .row:hover .chevron { transform: none; } }
related-list.tsxrelated-list.tsxrelated-list.tsxtsx
"use client";
import type { ReactNode } from "react";
import { ArcLink } from "../lib/arc-provider";
import { playUiSound } from "../sound/play";
import { UiIcon } from "../theme/ui-icon";
import styles from "./related-list.module.css";
export interface RelatedItem {
href: string;
title: string;
/** One line, cut with an ellipsis when it does not fit. */
description?: string;
/** A badge or chip at the end of the row, before the chevron. */
meta?: ReactNode;
current?: boolean;
}
export interface RelatedListProps {
items: RelatedItem[];
/** The accessible name of the list, e.g. "관련 항목". */
label?: string;
className?: string;
}
/**
* Rows where the whole row is the link. Quieter than a grid of cards, so a long list of "related" or "more like this"
* stays readable: one rule between rows and nothing else. The description is cut to a single line — a row that wraps
* stops being scannable, which is the only reason to choose this over LinkCard.
*/
export function RelatedList({ items, label, className }: RelatedListProps) {
return (
<ul aria-label={label} className={[styles.list, className].filter(Boolean).join(" ")}>
{items.map(item => (
<li key={item.href} className={styles.item}>
<ArcLink
href={item.href}
aria-current={item.current ? "page" : undefined}
className={styles.row}
onClick={() => playUiSound("tick")}
>
<span className={styles.title}>{item.title}</span>
{item.description ? <span className={styles.description}>{item.description}</span> : null}
{item.meta ? <span className={styles.meta}>{item.meta}</span> : null}
<UiIcon iconRole="chevron-right" size={16} strokeWidth={1.75} className={styles.chevron} aria-hidden="true" />
</ArcLink>
</li>
))}
</ul>
);
}
RelatedList.displayName = "RelatedList";
export default RelatedList;