Groo Design

Pagination

여러 쪽으로 나뉜 목록의 쪽을 오가는 줄 — 쪽 단위로 훑는 목록(주로 데스크톱 768 이상) 아래 가운데에 둔다. 몇 쪽인지 한눈에 보이고, 쪽이 바뀌어도 칸 자리가 흔들리지 않는다. 폰(768 미만)의 긴 목록은 쪽을 나누지 않고 끝없이 불러온다(아래 "폰은 끝없이 불러오기"). 데이터 표는 Table Pagination 이다.

구조는 당근 SEED Pagination(Apache-2.0)을 따른다 — "여러 페이지로 나뉜 콘텐츠를 탐색할 수 있도록 돕는 네비게이션 컴포넌트" 로, 이전 · 다음 버튼 · 쪽 번호 · 생략. 번호 · 화살표 칸 40 × 40 을 사이 없이 잇고, 지금 쪽은 짙게 채우며, 칸 수는 화면 폭으로 고정이다(480 이상 9칸 · 미만 7칸). 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-10-04 사용자 결정). 옛 Pagination 스펙(shadcn — 지금 쪽 테두리 · 칸 사이 4 · 이전 · 다음 글자)을 대신한다.

수치 원본은 pagination.yaml(넘김 줄)과 infinite-list.yaml(폰의 끝없이 불러오기 — 목록 끝 자리)이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.

라이트
카드 혜택(데스크톱) — 5 / 12쪽, 9칸
첫 쪽 — 이전 자리는 빈 칸
좁은 화면(480 미만) — 7칸
다크
카드 혜택(데스크톱) — 5 / 12쪽, 9칸
첫 쪽 — 이전 자리는 빈 칸
좁은 화면(480 미만) — 7칸
카드 혜택(데스크톱) 5 / 12쪽 · 첫 쪽 · 좁은 화면 7칸 — 라이트 · 다크

직접 골라 보기

전체 쪽 수 · 지금 쪽 · 화면 폭(480 이상 · 미만)을 고르면 스펙대로 그린 줄과 그 코드가 바뀐다. 실제로 누르면 칸 자리는 그대로이고 생략(…)만 옮겨 간다. 첫 · 마지막 쪽에서는 화살표 자리가 비고, 키보드로 끝 쪽에 닿으면 초점이 지금 쪽 번호로 옮겨 간다.

삼성카드 탭탭배달 · 쇼핑 20% 할인
하나카드 원큐해외 결제 수수료 면제
우리카드 D4구독 · OTT 15% 할인
읽는 글—
칸 자리는 그대로이고 생략(…)만 옮겨 간다 — 9칸(화살표 포함). 첫 · 마지막 쪽에서는 화살표 자리가 빈 칸이고, 키보드로 끝 쪽에 닿으면 초점이 지금 쪽 번호로 옮겨 간다.
전체 쪽 수
화면 폭
지금 쪽
칸
모드
import { useSearchParams } from "react-router-dom"
import { Pagination } from "@/components/ui/pagination"

const [params] = useSearchParams()
const page = Number(params.get("page") ?? 1)

<section ref={listRef} aria-labelledby="card-list-title">…</section>
{/* 칸은 링크 — 쪽마다 방문 기록이 쌓여 뒤로 가기가 앞 쪽으로 온다 */}
<Pagination totalPages={12} page={page} getHref={(p) => `?page=${p}`} scrollTarget={listRef} aria-label="카드 혜택 페이지 탐색" />

Anatomy

ⓐ Root — 칸 40 을 사이 없이ⓑ Previous · Nextⓒ Page(지금 쪽 채움)ⓓ Ellipsis
넘김 줄은 이전 · 쪽 번호 · 생략 · 다음으로, 칸 40 이 붙어 있고 지금 쪽은 짙게 채운다
부위설명
ⓐ Root넘김 줄 — 목록 아래 가운데. 칸을 사이 없이 잇는다.
ⓑ Previous · Next이전 · 다음 — 아이콘만 40 × 40. 첫 · 마지막 쪽에서는 그 자리가 빈 칸이다.
ⓒ Page쪽 번호 — 40 × 40, 14 / 19 · 700. 지금 쪽은 짙은 채움.
ⓓ Ellipsis생략 — 40 상자 + 점 셋. 누르지 않는다.

부위

부위설명
root넘김 줄 — 목록 아래 가운데. 칸을 사이 없이 잇는다
item쪽 번호 칸 — 40 × 40
label쪽 번호 — 숫자 폭을 같게
arrow이전 · 다음 — 아이콘만 40 × 40
ellipsis생략 — 40 상자 + 점 셋. 누르지 않는다
empty빈 칸 — 첫 · 마지막 쪽에서 화살표 자리 40
focusRing키보드 포커스 링(웹)

Properties

칸

번호 · 화살표 · 생략 칸은 모두 40 × 40 이고 사이 없이 붙는다. 번호는 14 / 19 · 700 · fg-neutral · 숫자 폭을 같게, 화살표는 아이콘만(chevron 16), 모서리는 8 이다. 바탕은 누를 때 · 마우스를 올릴 때만 보인다.

쪽 번호

속성값비고
크기40px40 × 40 — 칸 40 그대로(사용자 결정 2026-10-04)
모서리radius-r2 8px
배경transparent누를 때 · 마우스를 올릴 때만 칠한다(지금 쪽은 채움)
터치 영역40 × 44위아래만 44(보이지 않는 여백 2 씩), 옆은 칸 폭 40 — v106 '누르는 영역 44' 의 예외. AA ✓ · AAA ⚠
커서pointer
전환 대상background-color, scale바탕 색과 누름 축소 둘 다 — 다른 누르는 칸(card · chart …)과 같은 표기, SEED pagination-button 도 colorDuration · scaleDuration 둘(Q-011-6). 시간은 motion 절
전환 시간motion-duration-color-transition 150ms
전환 곡선motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1)

번호 글자

