Contents
페이지의 섹션으로 가는 링크를 모은 "이 페이지의 내용" 목록입니다. Contents.Root는 목록을 담은 nav 랜드마크이고, 각 Contents.Item은 섹션 제목을 프래그먼트로 가리키는 Contents.Link를 담으며, Contents.List는 한 제목 아래의 섹션을 중첩합니다. 페이지를 스크롤하는 동안 지금 읽고 있는 섹션의 링크가 aria-current="location"을 받습니다.
이 페이지의 내용
<Contents.Root label="On this page">
<Contents.Item>
<Contents.Link href="#install">Install</Contents.Link>
</Contents.Item>
<Contents.Item>
<Contents.Link href="#tokens">Tokens</Contents.Link>
<Contents.List>
<Contents.Item>
<Contents.Link href="#colors">Colors</Contents.Link>
</Contents.Item>
</Contents.List>
</Contents.Item>
</Contents.Root>
Install
Add the package and its stylesheet, then wrap the app in its providers.
Tokens
Build your token package on top of the base set, and keep your overrides in your own layer.
Colors
Point the appearance tokens at your palette. Every component follows.
Spacing
Density tokens set the gaps, so a compact screen needs no new CSS.
Ship
Publish the package with a changeset.
Nav처럼 링크는 직접 넣습니다. Todae는 페이지의 제목을 읽지 않으므로, 목록에는 여러분이 고른 내용이 들어가고 서버에서 렌더링할 수 있습니다. 각 제목에 링크의 href가 가리키는 id를 붙이세요. 다른 페이지로 가는 링크는 현재 항목으로 표시되지 않습니다.
읽고 있는 섹션
섹션은 제목이 자기 기준선을 지나 스크롤되면 현재 섹션이 됩니다. 기준선은 프래그먼트 링크가 제목을 스크롤해 두는 자리로, 스크롤 컨테이너의 scroll-padding-block-start에 제목 자신의 scroll-margin-block-start를 더한 위치입니다. 고정된 헤더를 피하려고 둘 중 하나를 설정하면, 목록은 링크를 눌렀을 때 스크롤되는 그 기준선에서 섹션을 읽은 것으로 칩니다. 현재 섹션은 제목이 기준선을 지난 섹션 중 마지막 섹션이고, 첫 제목보다 위에서는 어떤 링크도 현재 항목이 아닙니다.
Contents.Root는 첫 제목을 감싼 컨테이너 중 실제로 스크롤되는 가장 가까운 것, 또는 페이지에서 IntersectionObserver로 제목을 따라갑니다. 스크롤 컨테이너, 첫 제목의 부모, 창의 크기가 바뀌면 기준선을 다시 재고, 링크보다 늦게 마운트되거나, 교체되거나, id가 바뀐 제목도 따라갑니다.
짧은 마지막 섹션은 페이지가 그만큼 스크롤되지 않아 기준선에 닿지 못할 수 있습니다. 그래서 링크를 누르거나, 목록이 마운트될 때나 해시가 바뀔 때 해시가 화면에 보이는 섹션을 가리키면 그 섹션이 선택됩니다. 해시가 바뀌어도 선택된 섹션을 가리키면 선택이 유지되므로, 링크를 누른 뒤 부드러운 스크롤이 아직 진행 중이어도 선택이 남습니다. 선택은 사용자가 휠, 터치, 또는 입력 필드가 받지 않고 위젯이 멈추거나 취소하지 않은 스크롤 키(버튼에서 Space는 버튼을 누릅니다)로 스크롤하거나, 그 섹션이 화면에 들어왔다가 벗어나거나, 해시가 바뀌어 다른 것을 가리킬 때까지 현재 항목으로 남습니다.
Chromium은 native CSS scrollspy를 progressive enhancement로 써서 현재 링크를 칠합니다. 그 링크가 스크립트의 링크가 아니면 스크립트의 링크가 표시를 유지하므로, 현재로 보이는 링크는 하나입니다. 스크립트는 계속 aria-current="location"을 설정합니다. Firefox와 Safari가 이 기능을 지원할 때까지 그 속성이 기준입니다.
모양
링크는 contents.rule.color와 contents.rule.width로 그린 하나의 선 옆에 놓이고, 현재 링크는 그 선 위에 contents.marker.color와 contents.marker.width로 표시를 그리며, 색은 contents.link.fg.current, 굵기는 contents.link.font.current-weight입니다. 중첩된 목록은 단계마다 링크의 텍스트를 contents.indent만큼 들여 쓰므로 모든 표시가 같은 선 위에 놓입니다. 강제 색상 모드에서 표시는 Highlight입니다.
Contents.Link는 라우터 링크를 위해 Link처럼 render를 받습니다. 위 예시는 이를 써서 자기 상자만 스크롤합니다.
좁은 화면에서
Contents에는 자체 접기 기능이 없습니다. 페이지 옆에 열을 둘 자리가 없으면, 이 사이트처럼 목록을 페이지 제목 아래의 Disclosure에 넣거나 빼세요.
<Disclosure.Root>
<Disclosure.Summary>On this page</Disclosure.Summary>
<Contents.Root aria-label="On this page">…</Contents.Root>
</Disclosure.Root>
접근성을 지키는 조합
Todae가 맡는 것: label, aria-label, aria-labelledby 또는 StringsProvider의 contents 문자열로 이름이 붙는 nav 랜드마크. 각 목록은 ul role="list"입니다. 읽고 있는 섹션의 링크에 aria-current="location"을 붙여, 색으로 그린 표시만이 아니라 스크린 리더도 어느 섹션인지 말해 줍니다. 포커스 링은 링크 안쪽에 그려지므로, 양옆을 자르는 스크롤 컨테이너 안에서도 온전히 보입니다.
직접 제공할 것: 링크마다 id가 있는 제목과, 제목과 같은 링크 텍스트. 섹션이 고정된 헤더 아래에 가려지지 않도록 헤더 높이를 scroll-padding-block-start나 scroll-margin-block-start로 지정하세요.
주의할 점: 옆 열과 접힌 복사본이 동시에 보이는 것처럼 같은 이름의 Contents가 한 페이지에 둘 있는 경우. 쓰지 않는 쪽은 display: none으로 숨기세요. 제목이 아닌 요소로 가는 링크는 스크린 리더 사용자가 제목 탐색으로 닿을 수 없습니다.
언제 쓰나요
언제: 가이드, 문서 페이지, 설정 페이지처럼 섹션이 여럿인 긴 페이지에서 독자가 섹션 사이를 오가며 지금 위치를 알고 싶을 때.
쓰지 않을 때: 페이지 사이 이동 → Nav. 현재 페이지 위의 경로 → Breadcrumbs. 한 번에 하나만 보이는 패널 → Tabs. 짧은 섹션이 두세 개뿐이라 목록이 필요 없는 페이지.
Props
| 이름 | 타입 | 설명 |
|---|---|---|
label | React.ReactNode | 목록 위에 보이는 텍스트로, 랜드마크의 이름도 됩니다. |
aria-label | string | label이나 aria-labelledby가 없을 때의 랜드마크 이름. 기본값은 StringsProvider의 contents 문자열입니다. |
children | React.ReactNode | Contents.Item 요소. |
| 이름 | 타입 | 설명 |
|---|---|---|
children | React.ReactNode | Contents.Item 요소. |
| 이름 | 타입 | 설명 |
|---|---|---|
children | React.ReactNode | Contents.Link, 또는 Contents.Link와 그 뒤에 오는 하위 섹션의 Contents.List. |
| 이름 | 타입 | 설명 |
|---|---|---|
href | string | 섹션의 프래그먼트. 예: #props. 다른 페이지로 가는 링크는 현재 항목으로 표시되지 않습니다. |
render | (props: React.ComponentPropsWithRef<"a">) => React.ReactElement | 라우터 링크처럼 일반 a가 아닌 것을 렌더링합니다. 클래스 이름을 포함한 모든 prop을 받으며, 이를 a에 전달해야 합니다. |
aria-current | "page"| "step"| "location"| "date"| "time"| "true"| "false" | 읽고 있는 섹션의 링크에 Contents.Root가 location으로 설정합니다. 직접 전달한 값이 우선합니다. |
children | React.ReactNode | 링크 텍스트로, 보통 섹션의 제목입니다. |