본문 바로가기

Components · 내비게이션

Tab (탭)

같은 계층의 콘텐츠 묶음 중 하나를 골라 같은 자리에서 전환한다. 제안서·상세페이지 섹션 전환에 적합하다.

정의

같은 계층의 여러 콘텐츠 묶음 중 하나만 골라 같은 자리에서 전환해 보여 주는 컴포넌트다.

Anatomy

규격 소재 시공사례 활성 탭의 패널 내용이 이 자리에 표시됩니다. ① 탭 목록(tablist) ③ 활성 탭 + 인디케이터 ② 비활성 탭 ④ 탭 패널
부위토큰
① 탭 목록하단 보더 --tab-border · 탭 사이 간격 --tab-gap
② 비활성 탭fg --tab-fg · 패딩 --tab-pad-y/--tab-pad-x · --text-label
③ 활성 탭fg --tab-fg-active · 하단 인디케이터 --tab-indicator 두께 --tab-indicator-size
④ 탭 패널본문 --text-body · 상단 여백 --space-6

변형

변형용도
수평 탭(기본)같은 화면의 3~6개 섹션 전환 — 제안서·상세페이지 섹션

size·수직·중첩 변형은 제공하지 않는다 — 콘텐츠가 계층이면 Accordion을 쓴다.

상태

상태비활성 탭활성 탭
Defaultfg --tab-fgfg --tab-fg-active + 인디케이터 --tab-indicator
Hoverfg --tab-fg-active
Focusbox-shadow: var(--focus-ring)동일
Selectedaria-selected="false"aria-selected="true"(인디케이터 표시)
Disabledopacity: var(--opacity-disabled) + cursor: not-allowed

Do / Don't

Do
  • 탭은 3~6개로 유지한다 — 항목이 많으면 묶거나 사이드 메뉴를 쓴다.
  • 탭 라벨은 1~2단어 명사로, 순서를 바꿔도 뜻이 통하게 병렬로 짓는다.
  • 활성 탭은 색 + 인디케이터(밑줄) 둘 다로 구분한다.
Don't
  • 탭 안에 또 탭을 넣지 말 것(중첩 금지) — 계층은 Accordion으로.
  • 순서·인과가 있는 흐름에 탭을 쓰지 말 것 — 그건 Step Indicator다.
  • 탭 전환에 페이지 새로고침을 걸지 말 것 — 같은 화면에서 콘텐츠만 바꾼다.

콘텐츠 규칙

  • 탭 라벨은 1~2단어 명사구(≤10자) — 문장·물음표 금지.
  • 라벨은 병렬 구조로 통일한다("규격 / 소재 / 시공사례"처럼 같은 결).
  • 탭 개수는 3~6개 권장, 2개면 탭 대신 세그먼트·토글을 검토한다.

키보드 인터랙션

동작
Tab탭 목록으로 진입 → 다시 Tab이면 활성 패널로 이동(탭 사이 이동은 방향키)
/ 이전/다음 탭으로 이동하며 즉시 선택(자동 활성화)
Home / End첫 번째 / 마지막 탭으로 이동
Enter / Space포커스된 탭 선택(수동 활성화 방식일 때)

접근성

  • role="tablist" 컨테이너 + 각 탭 role="tab"·aria-selected·aria-controls(패널 id) + 패널 role="tabpanel"·aria-labelledby(탭 id).
  • 로빙 tabindex: 활성 탭만 tabindex="0", 나머지는 tabindex="-1"Tab 한 번에 탭 목록을 지나가고 방향키로 탭을 옮긴다(아래 JS).
  • 활성 표시는 색만이 아니라 인디케이터(밑줄)로도 — 색맹·저시력에 대비한다.
  • 포커스 링 제거 금지, 각 탭 히트영역 ≥ --target-min. 기준: ../guidelines/accessibility.md.

코드

표준 규격 600×600mm, 친환경 그래픽 노면표시재로 제작합니다.