속성값비고
글자text-t4 14px / 19px
굵기700
글자·아이콘color-fg-neutral #1A1F2E · 다크 #F5F6FA
숫자 폭tabular-nums숫자 폭을 같게. 네 자리 이상이 칸을 넘으면 말줄임 — 숫자는 칸 이름에 있다

이전 · 다음

속성값비고
크기40px이전 · 다음 — 아이콘만. 누르는 영역은 쪽 번호와 같다(40 × 44)
모서리radius-r2 8px
아이콘 크기16pxlucide chevron-left · chevron-right
색color-fg-neutral #1A1F2E · 다크 #F5F6FA
배경transparent
전환 대상background-color, scale쪽 번호 칸과 같다(Q-011-6)
전환 시간motion-duration-color-transition 150ms
전환 곡선motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1)

넘김 줄

속성값비고
간격0px칸이 붙는다(SEED) — 9칸이면 360, 7칸이면 280
가로 맞춤center목록 아래 가운데(SEED)
위 바깥 여백spacing-x6 24px목록 끝 → 넘김 줄 24(옛 DESIGN.md Pagination 절의 값 — SEED 는 "목록 하단" 만 적었다)

지금 쪽

지금 쪽은 짙은 채움(bg-neutral-inverted) + 반전 글자(fg-neutral-inverted)다 — 다크에서는 밝은 칸에 짙은 글자다. 브랜드 색으로 칠하지 않는다. 보조 기술에는 aria-current="page" 로 알린다.

라이트 — 지금 쪽 짙은 채움 · 반전 글자
다크 — 지금 쪽 짙은 채움 · 반전 글자
지금 쪽 — 짙은 채움, 라이트 · 다크

지금 쪽

currentitem 배경label 글자·아이콘설명
other (기본)——다른 쪽
currentcolor-bg-neutral-inverted #1A1F2E · 다크 #F5F6FA — 짙은 채움 — SEED bg.neutral-solid 의 groo 짝(다크는 밝은 반전)color-fg-neutral-inverted #FFFFFF · 다크 #242938지금 쪽 — 짙은 채움

칸 수 — 9 · 7

칸 수는 화면 폭으로 고정이다 — 480 이상 9칸, 480 미만 7칸(화살표 둘 포함). 쪽이 바뀌어도 줄의 폭과 칸 자리는 그대로이고 생략(…)만 옮겨 간다. 전체 쪽이 칸 수보다 적으면 모든 번호를 보인다.

지금 쪽9칸(480 이상)7칸(480 미만)
앞쪽‹ 1 2 3 4 5 … N ›‹ 1 2 3 4 … ›
가운데‹ 1 … p−1 p p+1 … N ›‹ … p−1 p p+1 … ›
뒤쪽‹ 1 … N−4 N−3 N−2 N−1 N ›‹ … N−3 N−2 N−1 N ›

7칸의 가운데 구간에서는 첫 · 마지막 번호가 없다(SEED) — 앞뒤 쪽을 바로 고르는 것을 먼저 둔다.

9칸 — 480 이상
앞쪽 3 / 12
가운데 6 / 12
뒤쪽 10 / 12
7칸 — 480 미만
앞쪽 2 / 12
가운데 6 / 12
뒤쪽 11 / 12
9칸 · 7칸 — 앞쪽 · 가운데 · 뒤쪽, 칸 자리는 그대로

칸 수

count너비설명
regular (기본)360px — 9칸 — 화살표 2 + 번호 · 생략 7. 1 2 3 4 5 … N · 1 … p−1 p p+1 … N · 1 … N−4 N−3 N−2 N−1 N9칸 — 480 이상(화살표 둘 포함)
narrow280px — 7칸 — 화살표 2 + 번호 · 생략 5. 1 2 3 4 … · … p−1 p p+1 … · … N−3 N−2 N−1 N(가운데 구간에서는 첫 · 마지막 번호가 없다)7칸 — 480 미만(화살표 둘 포함)

끝 쪽 — 빈 칸

첫 쪽에서는 이전 자리에, 마지막 쪽에서는 다음 자리에 40 빈 칸을 둔다 — 막힌 화살표를 그리지 않고 자리만 지킨다(SEED). 키보드로 "다음" 을 눌러 마지막 쪽에 닿으면 누르던 버튼이 빈 칸이 되므로, 초점을 지금 쪽 번호로 옮긴다(초점이 화면 맨 앞으로 떨어지지 않게).

첫 쪽 — 이전 자리를 비운다(막힌 화살표를 그리지 않는다)
마지막 쪽 — 키보드로 “다음” 을 눌러 닿으면 그 자리가 빈 칸이 되어 초점을 지금 쪽(12)으로
첫 쪽 — 이전 자리 빈 칸 · 마지막 쪽 — 초점이 지금 쪽으로

누르는 영역 — 40 의 예외

칸은 40 이다 — 누르는 영역은 위아래만 44 로 넓히고(보이지 않는 여백 2 씩), 옆은 칸 폭 40 그대로다. 칸이 붙어 있어 옆으로 넓히면 이웃 칸과 겹친다. 이것은 v106 "누르는 영역은 늘 44 이상" 의 예외다 — groo 폰은 끝없이 불러오기라 넘김 줄은 주로 마우스로 누른다(사용자 결정 2026-10-04). WCAG 2.5.8(AA, 24) 은 통과하고 2.5.5(AAA, 44) 는 옆으로 미달이다.

칸 40 × 40 — 누르는 영역은 위아래만 44(보이지 않는 여백 2 씩), 옆은 칸 폭 40 그대로. v106 “누르는 영역 44” 의 예외(AA ✓ · AAA ⚠)
누르는 영역 — 40 × 44(분홍), 칸끼리 붙어 옆으로는 넓히지 않는다

State

