notes
MU - data -* ( 데이터 선택자 )
CSS MU·data-* 속성을 활용한 요소 선택자 문법과 사용 메모.
- Radix 컴포넌트는 상태를
data-state같은 data- 속성으로 DOM에 꺼내 둔다. - 스타일은
[data-state="active"]선택자나 Tailwind의data-[state=active]:로 건다. - JS에서는
el.dataset으로 읽고,data-user-name은dataset.userName으로 바뀜다.
Radix로 탭을 만들다가 활성 탭 색을 어디서 바꾸는지 찾아보니, 클래스를 붙였다 떼는 게 아니라 data-state 속성 하나였다. 그때 제대로 알아두려고 정리한 글.
data- 속성이란
HTML5부터는 태그에 내가 임의로 정한 속성을 붙일 수 있고, 이걸 Custom Data Attribute라고 부른다. 브라우저는 이 값으로 아무것도 안 하고 그냥 들고만 있다. 그래서 CSS나 JS가 꺼내 쓰기 좋다.
data-* 속성은 두 부분으로 구성된다.
- 속성 이름은 대문자를 포함해서는 안되며 "data-"접두어 다음에 적어도 하나 이상의 문자가 있어야 한다.
- 속성 값은 임의의 문자열이 될 수 있다.
ex) data-id / data-name / data-age / data-job...
상태를 data- 속성으로 넘기면 좋은 점
예전엔 isActive ? 'tab on' : 'tab' 같은 클래스 조합을 짠는데, 상태가 두세 개 겹치면 문자열이 금방 지저분해졌다. 상태 값을 data-state처럼 DOM에 그대로 꺼내 두면, 스타일은 CSS 선택자가 알아서 고른다. JS는 값만 바꾸고 끝.
테스트 코드에서도 같은 이름으로 확인할 수 있어서 편하다. 예를 들어 테스트에서 toHaveAttribute('data-state', 'active')로 지금 열린 탭을 바로 검사한다.
Radix Tabs로 보는 예시
Radix는 탭을 누를 때마다 내부 상태를 바꾸고, 그 결과를 엘리먼트 속성으로 써 준다. 내가 직접 쓰는 건 아래 값들을 읽는 CSS뿐이다.
핵심은 이 한 줄이다. 나머지 스타일은 모양 내는 것뿐이라 아래 코드에서 참고만 한다.
.TabsTrigger[data-state="active"] {
color: var(--violet-11);
box-shadow: inset 0 -1px 0 0 currentColor, 0 1px 0 0 currentColor;
}
import React from "react";
import * as Tabs from "@radix-ui/react-tabs";
import "./styles.css";
// 탭리스트 출력
<Tabs.List className="TabsList" aria-label="Manage your account">
<Tabs.Trigger className="TabsTrigger" value="tab1">
Account
</Tabs.Trigger>
<Tabs.Trigger className="TabsTrigger" value="tab2">
Password
</Tabs.Trigger>
</Tabs.List>
// 탭 value에 따른 component output
<Tabs.Content className="TabsContent" value="tab1">
<p className="Text">
Make changes to your account here. Click save when you're done.
</p>
<fieldset className="Fieldset">
<label className="Label" htmlFor="name">
Name
</label>
<input className="Input" id="name" defaultValue="Pedro Duarte" />
</fieldset>
<fieldset className="Fieldset">
<label className="Label" htmlFor="username">
Username
</label>
<input className="Input" id="username" defaultValue="@peduarte" />
</fieldset>
<div
style={{ display: "flex", marginTop: 20, justifyContent: "flex-end" }}
>
<button className="Button green">Save changes</button>
</div>
</Tabs.Content>
폼 꾀미는 스타일은 빼고 탭에 필요한 것만 남겼다.
@import "@radix-ui/colors/black-alpha.css";
@import "@radix-ui/colors/mauve.css";
@import "@radix-ui/colors/violet.css";
.TabsRoot {
display: flex;
flex-direction: column;
width: 300px;
box-shadow: 0 2px 10px var(--black-a4);
}
.TabsList {
flex-shrink: 0;
display: flex;
border-bottom: 1px solid var(--mauve-6);
}
.TabsTrigger {
all: unset;
flex: 1;
height: 45px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: center;
background-color: white;
font-size: 15px;
color: var(--mauve-11);
user-select: none;
}
.TabsTrigger:hover {
color: var(--violet-11);
}
/* 포인트: JS로 클래스를 바꾸지 않고 data-state 값만 보고 색을 바꾼다 */
.TabsTrigger[data-state="active"] {
color: var(--violet-11);
box-shadow: inset 0 -1px 0 0 currentColor, 0 1px 0 0 currentColor;
}
.TabsTrigger:focus {
position: relative;
box-shadow: 0 0 0 2px black;
}
.TabsContent {
flex-grow: 1;
padding: 20px;
background-color: white;
outline: none;
}Tailwind에서는
Tailwind도 같은 속성을 변형(variant)으로 바로 읽는다. shadcn/ui 컴포넌트를 열어 보면 대부분 이 방식이다.
<Tabs.Trigger
value="tab1"
className="px-5 h-11 text-gray-500 data-[state=active]:text-violet-600 data-[state=active]:shadow-[inset_0_-1px_0_0_currentColor]"
>
Account
</Tabs.Trigger>
data-[state=active]:는 [data-state="active"] 선택자로 바뀜다. 값 없이 존재만 보는 속성(data-disabled 같은 것)은 data-[disabled]:opacity-50처럼 쓴다.
JS에서 읽고 쓸 때
const el = document.querySelector('[data-user-name]') as HTMLElement;
el.dataset.userName; // "kim"
el.dataset.state = 'open'; // data-state="open" 으로 반영됨
delete el.dataset.state; // 속성 제거
이벤트 위임에서 어떤 항목을 눌렀는지 알아낼 때도 자주 쓴다. 목록 부모에 클릭 하나만 걸고 e.target.closest('[data-id]')?.dataset.id로 꺼내면 된다.