
CSS only 탭 UI, 실제로 쓸 수 있을까
JavaScript 없이도 탭 UI를 구현할 수 있다는 말, 처음 들었을 때 저도 반신반의했습니다. 그런데 직접 세 가지 방식을 다 구현해보고 나서 생각이 바뀌었습니다. 쓸 수 있는 상황이 분명히 있고, 쓰면 안 되는 상황도 분명히 있습니다. 문제는 "CSS only로 모든 걸 해결할 수 있다"는 식의 글들이 그 경계를 잘 알려주지 않는다는 점입니다. 저는 그 경계를 직접 부딪히며 알게 됐습니다.
radio input으로 탭 만들기
CSS only 탭의 가장 대표적인 방법은 radio input과 label을 조합하는 방식입니다. 원리 자체는 단순합니다. radio input에는 같은 name을 가진 버튼 중 하나만 선택할 수 있다는 특성이 있고, 선택된 상태를 CSS의 :checked 가상 클래스로 감지할 수 있습니다. 그 상태에서 형제 선택자(~)를 이용해 해당 input 다음에 오는 콘텐츠 패널만 보이게 만드는 구조입니다.
말로 쓰면 단순해 보이는데, 실제로 구현하면서 꽤 오래 헤맸던 부분이 있습니다. HTML 구조 순서였습니다. input 요소들이 반드시 콘텐츠 패널보다 앞에 있어야 형제 선택자가 동작하는데, 처음에 저는 콘텐츠를 먼저 배치했다가 CSS가 아무런 반응을 하지 않아서 한참을 디버깅했습니다. 원인을 알고 나면 황당하지만, 이 순서 규칙을 명확하게 짚어주는 글이 의외로 없었습니다. 대부분 완성된 구조만 보여주고 왜 그 순서여야 하는지는 설명하지 않았습니다.
이 방식에서 가장 실질적인 단점은 탭이 늘어날수록 CSS 선택자를 손으로 늘려야 한다는 겁니다. 탭이 3개면 3개 케이스를 직접 작성해야 하고, 나중에 탭을 추가할 때마다 CSS 파일을 열어야 합니다. 탭 구성이 자주 바뀌는 프로젝트라면 이 구조는 금방 짐이 됩니다. 처음에는 깔끔해 보여도 유지보수 단계에서 한계가 드러납니다.
:target 선택자와 URL 해시
두 번째 방식은 anchor 링크와 :target 선택자를 활용합니다. URL의 해시(#) 값과 일치하는 id를 가진 요소가 :target 상태가 되고, CSS로 그 요소만 표시하는 구조입니다. 예를 들어 URL이 #tab2로 바뀌면 id가 tab2인 패널만 보이게 됩니다.
이 방식의 분명한 장점은 딥링크가 자동으로 지원된다는 점입니다. 특정 탭이 열린 상태의 URL을 그대로 공유하면, 받는 사람도 같은 탭이 열린 화면을 볼 수 있습니다. radio input 방식으로는 구현하기 어려운 기능입니다.
그런데 제가 예상 밖으로 까다롭다고 느꼈던 건 초기 로드 처리였습니다. 처음 페이지에 들어올 때 URL에 아무 해시도 없으면 :target이 어디에도 적용되지 않아서 탭이 하나도 안 보이는 상황이 생깁니다. 저는 첫 번째 패널에 기본 display를 설정해두고, :target이 다른 패널로 지정됐을 때만 첫 번째 패널을 숨기는 방식으로 해결했습니다. 논리가 약간 거꾸로인데, 이 구조를 처음 보는 사람에게 설명하기가 생각보다 어렵습니다. 코드 자체보다 의도를 이해시키는 데 시간이 더 걸렸습니다.
치명적인 제약은 SPA(Single Page Application) 환경입니다. 클라이언트 사이드 라우팅을 쓰는 프로젝트에서는 해시 변경이 라우터와 충돌할 수 있어서, 그런 환경에서는 이 방식을 사용하는 것 자체가 리스크입니다. 프레임워크 없이 정적 HTML 페이지를 만드는 경우라면 고려해볼 수 있지만, React나 Vue를 쓰는 프로젝트라면 처음부터 배제하는 편이 낫습니다.
details 태그, 아코디언형 탭의 가능성
세 번째 방식은 HTML5의 details 태그를 활용합니다. 클릭하면 내용이 펼쳐지고 다시 클릭하면 접히는 인터랙션을 브라우저가 기본으로 처리해주는 요소인데, 여러 개의 details 요소에 같은 name 속성을 지정하면 하나가 열릴 때 나머지가 자동으로 닫히는 아코디언 구조를 만들 수 있습니다.
이 방식의 실질적인 강점은 접근성입니다. 스크린 리더 지원이 브라우저 레벨에서 기본으로 처리되기 때문에, radio input + label 조합처럼 WAI-ARIA 속성을 별도로 달지 않아도 됩니다. 접근성을 신경 써야 하는 프로젝트에서 이 차이는 생각보다 큽니다. 속성 하나 빠뜨리면 스크린 리더 사용자가 탭 구조를 전혀 인식하지 못할 수 있는데, details 태그는 그 위험 자체가 없습니다.
다만 한 가지 주의할 점이 있습니다. details 태그의 name 속성을 통한 그룹화는 비교적 최근에 표준화된 기능이라 구형 브라우저에서는 지원되지 않습니다. 프로젝트의 브라우저 지원 범위를 먼저 확인하지 않고 도입했다가는 일부 환경에서 아코디언이 아니라 모든 패널이 독립적으로 열리는 상황을 만날 수 있습니다. MDN에서 브라우저 호환 테이블을 먼저 확인하는 습관이 필요합니다.
CSS only는 '완전한 대안'이 아니다
CSS only 탭을 JavaScript의 완전한 대체제로 소개하는 글들이 꽤 있습니다. 저도 처음에는 그렇게 믿고 적극적으로 도입했는데, 실제로 프로젝트에 적용하면서 그 표현이 과하다는 걸 알게 됐습니다.
가장 명확한 한계는 전환 효과입니다. CSS transition으로 기본적인 페이드 효과는 만들 수 있지만, 탭 전환 시 높이가 자연스럽게 변하거나 슬라이드 방향에 따라 애니메이션이 달라지는 섬세한 인터랙션은 결국 JavaScript가 필요합니다. CSS only라는 조건을 지키기 위해 사용자 경험을 희생하는 건 방향이 틀린 겁니다. 기술 선택의 기준은 기술 자체가 아니라 사용자여야 합니다.
저는 지금도 JavaScript 의존도를 줄일 수 있는 환경이라면 CSS 방식을 먼저 검토합니다. 하지만 탭 수가 많거나, 세밀한 애니메이션이 필요하거나, SPA 환경이거나, 팀원이 CSS 선택자 구조를 낯설어한다면 처음부터 JavaScript로 짓는 쪽을 선택합니다. 어떤 방식이 더 낫냐는 질문의 답은 언제나 프로젝트 맥락에 달려 있습니다. 세 가지 방식을 직접 구현해보고 각각의 한계를 체감해두면, 나중에 상황에 맞는 판단을 훨씬 빠르게 내릴 수 있습니다.
radio input + 형제 선택자 방식은 탭 모양 구현에 적합하지만 HTML 구조 순서를 반드시 지켜야 하고 탭 수가 늘면 CSS 관리가 부담스러워집니다. :target 선택자 방식은 딥링크 지원이 강점이지만 초기 로드 처리가 까다롭고 SPA 환경에서는 사실상 쓰기 어렵습니다. details 태그는 접근성 면에서 가장 안전하지만 탭 형태보다는 수직 아코디언 레이아웃에 더 자연스럽고, 브라우저 지원 범위를 반드시 확인해야 합니다.
참고:
MDN Web Docs — CSS :checked selector
Google Search Central — JavaScript SEO 가이드
CSS-Tricks — Tabs (no JavaScript)