상태모습
enabled바탕 없음(지금 쪽은 짙은 채움)
hovered웹 — bg-layer-default-pressed(지금 쪽은 bg-neutral-inverted-pressed), 축소 없음
pressed같은 바탕 + 2px 거리 축소
focused웹 — 키보드 포커스에만 칸 바깥 2px 링
disabled목록을 다시 받는 동안 — 번호 · 화살표 fg-disabled, 지금 쪽은 bg-disabled
라이트기본호버누름포커스막힘다른 쪽지금 쪽화살표
다크기본호버누름포커스막힘다른 쪽지금 쪽화살표
상태 — 호버 · 누름 · 포커스 · 막힘 × 다른 쪽 · 지금 쪽

상태 — 다른 쪽

상태item 배경item 배율item 커서item 축소 시간item 축소 곡선
enabled 기본transparent — 누를 때 · 마우스를 올릴 때만 칠한다(지금 쪽은 채움)—pointer——
hovered 웹 — 누름과 같은 바탕, 축소 없음color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D — 누름과 같은 색 — 축소 없음(v106)—pointer——
pressed 누름 — 바탕 + 2px 거리 축소color-bg-layer-default-pressed #F7F8FD · 다크 #353B4D — SEED bg.transparent-pressed 의 불투명 짝2px 거리 축소 — 기준 40 → 0.95(v104 — SEED 0.97). 모션 줄이기면 하지 않는다pointermotion-duration-pressed-scale 150msmotion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1)
disabled 막힌 넘김(목록을 다시 받는 동안 · 막힌 화면) — 전용 색transparent—not-allowed——

상태 — 지금 쪽

상태item 배경item 배율item 커서item 축소 시간item 축소 곡선
enabled 기본color-bg-neutral-inverted #1A1F2E · 다크 #F5F6FA — 짙은 채움 — SEED bg.neutral-solid 의 groo 짝(다크는 밝은 반전)—pointer——
hovered 웹 — 누름과 같은 바탕, 축소 없음color-bg-neutral-inverted-pressed #535866 · 다크 #B7BDCC—pointer——
pressed 누름 — 바탕 + 2px 거리 축소color-bg-neutral-inverted-pressed #535866 · 다크 #B7BDCC — v112 — Button neutralSolid 의 누름과 같다2px 거리 축소 — 기준 40 → 0.95(v104 — SEED 0.97). 모션 줄이기면 하지 않는다pointermotion-duration-pressed-scale 150msmotion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1)
disabled 막힌 넘김(목록을 다시 받는 동안 · 막힌 화면) — 전용 색color-bg-disabled #F5F6FA · 다크 #353B4D — 흐리게 하지 않는다(v106)—not-allowed——

모션

전환시간곡선대상비고
바탕 색motion-duration-color-transition 150msmotion-ease-easing cubic-bezier(0.35, 0, 0.35, 1)background-color
누름motion-duration-pressed-scale 150msmotion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1)scale2px 거리 축소 — 모션 줄이기면 하지 않는다

Guidelines

언제 쪽을 나누나

