이 글에서 정리하는 내용
Tailwind CSS v4 기준으로, variant를 정리합니다. 버튼, 입력창, 탭, 아코디언처럼 상태가 자주 바뀌는 UI에서 어떤 variant를 먼저 써야 하는지 판단 기준을 잡는 데 초점을 둡니다.
- 상태 variant는 클래스 암기가 아니라 조건 분리 문제입니다
- hover, focus-visible, disabled는 브라우저가 판단하는 상태입니다
- aria variant는 의미가 있는 UI 상태에 사용합니다
- data variant는 컴포넌트 내부 상태를 다룰 때 자주 씁니다
- 상태 variant를 조합할 때 확인할 기준
- 정리: 상태를 누가 알고 있는지부터 확인하기
상태 variant는 클래스 암기가 아니라 조건 분리 문제입니다

Tailwind CSS에서 상태 스타일을 처음 다룰 때는 같은 문법이 먼저 눈에 들어옵니다. 그래서 variant를 “앞에 붙이는 접두사” 정도로 외우기 쉽습니다. 버튼 한두 개를 만들 때는 이 방식으로도 크게 막히지 않습니다. 문제는 버튼, 탭, 입력창, 드롭다운처럼 상태가 여러 개 붙는 UI를 만들 때부터 생깁니다.
예를 들어 저장 버튼 하나에도 기본 배경, 마우스를 올렸을 때의 배경, 키보드로 접근했을 때의 outline, 비활성 상태의 투명도, 클릭할 수 없다는 커서 표현이 같이 들어갑니다. 이때 모든 클래스를 같은 성격의 장식으로 보면 나중에 와 가 충돌하거나, 키보드 포커스 표시가 빠지거나, 접근성 상태와 화면 상태가 따로 움직이는 코드가 되기 쉽습니다.
먼저 나눠야 할 기준은 단순합니다. 이 상태를 브라우저가 이미 알고 있는지, 접근성 속성으로 의미를 드러내야 하는지, 아니면 컴포넌트 로직이 따로 관리하는지 확인합니다. Tailwind의 variant는 이 조건을 utility 앞에 붙여서 “이 상황에서만 이 스타일을 적용한다”라고 적는 방식입니다.
<button type="button" class="rounded-lg bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-900 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:text-slate-500"
> 저장하기
</button> 이 코드는 버튼 스타일을 한 줄로 몰아넣은 것처럼 보이지만, 실제로는 상태가 나뉘어 있습니다. 은 기본 상태은 포인터를 올렸을 때은 키보드 탐색 등 포커스 표시가 필요한 상황는 실제 속성이 적용된 상황을 담당합니다.
여기서 중요한 부분은 가 “비활성처럼 보이게 하는 클래스”만은 아니라는 점입니다. 실제 버튼이 비활성 상태가 되려면 HTML에 속성이 있어야 합니다. Tailwind class는 그 상태를 시각적으로 표현할 뿐이고, 상태 자체를 만드는 역할은 마크업이나 로직이 담당합니다.
hover, disabled는 브라우저가 판단하는 상태입니다
는 브라우저가 비교적 직접 판단할 수 있는 상태입니다. 마우스가 올라왔는지, 요소가 포커스를 받았는지, 폼 컨트롤이 비활성화되었는지 같은 조건입니다. 그래서 버튼, 링크, input 같은 기본 인터랙션 요소에서는 이 variant를 먼저 검토하게 됩니다.
는 시각적 반응을 주기에 좋지만, 핵심 정보를 hover에만 의존하면 모바일이나 터치 환경에서 흐름이 흔들릴 수 있습니다. 예를 들어 상품 카드에서 hover했을 때만 가격, 삭제 버튼, 상세 버튼이 보인다면 터치 기기에서는 조작 기준이 불분명해질 수 있습니다. hover는 필수 상태보다 보조 반응에 두는 것이 낫습니다.
은 와 구분해서 봐야 합니다. 는 현재 포커스를 받은 요소에 항상 반응합니다. 반면 은 브라우저가 포커스 표시가 필요하다고 판단한 상황에 맞춰 동작합니다. 버튼을 마우스로 클릭할 때마다 outline이 튀어나오는 것이 어색하다면 을 쓰는 쪽이 UI 흐름에 더 잘 맞습니다. 특히 키보드 탭 이동을 고려해야 하는 관리자 화면, 폼 화면, 결제 화면에서는 이 차이가 바로 드러납니다.
<input type="email" class="w-full rounded-md border border-slate-300 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-500" placeholder="email@example.com"
/> 입력창에서는 만 고집할 필요는 없습니다. 텍스트 입력창은 사용자가 값을 입력하는 동안 포커스 위치를 계속 확인해야 하므로 스타일도 자연스럽게 쓰입니다. 반대로 버튼이나 링크처럼 클릭 후 포커스 링이 과하게 보이는 요소에서는 을 먼저 고려하면 됩니다. 기준은 “포커스가 보이는 것이 사용자에게 지금 필요한 정보인가”입니다.
는 더 조심해서 봐야 합니다. native 속성이 붙은 버튼이나 input은 포커스를 받을 수 없고, 폼 제출에도 참여하지 않습니다. 그래서 “흐리게 보이게만 하고 클릭은 막지 않는 상태”를 만들고 싶다면 가 아니라 다른 상태 관리가 필요합니다. 반대로 실제로 제출할 수 없는 버튼이라면 class만 바꾸지 말고 속성도 함께 내려와야 합니다.
<button type="submit" disabled class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:text-slate-500 disabled:hover:bg-slate-300"
> 결제하기
</button> 마지막의 은 비활성 버튼에 hover 색상이 남는 느낌을 줄이고 싶을 때 사용할 수 있는 방식입니다. 다만 이런 보정 class가 여러 곳에서 반복된다면 버튼의 기본 설계부터 다시 볼 필요가 있습니다. “활성 버튼에만 hover가 있어야 한다”는 기준이 프로젝트에 있다면, 공통 Button 컴포넌트나 class 조립 함수에서 이 규칙을 한 번에 처리하는 쪽이 관리하기 쉽습니다.
aria variant는 의미가 있는 UI 상태에 사용합니다
variant는 화면의 상태를 접근성 속성과 함께 표현할 때 사용합니다. 탭 메뉴의 선택 상태, 아코디언 버튼의 열림 상태, 토글 버튼의 눌림 상태처럼 화면에 보이는 정보가 보조기술에도 전달되어야 하는 경우가 여기에 들어갑니다.
예를 들어 탭 UI에서 현재 선택된 탭을 굵게 보이게 하고 싶다고 해서 만 붙일 수도 있습니다. 하지만 이 탭이 실제 탭 역할을 하고 있고, 선택된 탭이라는 의미가 필요하다면 를 함께 고려해야 합니다. Tailwind에서는 를 사용해 이 상태를 바로 스타일 조건으로 연결할 수 있습니다.
<div role="tablist" class="flex gap-2 border-b border-slate-200"> <button type="button" role="tab" aria-selected="true" class="border-b-2 border-transparent px-3 py-2 text-sm text-slate-500 aria-selected:border-blue-600 aria-selected:text-blue-600" > 전체 </button> <button type="button" role="tab" aria-selected="false" class="border-b-2 border-transparent px-3 py-2 text-sm text-slate-500 aria-selected:border-blue-600 aria-selected:text-blue-600" > 진행중 </button>
</div> 이 예시에서 스타일 기준은 별도의 class가 아니라 입니다. 선택 상태를 화면에만 숨겨두지 않고 마크업의 의미로도 드러내기 때문에, 탭 구조를 나중에 수정할 때 상태 기준이 비교적 선명합니다.
다만 를 단순 스타일용 attribute처럼 쓰면 안 됩니다. 를 붙였는데 실제 패널은 닫혀 있다면 화면과 의미가 어긋납니다.같은 속성은 “보이는 모양”이 아니라 “현재 UI의 의미 상태”를 나타내야 합니다.
<button type="button" aria-expanded="true" class="group flex w-full items-center justify-between rounded-lg border border-slate-200 px-4 py-3 text-left text-sm font-medium aria-expanded:bg-slate-50"
> 배송 정보 <span class="transition group-aria-expanded:rotate-180">⌄</span>
</button> 상태가 붙은 요소와 실제로 바꾸고 싶은 요소가 다르면 를 떠올리면 됩니다. 버튼 자체의 배경을 바꾸는 것은 로 충분하지만, 버튼 안의 아이콘을 회전시키려면 부모 상태를 자식이 참조해야 합니다.
이 구조는 아코디언이나 드롭다운 버튼에서 자주 보입니다. 열림 상태는 버튼이 알고 있고, 회전해야 하는 요소는 버튼 안의 아이콘입니다. 상태의 위치와 스타일이 적용될 위치가 다르면같은 관계형 variant까지 같이 검토해야 합니다.
는 와 다르게 봐야 합니다. 속성은 지원되는 form control에서 실제 비활성 동작까지 연결됩니다. 반면 는 “비활성 상태로 인식되어야 한다”는 의미를 전달하지만, 클릭 방지나 키보드 동작 차단을 자동으로 해주지는 않습니다. 커스텀 버튼이나 메뉴 항목처럼 native 를 쓸 수 없는 구조에서는 가 필요할 수 있지만, 동작 제한은 JavaScript나 컴포넌트 로직에서 별도로 처리해야 합니다.
data variant는 컴포넌트 내부 상태를 다룰 때 자주 씁니다
variant는 접근성 의미보다 컴포넌트가 관리하는 상태를 스타일에 연결할 때 자주 보입니다. 예를 들어같은 attribute는 UI 라이브러리나 직접 만든 컴포넌트에서 내려오는 경우가 많습니다.
Tailwind에서는 attribute가 존재하는지만 볼 수도 있고, 특정 값을 가진 경우만 볼 수도 있습니다. 는 attribute가 있을 때 적용되고는 일 때만 적용됩니다.
<button type="button" data-active class="rounded-full border border-slate-200 px-3 py-1.5 text-sm text-slate-600 data-active:border-blue-600 data-active:bg-blue-50 data-active:text-blue-700"
> 인기순
</button> 필터 칩처럼 단순히 “현재 선택된 모양”만 필요한 경우에는 가 읽기 편합니다. 접근성 역할이 명확한 탭이나 토글 버튼이라면를 먼저 검토해야 하지만, 내부 정렬 옵션이나 카드 강조 상태처럼 앱 로직의 상태에 가까운 값은 가 더 자연스럽습니다.
<div data-state="open" class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm data-[state=open]:border-blue-300 data-[state=open]:shadow-md data-[state=closed]:opacity-70"
> <button type="button" class="flex w-full items-center justify-between text-sm font-semibold"> 상세 조건 <span>⌄</span> </button>
</div> 처럼 대괄호가 들어간 문법은 처음 보면 Tailwind만의 특수한 문법처럼 느껴질 수 있습니다. 실제로는 특정 attribute selector를 utility variant로 표현하는 방식입니다. 값이 정해진 상태를 직접 확인해야 할 때 사용합니다.
Headless UI, Radix UI, shadcn/ui 계열 컴포넌트를 쓰다 보면같은 attribute를 자주 보게 됩니다. 이때 무조건 React state에 따라 을 조건부로 조립하기보다, 컴포넌트가 이미 내려주는 상태를 Tailwind variant로 받아서 처리하면 마크업의 상태와 스타일 조건이 같은 위치에 남습니다.
물론 도 과하면 읽기 어려워집니다.가 한 프로젝트 안에서 뒤섞이면 같은 선택 상태를 서로 다른 이름으로 관리하게 됩니다. 프로젝트에서 자주 쓰는 상태 이름은 먼저 정해두는 것이 좋습니다. 예를 들어 단순 활성 상태는 선택 가능한 목록의 선택 상태는 열림과 닫힘은 처럼 나눌 수 있습니다.
상태 variant를 조합할 때 확인할 기준

