@juchan/jds
v1.0.0
Published

Readme
순수 headless 컴포넌트 라이브러리입니다. 설치해도 아무 스타일이 없습니다.
버그가 아니라 설계입니다. 라이브러리는 동작 · 접근성 배선 · data-* 상태 노출만
책임지고 모양은 전부 소비자의 것입니다. 화면에 아무것도 보이지 않는 상태에서
시작해 className으로 채워 나가는 것이 정상 흐름입니다.
바로 붙여 쓸 수 있는 완성 스니펫은 Storybook의 Recipes/ 섹션에 있습니다.
📚 Deployment
- 항상 release branch의 최신 변경 사항을 반영합니다.
Package
- Link: https://www.npmjs.com/package/@juchan/jds
pnpm
pnpm add @juchan/jdsnpm
npm install @juchan/jdsStorybook
- Link: https://juchanhwang.github.io/jds/
설치와 CSS import
패키지만 설치하면 절반입니다. CSS를 import하지 않으면 토큰 유틸리티가 생성되지 않고, 에러도 없이 스타일만 조용히 빠집니다.
Tailwind CSS v4 소비자
@import "tailwindcss";
@import "@juchan/jds/theme.css";theme.css가 tokens.css를 함께 끌어오므로 이 두 줄이면 됩니다.
bg-primary-60 · text-b2 · text-gray-80 같은 유틸리티가 생성됩니다.
Tailwind를 쓰지 않는 소비자
@import "@juchan/jds/tokens.css";CSS 변수만 들어옵니다. var(--jds-color-primary-60) 형태로 직접 참조하세요.
peerDependencies
React 18 또는 19 (react, react-dom). 런타임 의존성은 없습니다.
컴포넌트
7종입니다. 훅 · 아이콘 · 내부 유틸리티는 공개하지 않습니다.
| 컴포넌트 | 루트 DOM | 비고 |
| --- | --- | --- |
| Button | <button> (as로 변경 가능) | 다형성. type="button"이 기본값 |
| Checkbox | <button role="checkbox"> + hidden input | indeterminate 지원, Checkbox.Indicator |
| Radio | <input type="radio"> | 단일 요소. native name 그룹핑 |
| Switch | <button role="switch"> + hidden input | Switch.Thumb |
| TextInput | <div> 래퍼 | 에러 · 초기화 버튼 |
| InputField | <div> 래퍼 | 라벨 슬롯 · readOnly |
| Select | context (자체 DOM 없음) | Select.Trigger / .Content / .Option |
최소 사용 예시입니다. 클래스는 전부 소비자 몫이라 아래에는 하나도 없습니다 — 그래서 이대로 렌더하면 아무것도 보이지 않습니다.
import { Button, Checkbox, Select, type SelectOption } from '@juchan/jds';
const options: SelectOption[] = [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
];
<Button onClick={submit}>제출</Button>;
<Checkbox name="agree" aria-label="동의">
<Checkbox.Indicator>✓</Checkbox.Indicator>
</Checkbox>;
<Select options={options} name="framework">
<Select.Trigger>선택</Select.Trigger>
<Select.Content>
{options.map((option, index) => (
<Select.Option key={option.value} index={index}>
{option.label}
</Select.Option>
))}
</Select.Content>
</Select>;공개 타입은 CheckedState · SelectOption · ButtonProps 세 가지입니다.
규약
data-* 상태
순수 headless에서 data-*는 소비자가 스타일을 걸 수 있는 유일한 상태 채널입니다.
불리언형은 값이 없는 presence이고, 열거형은 data-state 하나로 통일됩니다.
| 컴포넌트 | 속성 |
| --- | --- |
| Button | data-disabled |
| Checkbox | data-state(checked | unchecked | indeterminate) · data-disabled |
| Radio · Switch | data-state(checked | unchecked) · data-disabled |
| TextInput | data-disabled · data-invalid |
| InputField | data-disabled · data-invalid · data-readonly |
| Select | Trigger data-state(open | closed) · data-disabled · data-placeholder / Option data-state · data-highlighted · data-disabled |
폼 참여용 hidden input에는 data-*를 붙이지 않습니다.
상세는 docs/conventions/data-attributes.md.
prop 합성 순서
소비자 핸들러를 먼저 호출하고, 소비자가 event.preventDefault()를 호출했으면
내부 동작을 스킵합니다. 기본 동작을 끄는 공식 통로입니다.
ARIA · role · data-*는 소비자 props 스프레드 앞에 있어 필요하면 덮어쓸 수
있습니다. 핸들러와 ref는 스프레드 뒤에서 합성해 유실되지 않습니다.
예외: disabled 상태에서는 소비자 핸들러도 호출하지 않습니다. native
<button disabled>의 의미론을 비-native 요소까지 일관 적용합니다.
상세는 docs/conventions/prop-composition.md.
forwardRef
7종 전부 ref를 전달합니다. Select는 Trigger · Content · Option이 각각
받습니다(루트는 자체 DOM이 없습니다). TextInput · InputField의 ref는 래퍼
<div>가 아니라 **<input>**을 가리킵니다.
색 대비
흰 배경 위 전경색은 팔레트별 최소 scale 이상만 쓰세요 — primary 50 · error 50 ·
warning 70 · blue 60 · green 70 · gray 60 (WCAG 2.2 AA).
상세는 docs/conventions/color-contrast.md.
다크 모드
v1은 다크 값을 배포하지 않습니다. .dark 스코프에서 --jds-color-*를 재정의하면
@theme inline 덕분에 유틸리티가 그대로 따라옵니다.
.dark {
--jds-color-gray-10: #1f2937;
--jds-color-gray-80: #e5e7eb;
}v1.0.0 breaking changes
0.9.x에서 올라올 때 동작이 바뀌는 지점입니다.
비제어 모드에서
onChange계열 콜백이 호출되기 시작합니다.Select의onValueChange/onOpenChange,Checkbox의onCheckedChange가 대상입니다. 이전에는 비제어에서 호출되지 않아 변경을 감지할 수 없었습니다. 콜백 안에서 부수효과를 돌리고 있었다면 호출 횟수를 확인하세요.prop 합성 순서가 통일됩니다. 소비자 핸들러가 먼저 실행되고
event.defaultPrevented면 내부 동작이 스킵됩니다. 이전에는 컴포넌트마다 소비자와 내부 중 한쪽이 다른 쪽을 통째로 덮어썼습니다 —Checkbox는 소비자onClick이 내부 토글을 죽였고,Button은 소비자onClick이 무시됐습니다.Select에서 닫힌 상태의 ArrowUp / ArrowDown이 목록을 엽니다. 이전에는 보이지 않는 강조 인덱스만 움직여 키보드 사용자에게는 아무 반응이 없었습니다.data-*속성 규약이 확정됩니다. 불리언형은''또는 속성 부재로 표현합니다("true"문자열이 아닙니다).Checkbox.Indicator의data-state가 root와 같은 3값 도메인이 됩니다. 이전 값 도메인에 의존한 CSS는 갱신이 필요합니다.Checkbox의 hidden input이 더 이상 탭 스톱이 아닙니다. 이전에는 Tab을 누르면 보이지 않는 input에 포커스가 갔습니다. 버그 수정이지만 폼의 탭 순서가 바뀝니다.
알아둘 동작
indeterminate는 폼 제출 시checked=false로 나갑니다. hidden input의.indeterminateDOM 프로퍼티는 제출값을 바꾸지 않습니다. "부분 선택"을 서버로 보내야 한다면 별도 필드를 쓰세요.Radio만 DOM 구조가 다릅니다.Checkbox·Switch가<button role="...">루트인 것과 달리<input type="radio">단일 요소입니다.RadioGroup이 v1 스코프 밖이라 배타 선택을 브라우저의 nativename그룹핑에 맡기기 때문입니다.<button role="radio">로 가면 roving focus와 배타성을 직접 구현해야 하고, 그것이 곧 RadioGroup입니다.Select.Content는document.body로 portal됩니다. 부모의overflow에 잘리지 않는 대신z-index는 소비자가 정해야 합니다.Select의 강조 이동은 일부 스크린리더에서 자동 안내되지 않을 수 있습니다. 목록이 열려도 DOM 포커스가 트리거에 남는 설계라, listbox에 부여된aria-activedescendant가 보조기술에 전달되지 않을 수 있습니다. 완전한 포커스 관리(WAI-ARIA APG combobox 패턴)는 v1 스코프 밖이며 후속 버전에서 다룹니다.TextInput·InputField의 초기화 버튼은 제어 모드에서만 나타납니다. 내부 상태를 두지 않아 비제어 입력의 현재 값을 알 수 없습니다.