동작 상황으로 고른다 — "늘 데스크톱은 Pagination" 이 아니다(2026-10-23 사용자 결정 — 가계부 다시 짜기 https://claude.ai/artifact/CCAra9mCf1q77pqgM1D2xc 5A). 사용자 말: "데스크톱처럼 쪽 단위로 훑는 자리는 Pagination 이 좋을 수 있지만, 스크롤로 내려가는 환경(폰 · 긴 목록)은 끝에 닿으면 저절로 더 불러오는 쪽이 동작상 자연스럽다. 가계부처럼 달이 자연스러운 묶음인 목록은 달 이동이 쪽 넘김이다."

이런 목록쓰는 것
쪽 단위로 훑는 자리 — 데스크톱(검색 결과 · 카드 혜택 격자)Pagination 이 좋을 수 있다 — 몇 쪽인지 · 어디쯤인지 보이고 주소로 돌아온다
스크롤로 내려가는 환경 — 폰 · 긴 목록끝없이 불러오기 — 아래
자연스러운 묶음이 있는 목록(가계부 거래 — 달)묶음 옮기기가 쪽 넘김 — 달 하나가 한 쪽이고, 넘김 줄을 따로 두지 않는다
데이터 표(직원 · 휴가 내역 · 회비)Table Pagination — 줄 수 · 범위
전체가 화면 3 ~ 4개 분량 이하쪽을 나누지 않고 한 번에(SEED)
단계를 거치는 흐름(가져오기 · 처음 쓰기)쪽 넘김이 아니라 단계 버튼

1쪽 이하면 넘김 줄을 그리지 않는다. 같은 목록이 폰에서는 끝없이 불러오고 데스크톱에서는 쪽을 나눌 수 있다(지금 카드 혜택과 같다). 이 표는 고르는 참고다 — 화면마다 동작으로 고른다. 화면 틀 쪽 표는 DESIGN.md Layout "화면 틀" 의 "목록 넘기기".

desk.groo.app
그루 담다
워크스페이스
기록
거래 검색
카드 혜택
삼성카드 탭탭배달 · 쇼핑 20% 할인
하나카드 원큐해외 결제 수수료 면제
우리카드 D4구독 · OTT 15% 할인
롯데카드 로카간편결제 1.5% 할인
NH농협 올바른병원 · 약국 5% 할인
BC카드 바로영화 · 공연 3,000원 할인
신한카드 Deep카페 · 편의점 10% 할인
KB국민 노리대중교통 · 통신 7% 할인
이렇게 — 데스크톱의 긴 목록(카드 혜택 격자) — 목록 아래 가운데 넘김 줄
9:41
카드 혜택
신한카드 Deep카페 · 편의점 10% 할인
KB국민 노리대중교통 · 통신 7% 할인
현대카드 M주유 · 마트 M포인트 적립
삼성카드 탭탭배달 · 쇼핑 20% 할인
하나카드 원큐해외 결제 수수료 면제
우리카드 D4구독 · OTT 15% 할인
롯데카드 로카간편결제 1.5% 할인
NH농협 올바른병원 · 약국 5% 할인
이렇게 — 폰의 긴 목록 — 쪽을 나누지 않고 끝없이 불러온다
사용자
사용자
이름
부서
김민수
개발팀
이서연
디자인팀
박지훈
개발팀
최유진
인사팀
이렇게 — 데이터 표 — Table Pagination(줄 수 · 범위 · 이전 · 다음)
9:41
카드 혜택
신한카드 Deep카페 · 편의점 10% 할인
KB국민 노리대중교통 · 통신 7% 할인
현대카드 M주유 · 마트 M포인트 적립
삼성카드 탭탭배달 · 쇼핑 20% 할인
하나카드 원큐해외 결제 수수료 면제
이렇게 하지 않는다 — 폰에 쪽 넘김 · “더 보기” 버튼 — 폰은 끝없이 불러오기
쓰는 자리 — 데스크톱 목록 넘김 · 폰 끝없이 · 표는 Table Pagination

쪽을 넘기면

쪽을 넘기면 목록의 위 끝이 화면 위에 오게 스크롤하고, 보조 기술에 "2페이지, 전체 12페이지" 를 한 번 알린다(숨은 상태 글). 초점은 누른 칸에 남는다 — 끝 쪽에 닿아 그 칸이 빈 칸이 되면 지금 쪽 번호로 옮긴다. 넘기는 동안 목록은 Skeleton 의 "다른 내용을 받을 때" 를 따른다(바뀔 줄만 기다리고, 옛 쪽의 줄을 남기지 않는다).

desk.groo.app
그루 담다
워크스페이스
기록
거래 검색
카드 혜택
신한카드 Deep카페 · 편의점 10% 할인
KB국민 노리대중교통 · 통신 7% 할인
현대카드 M주유 · 마트 M포인트 적립
삼성카드 탭탭배달 · 쇼핑 20% 할인
하나카드 원큐해외 결제 수수료 면제
우리카드 D4구독 · OTT 15% 할인
1쪽 끝에서 다음(›)을 누름
desk.groo.app
그루 담다
워크스페이스
기록
거래 검색
카드 혜택
삼성카드 탭탭배달 · 쇼핑 20% 할인
하나카드 원큐해외 결제 수수료 면제
우리카드 D4구독 · OTT 15% 할인
롯데카드 로카간편결제 1.5% 할인
NH농협 올바른병원 · 약국 5% 할인
BC카드 바로영화 · 공연 3,000원 할인
2쪽 — 목록의 위 끝이 화면 위에, 초점은 누른 칸(›)에 남는다
2페이지, 전체 12페이지
다음을 누름 — 목록 맨 위 · "2페이지" 알림 · 초점은 누른 칸

쪽은 주소에 — 칸은 링크

웹 화면의 목록은 쪽을 주소에 둔다(?page=3) — 칸이 링크(<a>)라 새 탭에서 열고 주소를 나눌 수 있고, 뒤로 가기가 앞 쪽으로 돌아온다(쪽마다 방문 기록이 쌓인다). 주소가 없는 자리(대화상자 · 시트 안 목록)만 칸이 버튼이다. 어느 쪽이든 모양 · 키보드 · 이름은 같다.

폰은 끝없이 불러오기

768 미만의 긴 목록은 쪽을 나누지 않는다 — 목록 끝이 화면 아래에서 300 안으로 오면 다음 쪽을 받아 아래에 잇는다. 목록 맨 아래 한 자리가 지금 상태를 보인다(infinite-list.yaml).

상태목록 끝 자리
받는 중Progress Circle 24 가운데 — 1초가 지나야 보인다(시간표는 Skeleton 의 "기다리는 동안")
못 불러옴"더 불러오지 못했어요." + Button neutralWeak small "다시 시도" — 이미 받은 줄은 그대로
끝"모두 봤어요."(목록마다 "카드를 모두 봤어요." 처럼) — 한 쪽으로 끝나는 짧은 목록에는 두지 않는다

폰 목록은 둘로 가른다(2026-10-22 사용자 결정 6B — https://claude.ai/artifact/NuEPiLRH2fJidXdKzcDsCX) — 목록 화면의 긴 목록은 끝없이 불러온다(위). 카드 · 띠 안의 몇 줄 미리보기는 머리의 "전체 보기" 로 목록 화면에 보낸다(Card 의 머리 동작). 목록 끝에 줄을 덧붙이는 "더 보기" 버튼은 두지 않는다 — 끝없이 불러오기와 두 벌이 된다. 데스크톱은 쪽 넘김이다. 첫 쪽을 못 불러오면 목록 자리가 Result Section 의 실패다. 끝없이 불러오는 목록이 길어져도 맨 위로 가는 길은 상단 바 · 탭 바(지금 탭 다시 누르기)가 맡는다.

9:41
카드 혜택
삼성카드 탭탭배달 · 쇼핑 20% 할인
하나카드 원큐해외 결제 수수료 면제
우리카드 D4구독 · OTT 15% 할인
롯데카드 로카간편결제 1.5% 할인
NH농협 올바른병원 · 약국 5% 할인
BC카드 바로영화 · 공연 3,000원 할인
받는 중 — 원 24, 1초가 지나야 보인다
9:41
카드 혜택
삼성카드 탭탭배달 · 쇼핑 20% 할인
하나카드 원큐해외 결제 수수료 면제
우리카드 D4구독 · OTT 15% 할인
롯데카드 로카간편결제 1.5% 할인
NH농협 올바른병원 · 약국 5% 할인
BC카드 바로영화 · 공연 3,000원 할인
더 불러오지 못했어요.
못 불러옴 — 받은 줄은 그대로 + 다시 시도
9:41
카드 혜택
삼성카드 탭탭배달 · 쇼핑 20% 할인
하나카드 원큐해외 결제 수수료 면제
우리카드 D4구독 · OTT 15% 할인
롯데카드 로카간편결제 1.5% 할인
NH농협 올바른병원 · 약국 5% 할인
BC카드 바로영화 · 공연 3,000원 할인
카드를 모두 봤어요.
끝 — 목록마다 맞춘 끝 글
목록 끝 — 받는 중 원 24 · 못 불러옴 + 다시 시도 · 끝 "모두 봤어요."

목록 끝 자리

statusprogress 크기message 글자·아이콘message 글자message 굵기retryButton 버튼 크기retryButton 위 바깥 여백설명
loading (기본)24px — Progress Circle 24 · neutral — skeleton.md 의 '목록 끝 더 불러오기'. 1초가 지나야 보인다(그 전에 오면 깜빡이지 않는다)—————다음 쪽을 받는 중 — 원
error—color-fg-neutral-muted #535866 · 다크 #B7BDCC — "더 불러오지 못했어요." — 이미 받은 줄은 그대로 둔다. 문장으로 된 상태 문구라 muted(2026-10-16 사용자 결정 글자 역할 D5 — 그대로 둔다)text-t4 14px / 400 / 19px400small 36 — Button neutralWeak small "다시 시도" — 누르면 그 쪽을 다시 받는다(로딩을 건다)spacing-x3 12px못 불러옴 — "더 불러오지 못했어요." + 다시 시도
end—color-fg-neutral-subtle #62697A · 다크 #A2A8B7 — "모두 봤어요."(목록마다 "카드를 모두 봤어요." 처럼 바꿀 수 있다) — 바탕 위 5.50 · 다크 6.09. 한 쪽만으로 끝나는 짧은 목록에는 두지 않는다text-t4 14px / 400 / 19px400——끝 — "모두 봤어요."

목록 끝 자리 — 공통

부위속성값비고
root상하 여백spacing-x6 24px목록 마지막 줄 아래 24 · 그 아래는 화면 바닥 여백(탭 바가 있으면 그 여백)
root가로 맞춤center
sentinel문턱300px목록 끝이 화면 아래 끝에서 300 안으로 오면 다음 쪽을 받는다(지금 Desk 앱 값). 받는 중이거나 끝이면 부르지 않는다

글 · 이름

넘김 줄의 이름은 "페이지 탐색"(목록이 둘 이상이면 "카드 혜택 페이지 탐색" 처럼), 칸 이름은 "N페이지", 화살표는 "이전 페이지" · "다음 페이지" 다(SEED 문구). 영어("pagination" · "More pages" · "Page x of y")를 쓰지 않는다.

코드

레시피 recipes/shadcn/components/ui/pagination.tsx 를 쓴다. 아래 미리보기는 스펙 값으로 그린 모습이다.

  • Pagination — 넘김 줄(<nav aria-label="페이지 탐색">). totalPages(1 이하면 그리지 않는다) · page · defaultPage(1) · onPageChange(page, { reason })(reason — "page-item" · "previous" · "next") · getHref(page)(주면 칸이 링크 — 쪽이 주소에 있을 때) · scrollTarget(목록 상자 — 쪽이 바뀌면 그 위 끝으로 스크롤) · disabled · aria-label. 칸 수는 breakpoint-sm(480)으로 9 · 7 을 스스로 고른다. 쪽이 바뀌면 숨은 상태 글로 "N페이지, 전체 M페이지" 를 알리고, 끝 쪽에서 초점을 지금 쪽 번호로 옮긴다.
  • paginationItems(page, totalPages, slots) — 칸 배열({ type: "page" | "ellipsis" | "previous" | "next" | "empty", page? })을 돌려주는 규칙 함수. 앱도 같은 규칙으로 쓴다.
  • InfiniteListEnd — 폰 목록 끝 자리. status("loading" · "error" · "end") · onRetry · endText(기본 "모두 봤어요.") · onReachEnd(목록 끝이 300 안으로 오면 한 번 부른다 — status 가 "loading" 이면 "다음 쪽이 있다" 는 뜻이다. 목록이 자라거나 300 밖으로 나갔다 다시 오면 또 부를 수 있고, "error" · "end" 이거나 "다시 시도" 를 누른 직후에는 부르지 않는다).

데스크톱 목록 — 쪽은 주소에

카드 혜택 — 5 / 12쪽
import { useSearchParams } from "react-router-dom"
import { Pagination } from "@/components/ui/pagination"

const [params] = useSearchParams()
const page = Number(params.get("page") ?? 1)

<section ref={listRef} aria-labelledby="card-list-title">…</section>
{/* 칸은 링크 — 쪽마다 방문 기록이 쌓여 뒤로 가기가 앞 쪽으로 온다 */}
<Pagination totalPages={totalPages} page={page} getHref={(p) => `?page=${p}`} scrollTarget={listRef} aria-label="카드 혜택 페이지 탐색" />

폰 목록 — 끝없이 불러오기

하나카드 원큐해외 결제 수수료 면제
우리카드 D4구독 · OTT 15% 할인
NH농협 올바른병원 · 약국 5% 할인
BC카드 바로영화 · 공연 3,000원 할인
카드를 모두 봤어요.
폰 카드 혜택 — 목록 끝의 원 · 끝 글
import { InfiniteListEnd } from "@/components/ui/pagination"

<ul>{cards.map((c) => <CardRow key={c.id} card={c} />)}</ul>
<InfiniteListEnd
  status={isError ? "error" : hasNextPage ? "loading" : "end"}
  onReachEnd={fetchNextPage}
  onRetry={fetchNextPage}
  endText="카드를 모두 봤어요."
/>

Behavior

인터랙션동작
번호 누르기그 쪽으로 — 목록 위 끝으로 스크롤, "N페이지, 전체 M페이지" 를 알린다. 칸 자리는 그대로, 생략만 옮겨 간다
이전 · 다음 누르기앞 · 뒤 쪽으로. 끝 쪽에 닿으면 그 칸이 빈 칸이 되고 초점은 지금 쪽 번호로
Tab이전 → 번호들 → 다음. 생략 · 빈 칸은 건너뛴다. Enter(링크) · Enter · Space(버튼)로 넘긴다
마우스 호버누름과 같은 바탕 — 축소 없음
창 폭이 480 을 넘나듦9칸 ↔ 7칸
목록을 받는 동안넘김 줄을 막는다(disabled) — 두 번 넘기지 않게
폰 — 목록 끝이 300 안으로다음 쪽을 받는다 — 목록 끝에 원, 못 받으면 "다시 시도", 끝이면 끝 글

Accessibility

기준검증
WCAG 1.4.3 Color contrast(글자 ≥ 4.5:1)번호 fg-neutral 흰 면 위 16.41 · 다크 13.42 · 회색 페이지 위 15.20 · 15.20, 지금 쪽 fg-neutral-inverted on bg-neutral-inverted 16.41 · 13.42(누름 7.11 · 7.70) ✓. 끝 글 fg-neutral-subtle 5.50 · 6.09 ✓
WCAG 1.4.11 Non-text contrast(≥ 3:1)화살표 fg-neutral 16.41 · 13.42 ✓, 지금 쪽 칸 면 16.41 · 13.42 ✓. 키보드 포커스 링 흰 면 위 Desk 8.38 · 6.10 · HR 8.17 · 6.07 · 회색 페이지 위 7.76 · 6.90 · 7.56 · 6.88 ✓. 호버 바탕은 장식(회색 페이지 위에서는 거의 안 보인다 — 축소가 함께 알린다)
WCAG 1.4.1 Use of color지금 쪽은 채움 면(16.41)으로 알리고 aria-current="page" ✓
WCAG 2.4.3 Focus order끝 쪽에서 누르던 화살표가 빈 칸이 되면 초점을 지금 쪽 번호로 — body 로 떨어지지 않는다 ✓
WCAG 4.1.3 Status messages쪽이 바뀌면 "N페이지, 전체 M페이지" 를 role="status" 로 ✓
WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24)칸 40 × 44 ✓
WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44)⚠ 칸 40 — 위아래만 44, 옆은 40(칸이 붙어 넓히지 않는다). v106 "누르는 영역 44" 의 예외로 둔다(사용자 결정 2026-10-04 — 폰은 끝없이 불러오기). 폰 목록 끝의 "다시 시도" 는 44 ✓
ARIA<nav aria-label="페이지 탐색">, 칸은 링크(쪽이 주소에 있을 때) 또는 <button type="button"> — 이름 "N페이지", 지금 쪽 aria-current="page", 화살표 "이전 페이지" · "다음 페이지". 생략 · 빈 칸은 aria-hidden. 끝없이 불러오기 — 받는 동안 목록에 aria-busy, 끝 글은 글로 읽힌다