상태 variant가 늘어나는 순간부터는 “어떤 클래스가 맞는가”보다 “상태의 주인이 누구인가”를 먼저 확인해야 합니다. 버튼이 실제로 비활성인지, 단순히 아직 사용할 수 없어 보이는 상태인지, 선택 상태가 접근성 의미를 가져야 하는지, 컴포넌트 라이브러리가 이미 attribute를 내려주는지에 따라 선택지가 달라집니다.
| 상황 | 먼저 볼 variant | 확인할 기준 |
|---|---|---|
| 마우스를 올렸을 때만 반응 | 필수 정보가 hover에만 숨어 있지 않은지 확인합니다. | |
| 키보드 탐색 위치 표시 | outline을 없애는 대신 명확한 포커스 표시를 남깁니다. | |
| 실제 폼 컨트롤 비활성화 | HTML 속성이 함께 적용되는지 확인합니다. | |
| 탭, 토글, 아코디언 의미 상태 | , | 화면 상태와 ARIA 상태가 같은 값을 가리키는지 확인합니다. |
| 컴포넌트 내부 활성 상태 | , | 상태 이름이 프로젝트 안에서 일관되는지 확인합니다. |
예를 들어 저장 버튼이 로딩 중일 때 “누를 수 없음”을 표현해야 한다면 두 가지를 나눠서 봅니다. 실제 제출이 막혀야 하고 native button이라면 속성과 를 사용합니다. 하지만 메뉴 항목처럼 focus order에는 남겨야 하고, 현재만 실행 불가능한 상태를 알려야 한다면 와 별도 이벤트 차단이 필요할 수 있습니다.
또 다른 예로 탭 메뉴를 보면, 선택된 탭은 로도 충분히 색을 바꿀 수 있습니다. 하지만 구조를 쓰고 있다면 가 더 직접적인 상태입니다. 이때 와 를 둘 다 쓰는 방식도 가능하지만, 둘의 값이 어긋나면 유지보수 포인트가 늘어납니다. 같은 상태를 두 attribute에 중복 저장해야 한다면 그 이유가 분명해야 합니다.
클래스가 길어지는 것도 점검 신호입니다. Tailwind는 utility를 HTML에서 바로 조합하는 방식이 강점이지만, 상태 variant가 너무 길게 반복되면 컴포넌트 분리가 필요할 수 있습니다. 같은 버튼 상태 조합이 여러 곳에서 반복된다면 공통 Button 컴포넌트, class 조립 함수같은 프로젝트 규칙을 검토할 수 있습니다. 반대로 한두 번만 쓰는 특수한 상태라면 HTML 안에 그대로 두는 편이 더 읽기 쉬울 수 있습니다.
정리: 상태를 누가 알고 있는지부터 확인하기
Tailwind CSS의 상태 variant는 문법 자체보다 상태 기준을 잡아두는 것이 더 오래 갑니다.는 브라우저가 판단하는 상호작용 상태에 가깝고는 UI의 의미 상태를 접근성 속성과 연결할 때 쓰이며는 컴포넌트 로직이나 라이브러리가 내려주는 내부 상태를 스타일 조건으로 삼을 때 자주 사용합니다.
버튼을 만들 때는 hover보다 disabled와 을 먼저 점검해야 합니다. 마우스를 올렸을 때 예쁘게 바뀌는 것보다, 키보드로 접근했을 때 위치가 보이고, 비활성 상태에서 실제 동작이 막히는지가 더 기본입니다.
탭이나 아코디언처럼 의미 있는 상태가 있는 UI에서는 를 스타일 조건으로 사용할 수 있습니다. 다만 ARIA는 장식용 표시가 아니라 실제 의미 상태이므로, 화면과 값이 어긋나지 않게 관리해야 합니다.
컴포넌트 라이브러리나 직접 만든 UI 로직이를 내려준다면 variant를 활용하면 됩니다. 이때는 상태 이름을 프로젝트 안에서 일관되게 유지하는 것이 중요합니다. 같은 활성 상태를 어떤 곳에서는 다른 곳에서는 또 다른 곳에서는 로 쓰면 나중에 스타일을 찾는 비용이 커집니다.
다음에 상태 스타일을 붙일 때는 class 이름부터 찾기보다 질문을 먼저 바꿔보면 됩니다. 이 상태는 브라우저가 알고 있는가, 접근성 의미가 필요한가, 컴포넌트가 따로 내려주는 값인가. 이 세 가지를 나누면를 고르는 기준이 훨씬 선명해집니다.
같이 읽으면 좋은 글
- Tailwind CSS 실무 로드맵: 기본 클래스부터 반응형·상태·동적 클래스까지
- React 처음 배우는 순서: 컴포넌트부터 state, Zustand까지
- Tailwind CSS 클래스 감지 원리: 동적 className이 빠지는 이유