Core Concepts
스타일링
data-attribute 셀렉터, className 오버라이드, 다크모드, 애니메이션 패턴을 설명합니다.
reopt designUpdated
data-[attr] 셀렉터
opt-ui primitives는 컴포넌트 상태를 data attribute로 노출합니다. Tailwind의 data-* variant로 상태별 스타일을 선언적으로 적용할 수 있습니다.
| 셀렉터 | 의미 | 사용 예 |
|---|---|---|
data-[active-item] | Composite 내 현재 활성 아이템 | data-[active-item]:bg-bg-subtle |
data-[focus-visible] | 키보드 포커스 표시 | data-[focus-visible]:ring-2 |
data-[enter] | 진입 트랜지션 상태 | data-[enter]:opacity-100 |
data-[leave] | 퇴장 트랜지션 상태 | data-[leave]:opacity-0 |
data-[open] | Disclosure/Dialog 열림 상태 | [[data-open]>&]:rotate-180 |
aria-[invalid=true] | 폼 유효성 검증 실패 | aria-[invalid=true]:border-red-500 |
className 오버라이드
Primitive의 핵심 스타일 패턴은 className ?? defaultClass입니다.
tsx
// className을 전달하지 않으면 기본 스타일 적용
<DisclosureTrigger>열기</DisclosureTrigger>
// className을 전달하면 기본 스타일을 완전히 교체
<DisclosureTrigger className="custom-trigger-style">
열기
</DisclosureTrigger>tailwind-merge를 사용하지 않는 이유: 병합 시 어떤 스타일이 최종 적용되는지 예측하기 어렵습니다. 완전 교체 방식은 결과가 명확하고, 기본 스타일을 원하면 전달하지 않으면 됩니다.
tsx
// primitives/disclosure.tsx — 실제 구현
const triggerClass =
"flex w-full items-center justify-between px-4 py-4 text-left ...";
export function DisclosureTrigger({
className,
...props
}: DisclosureTriggerProps) {
return (
<AriaDisclosure
className={className ?? triggerClass} // 전달 시 교체, 미전달 시 기본
{...props}
/>
);
}다크 모드
OptThemeProvider가 data-theme를 관리하고, Tailwind dark: variant는 [data-theme$="-dark"] 셀렉터로 오버라이드됩니다. 모든 Primitive의 기본 스타일에 dark: variant가 포함되어 있어 별도 설정 없이 다크모드가 동작합니다.
tsx
// 기본 스타일에 dark: variant 포함
const triggerClass =
"... text-text-primary hover:bg-bg-subtle dark:hover:bg-white/[0.08]/50 ...";애니메이션 상태
Primitive 레이어가 노출하는 data-enter, data-leave, data-backdrop 속성을 globals.css에서 전역 트랜지션으로 처리합니다.
css
/* globals.css — primitive transition states */
[data-enter] {
opacity: 1;
transform: translateY(0);
transition:
opacity 150ms ease-out,
transform 150ms ease-out;
}
[data-leave] {
opacity: 0;
transform: translateY(-4px);
transition:
opacity 100ms ease-in,
transform 100ms ease-in;
}
[data-backdrop] {
transition: opacity 200ms ease-out;
}
[data-backdrop][data-leave] {
transition: opacity 150ms ease-in;
}Disclosure의 콘텐츠 영역은 Tailwind CSS 애니메이션 유틸리티와 data-[enter]/data-[leave]를 함께 사용합니다:
tsx
const contentClass =
"overflow-hidden " +
"data-[enter]:animate-in data-[enter]:fade-in data-[enter]:slide-in-from-top-1 " +
"data-[leave]:animate-out data-[leave]:fade-out";