Do / Don't

✅ Do

  • 데스크톱 목록 아래 가운데 — 칸 40 을 붙이고 지금 쪽은 짙게 채운다.
  • 칸 수를 화면 폭으로 고정(9 · 7) — 끝 쪽은 빈 칸으로 자리를 지킨다.
  • 쪽을 넘기면 목록 맨 위 + "N페이지" 알림.
  • 쪽은 주소에, 칸은 링크.
  • 폰은 끝없이 불러오고, 끝 글 · 다시 시도 · 받는 중을 보인다.
  • 달처럼 자연스러운 묶음이 있는 목록은 묶음 옮기기를 쪽 넘김으로(가계부 거래 — 달 이동).

❌ Don't

  • 지금 쪽을 테두리만 · 브랜드 색으로.
  • 칸 사이를 띄우거나 이전 · 다음에 글자.
  • 끝 쪽에서 막힌 화살표 · 초점을 잃는 화살표.
  • 화면 3 ~ 4개 분량 이하의 목록을 쪽으로 나누기 · 1쪽뿐인 넘김 줄.
  • 폰에 쪽 넘김 · 목록 끝에 줄을 덧붙이는 "더 보기" 버튼(카드 · 띠 미리보기는 머리 "전체 보기").
  • 표 아래에 이 넘김 줄(표는 Table Pagination).