html
<div class="pi-tabs">
  <div class="pi-tabs__list" role="tablist" aria-label="안전표지 상세">
    <button class="pi-tabs__tab" role="tab" id="tab-spec" aria-selected="true" aria-controls="panel-spec" tabindex="0">규격</button>
    <button class="pi-tabs__tab" role="tab" id="tab-material" aria-selected="false" aria-controls="panel-material" tabindex="-1">소재</button>
    <button class="pi-tabs__tab" role="tab" id="tab-case" aria-selected="false" aria-controls="panel-case" tabindex="-1">시공사례</button>
  </div>
  <div class="pi-tabs__panel" role="tabpanel" id="panel-spec" aria-labelledby="tab-spec">
    <p>표준 규격 600×600mm, 친환경 그래픽 노면표시재로 제작합니다.</p>
  </div>
  <div class="pi-tabs__panel" role="tabpanel" id="panel-material" aria-labelledby="tab-material" hidden>
    <p>GREENGUARD GOLD(UL 2818) 인증 라텍스 잉크를 사용합니다.</p>
  </div>
  <div class="pi-tabs__panel" role="tabpanel" id="panel-case" aria-labelledby="tab-case" hidden>
    <p>어린이보호구역 노면표시·안전표지 시공사례를 담았습니다.</p>
  </div>
</div>
css
.pi-tabs__list { display: flex; gap: var(--tab-gap); border-bottom: var(--border-thin) solid var(--tab-border); }
.pi-tabs__tab {
  padding: var(--tab-pad-y) var(--tab-pad-x);
  border: none; background: none; cursor: pointer;
  color: var(--tab-fg);
  font: var(--text-label);
  letter-spacing: var(--text-label-letter-spacing);
  border-bottom: var(--tab-indicator-size) solid transparent; margin-bottom: calc(var(--border-thin) * -1);
  min-height: var(--target-min);
  word-break: keep-all;
  transition: color var(--duration-base) var(--easing-out), border-color var(--duration-base) var(--easing-out);
}
.pi-tabs__tab:hover { color: var(--tab-fg-active); }
.pi-tabs__tab:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }
.pi-tabs__tab[aria-selected="true"] { color: var(--tab-fg-active); border-bottom-color: var(--tab-indicator); }
.pi-tabs__tab:disabled { opacity: var(--opacity-disabled); cursor: not-allowed; }
.pi-tabs__panel { padding-top: var(--space-6); color: var(--color-text-secondary); font: var(--text-body); word-break: keep-all; }
.pi-tabs__panel[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .pi-tabs__tab { transition: none; } }
js
document.querySelectorAll(".pi-tabs").forEach((tabs) => {
  const tablist = tabs.querySelector('[role="tablist"]');
  const tabsArr = [...tablist.querySelectorAll('[role="tab"]')];
  const select = (tab) => {
    tabsArr.forEach((t) => {
      const on = t === tab;
      t.setAttribute("aria-selected", on ? "true" : "false");
      t.tabIndex = on ? 0 : -1;
      document.getElementById(t.getAttribute("aria-controls")).hidden = !on;
    });
  };
  tablist.addEventListener("click", (e) => {
    const tab = e.target.closest('[role="tab"]');
    if (tab) select(tab);
  });
  tablist.addEventListener("keydown", (e) => {
    const i = tabsArr.indexOf(document.activeElement);
    const moves = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabsArr.length - 1 };
    if (i < 0 || !(e.key in moves)) return;
    e.preventDefault();
    const n = (moves[e.key] + tabsArr.length) % tabsArr.length;
    select(tabsArr[n]);
    tabsArr[n].focus();
  });
});

Changelog

날짜변경
2026-07-15문서 계약(10섹션) 적용 — Anatomy 도해·상태 매트릭스·콘텐츠 규칙·방향키/로빙 tabindex 실코드 신설. KRDS 벤치마킹 규격 승계