Specification

pagination.yaml · infinite-list.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Pagination 과 목록 끝 자리를 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 뒤의 규칙이 앞의 같은 값을 덮는다. 상태는 enabled 에서 바뀌는 값만 적었다.

Base

부위상태속성값
rootenabled위 바깥 여백marginTopspacing-x624px목록 끝 → 넘김 줄 24(옛 DESIGN.md Pagination 절의 값 — SEED 는 "목록 하단" 만 적었다)
간격gap0px칸이 붙는다(SEED) — 9칸이면 360, 7칸이면 280
가로 맞춤justifyContentcenter목록 아래 가운데(SEED)
itemenabled크기size40px40 × 40 — 칸 40 그대로(사용자 결정 2026-10-04)
모서리radiusradius-r28px
배경backgroundtransparent누를 때 · 마우스를 올릴 때만 칠한다(지금 쪽은 채움)
커서cursorpointer
터치 영역touchTarget40 × 44위아래만 44(보이지 않는 여백 2 씩), 옆은 칸 폭 40 — v106 '누르는 영역 44' 의 예외. AA ✓ · AAA ⚠
전환 대상transitionPropertybackground-color, scale바탕 색과 누름 축소 둘 다 — 다른 누르는 칸(card · chart …)과 같은 표기, SEED pagination-button 도 colorDuration · scaleDuration 둘(Q-011-6). 시간은 motion 절
전환 시간transitionDurationmotion-duration-color-transition150ms
전환 곡선transitionEasingmotion-ease-easingcubic-bezier(0.35, 0, 0.35, 1)
labelenabled글자·아이콘foregroundfg-neutral#1A1F2E다크 #F5F6FA
글자typographytext-t414px / 19px
굵기fontWeight700
숫자 폭numeralstabular-nums숫자 폭을 같게. 네 자리 이상이 칸을 넘으면 말줄임 — 숫자는 칸 이름에 있다
arrowenabled크기size40px이전 · 다음 — 아이콘만. 누르는 영역은 쪽 번호와 같다(40 × 44)
아이콘 크기iconSize16pxlucide chevron-left · chevron-right
모서리radiusradius-r28px
배경backgroundtransparent
색colorfg-neutral#1A1F2E다크 #F5F6FA
전환 대상transitionPropertybackground-color, scale쪽 번호 칸과 같다(Q-011-6)
전환 시간transitionDurationmotion-duration-color-transition150ms
전환 곡선transitionEasingmotion-ease-easingcubic-bezier(0.35, 0, 0.35, 1)
ellipsisenabled크기size40px
아이콘 크기iconSize16pxlucide ellipsis — 보조 기술에 숨긴다, 누르지 않는다
색colorfg-neutral#1A1F2E다크 #F5F6FA
emptyenabled크기size40px첫 쪽의 이전 · 마지막 쪽의 다음 자리 — 비워 둔다(보조 기술에 숨긴다)
itemhovered배경backgroundbg-layer-default-pressed#F7F8FD다크 #353B4D누름과 같은 색 — 축소 없음(v106)
arrowhovered배경backgroundbg-layer-default-pressed#F7F8FD다크 #353B4D
itempressed배경backgroundbg-layer-default-pressed#F7F8FD다크 #353B4DSEED bg.transparent-pressed 의 불투명 짝
배율scale2px 거리 축소기준 40 → 0.95(v104 — SEED 0.97). 모션 줄이기면 하지 않는다
축소 시간scaleDurationmotion-duration-pressed-scale150ms
축소 곡선scaleEasingmotion-ease-pressed-scalecubic-bezier(0, 0, 0.15, 1)
arrowpressed배경backgroundbg-layer-default-pressed#F7F8FD다크 #353B4D
배율scale2px 거리 축소
focusRingfocused링 두께width2px
링 색colorstroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84
링 간격offset2px칸 바깥 — 붙은 이웃 칸 위에 그린다(SEED)
itemdisabled배경backgroundtransparent
커서cursornot-allowed
labeldisabled글자·아이콘foregroundfg-disabled#8A91A0다크 #838997
arrowdisabled색colorfg-disabled#8A91A0다크 #838997
커서cursornot-allowed

current=current

부위상태속성값
itemenabled배경backgroundbg-neutral-inverted#1A1F2E다크 #F5F6FA짙은 채움 — SEED bg.neutral-solid 의 groo 짝(다크는 밝은 반전)
labelenabled글자·아이콘foregroundfg-neutral-inverted#FFFFFF다크 #242938
itemhovered배경backgroundbg-neutral-inverted-pressed#535866다크 #B7BDCC
itempressed배경backgroundbg-neutral-inverted-pressed#535866다크 #B7BDCCv112 — Button neutralSolid 의 누름과 같다
itemdisabled배경backgroundbg-disabled#F5F6FA다크 #353B4D흐리게 하지 않는다(v106)
labeldisabled글자·아이콘foregroundfg-disabled#8A91A0다크 #838997

count=regular

부위상태속성값
rootenabled너비width360px9칸 — 화살표 2 + 번호 · 생략 7. 1 2 3 4 5 … N · 1 … p−1 p p+1 … N · 1 … N−4 N−3 N−2 N−1 N

count=narrow

부위상태속성값
rootenabled너비width280px7칸 — 화살표 2 + 번호 · 생략 5. 1 2 3 4 … · … p−1 p p+1 … · … N−3 N−2 N−1 N(가운데 구간에서는 첫 · 마지막 번호가 없다)

Base

부위상태속성값
rootenabled상하 여백paddingYspacing-x624px목록 마지막 줄 아래 24 · 그 아래는 화면 바닥 여백(탭 바가 있으면 그 여백)
가로 맞춤justifyContentcenter
sentinelenabled문턱threshold300px목록 끝이 화면 아래 끝에서 300 안으로 오면 다음 쪽을 받는다(지금 Desk 앱 값). 받는 중이거나 끝이면 부르지 않는다

status=loading

부위상태속성값
progressenabled크기size24pxProgress Circle 24 · neutral — skeleton.md 의 '목록 끝 더 불러오기'. 1초가 지나야 보인다(그 전에 오면 깜빡이지 않는다)

status=error

부위상태속성값
messageenabled글자·아이콘foregroundfg-neutral-muted#535866다크 #B7BDCC"더 불러오지 못했어요." — 이미 받은 줄은 그대로 둔다. 문장으로 된 상태 문구라 muted(2026-10-16 사용자 결정 글자 역할 D5 — 그대로 둔다)
글자typographytext-t414px / 19px / 400
굵기fontWeight400
retryButtonenabled버튼 크기buttonSizesmall 36Button neutralWeak small "다시 시도" — 누르면 그 쪽을 다시 받는다(로딩을 건다)
위 바깥 여백marginTopspacing-x312px

status=end

부위상태속성값
messageenabled글자·아이콘foregroundfg-neutral-subtle#62697A다크 #A2A8B7"모두 봤어요."(목록마다 "카드를 모두 봤어요." 처럼 바꿀 수 있다) — 바탕 위 5.50 · 다크 6.09. 한 쪽만으로 끝나는 짧은 목록에는 두지 않는다
글자typographytext-t414px / 19px / 400
굵기fontWeight400

SEED 와 다른 점

  • 지금 쪽은 bg-neutral-inverted — SEED bg.neutral-solid(#2A3038)의 groo 짝이다(다크는 밝은 반전). 누름은 bg-neutral-inverted-pressed(v112).
  • 누름 · 호버 바탕은 불투명한 bg-layer-default-pressed · 누름 축소는 2px 거리(40 이면 0.95) — SEED 는 투명한 bg.transparent-pressed · scale 0.97 이다(v102 · v104).
  • 누르는 영역은 세로만 44 — SEED 는 40 그대로다. 옆은 칸이 붙어 넓히지 않는다(v106 의 예외 — 사용자 결정).
  • 끝 쪽에서 초점을 지금 쪽 번호로 — SEED 는 누르던 화살표가 빈 칸이 되어 초점이 body 로 떨어진다.
  • 쪽이 주소에 있으면 칸은 링크 — SEED 디자인 문서는 "Page Link" 라 부르지만 코드는 늘 버튼이다.
  • 쪽이 바뀌면 넘김 줄이 알린다 — SEED 는 "결과 안내는 목록 영역에서" 로 남겼다.
  • 폰은 끝없이 불러오기 — SEED 에는 끝없이 불러오기 · "더 보기" 의 규칙이 없다. groo 가 정했다(받는 시점 300 · 끝 글 · 다시 시도 — 2026-10-04 화면 틀 비교 페이지의 따라오는 것). 미리보기는 머리 "전체 보기", 줄을 덧붙이는 "더 보기" 는 두지 않는다(2026-10-22 사용자 결정 6B).

Migration notes

2026-10-23 — 쪽 넘김은 상황으로

"언제 쪽을 나누나" 의 "데스크톱의 긴 목록 → Pagination · 폰의 긴 목록 → 끝없이 불러오기" 를 동작 상황 표로 바꿨다 — 쪽 단위로 훑는 자리는 Pagination 이 좋을 수 있고, 스크롤로 내려가는 환경(폰 · 긴 목록)은 끝없이 불러오기, 달처럼 자연스러운 묶음이 있는 목록은 묶음 옮기기가 쪽 넘김이다(가계부 거래 — 달 하나 = 한 쪽). DESIGN 세 파일 Layout "화면 틀" 의 문턱 표 "긴 목록" 줄도 "상황에 따라" 로 바꾸고 "목록 넘기기" 표를 더했다. 사용자 결정 — 가계부 다시 짜기 https://claude.ai/artifact/CCAra9mCf1q77pqgM1D2xc 5A(고르지 않은 안 5B). 값 변경 없음.

2026-10-22 — 목록 화면 · 미리보기 둘로

""더 보기" 버튼은 두지 않는다" 는 근거 없이 들어간 문장이라 카드 머리 "전체 보기" 까지 막는 것처럼 읽혔다. 목록 화면의 긴 목록(끝없이 불러오기) · 카드 · 띠 안의 미리보기(머리 "전체 보기")로 가르고, 목록 끝에 줄을 덧붙이는 "더 보기" 만 두지 않는다고 적었다(사용자 결정 6B — https://claude.ai/artifact/NuEPiLRH2fJidXdKzcDsCX). 지금 동작은 그대로다.

2026-10-15 — 구현 세션 질문 정리(Q-011-6)

사용자가 비교 페이지에서 정리 항목을 "추천대로" 정했다(2026-10-15). 쪽 번호 칸 · 화살표 칸의 transitionProperty 를 background-color, scale 로 적었다 — 누를 때 2px 축소가 있으므로 다른 누르는 칸(카드 · 차트 타일)과 같은 표기이고, SEED pagination-button 도 색과 축소 둘을 전환한다. 화살표 칸에는 빠져 있던 전환 시간 · 이징도 쪽 번호 칸과 같게 적었다. 보이는 모습은 바뀌지 않는다 — 웹 · 레시피는 이미 둘 다 전환한다.

2026-10-04 — SEED Pagination 으로 다시 쓴다

사용자가 화면 틀 · 이동 비교 페이지에서 정했다 — SEED Pagination(10A: 번호 · 화살표 40 × 40 · 사이 0 · 14 / 19 · 700 · 모서리 8 · 지금 쪽 bg-neutral-inverted + fg-neutral-inverted 채움 · 이전 · 다음은 아이콘만 16 · 9칸 / 7칸 · 끝 쪽은 화살표 대신 40 빈 칸 + 초점은 지금 쪽 · 1쪽 이하면 그리지 않음) + 데이터 표는 Table Pagination. 같은 날 추가로 — 칸 40 그대로, 누르는 영역 44 규칙(v106)의 예외로 적는다(위아래만 44). 그리고 "따라오는 것" — 폰 긴 목록은 끝없이 불러오기 + 끝 글 · 실패 다시 하기 · 불러오는 동안 표시, 데스크톱은 Pagination, 쪽을 넘기면 목록 맨 위 + 쪽 바뀜 알림. groo 옛 스펙(10B — 지금 쪽 테두리 · 칸 사이 4 · 이전 · 다음 글자 · 끝 막힘 · 좁으면 "이전 · 현재/총 · 다음")과 44 로 키우기는 고르지 않았다. 옛 스펙은 pagination.history/v-pre-seed-nav.*.

제품은 앱 적용 단계에서 옮긴다(2026-10-04 조사 — Desk 웹 · HR 은 크로미움에 띄워 쟀다). 세 제품 모두 숫자 칸 · aria-current · 쪽 바뀜 알림이 0 이다.

  • Desk 웹 카드 혜택(데스크톱) — ghost sm 57.9 × 32 "이전 / 다음" + 화살표 14 · 12 / 500 + 가운데 "1 / 3", 끝에서 불투명도 .5(pages/card-benefit/ui/CardBenefitPage.tsx:676-810). "다음" 이 window.scrollTo 를 불러 실제로 스크롤하는 셸 상자가 움직이지 않아, 바닥에서 넘기면 2쪽 첫 카드가 −4564 에 있다(:780 · 804 ↔ widgets/layout/ui/AppLayout.tsx:122, F4). 넘김 줄 + 목록 맨 위로.
  • Desk 웹 · 앱 카드 혜택(폰) — 끝없이 불러오기는 있다(웹 IntersectionObserver · 앱 끝 300 앞 — card_benefits_screen.dart:121-153 · 300-345). 작은 스피너뿐이고 aria-live · 끝 글이 없으며, 앱은 첫 쪽 뒤의 실패를 화면에 보이지 않는다. 목록 끝 자리(받는 중 · 못 불러옴 + 다시 시도 · 끝 글)로.
  • HR 표 셋 — Pagination 이 아니라 Table Pagination 으로 옮긴다(그 스펙의 Migration notes).
  • 옛 스펙의 예시("HR 결재 목록" · "Desk 메모 보관함 142쪽")는 제품에 없다(S2).

이 페이지