JS Design System v0.6

Button

사용자의 의도를 명확하게 전달하고 행동을 유도합니다. 버튼의 형태와 색은 우선순위를 시각적으로 구분합니다.

1. Default

변형·크기를 지정하지 않은 기본 버튼입니다. 화면의 주 행동 하나에만 씁니다.

2. Variant

우선순위에 따라 Primary(화면당 하나) · Secondary · Tertiary · Text · Danger 를 씁니다. 제품 메인 컬러가 상태색과 겹치면 제품 컬러가 쓰이는 곳이 모두 중립색으로 내려가, Primary 도 함께 바뀝니다(Colors › 중립 전환).

3. Size

sm 36 · md 44 · lg 52. 테이블·툴바 안에서는 sm, 폼 하단은 md, 온보딩·빈 화면은 lg.

4. Icon

아이콘은 라벨 앞뒤 중 한쪽에 하나만 둡니다(양쪽 동시 금지). 아이콘 전용 버튼은 .icon 과 aria-label 을 함께 씁니다. 아이콘 크기는 버튼 크기를 따릅니다 — sm 16 · md 18 · lg 20.

5. State

Hover 와 Pressed 는 CSS 가 처리합니다 — 면은 기준 단계에서 글자색 반대 방향으로 한 단계·두 단계 갑니다. Loading 은 .loading 클래스로, 라벨은 자리만 지키고 보이지 않아 폭이 흔들리지 않으며 클릭은 마우스·키보드 모두 막힙니다. 비활성은 아래 Disabled 에서 따로 다룹니다.

6. Disabled

비활성은 투명도를 덮지 않고 전용 토큰을 씁니다 — fill 은 회색 면(--interaction-disable) + 회색 라벨(--text-disabled), outline 은 면을 비우고 보더만 회색으로 바뀝니다. 그래서 어떤 변형이든 비활성은 한 가지 회색으로 읽힙니다. 비활성 요소는 탭 순서에서 빠지므로, 막는 대신 왜 누를 수 없는지 알려주는 쪽을 먼저 검토하세요. 링크는 disabled 를 가질 수 없어 aria-disabled="true" 를 씁니다.

7. Group / Block

버튼 묶음은 .btn-group(8px 간격), 저장이 가장 오른쪽. 폼 하단에서는 .end 로 오른쪽 정렬. 전체 폭은 .block.

Props

HTML 에서는 클래스와 속성으로, React(@jiran/ds-react)에서는 같은 이름의 prop(variant · size …)으로 변형·상태를 지정합니다.

Prop / ClassType · ValuesDefaultDescription
variantprimary | secondary | tertiary | text | danger | danger secondary—우선순위. danger secondary 는 danger 아웃라인(React 는 danger-secondary). fill 은 primary · tertiary · danger, outline 은 secondary · danger secondary
sizesm | md | lgmd높이 36 / 44 / 52
.iconbooleanfalse아이콘 전용 정사각 버튼. aria-label 필수
.blockbooleanfalse전체 폭
.btn svgelement—아이콘은 라벨 앞뒤 어느 한쪽에 하나만(React leading / trailing). 크기는 sm 16 · md 18 · lg 20
a.btnelement—링크를 버튼 모양으로 쓸 때(React as). 이동이면 a, 실행이면 button. 링크는 disabled 속성을 가질 수 없어 비활성은 aria-disabled="true" 로 — React disabled 가 그 속성과 클릭 차단을 함께 처리합니다
.loadingbooleanfalse스피너만 보이고 라벨은 자리만 지킵니다(폭이 흔들리지 않게). aria-busy="true" 를 함께 두고, 마우스·키보드 클릭은 모두 막힙니다
.btn-groupcontainer—버튼 묶음(8px 간격). 폼 하단 우측 정렬은 .end
disabledattribute—비활성. 전용 토큰 — fill 은 --interaction-disable 면 + --text-disabled 라벨, outline 은 면을 비우고 보더만 회색. 투명도를 덮지 않아 변형마다 다른 색이 남지 않습니다
typebutton | submit | resetbutton폼 안에서는 명시

Usage

React: @jiran/ds-react 패키지를 설치하고 style.css 를 한 번 import 하면 위 React 탭 코드를 그대로 쓸 수 있습니다(React Package). HTML: assets/style.css 를 연결하고(일부만 필요하면 아래 컴포넌트 CSS 복사), 동작이 필요하면 assets/ds.js 를 함께 넣습니다 — 이 페이지의 프리뷰가 그 스크립트로 동작합니다. 색은 전부 토큰(var(--accent) 등)을 참조하므로 --accent-* 만 제품 컬러로 바꾸면 됩니다.

React @jiran/ds-react

npm i @jiran/ds-react

// main.tsx — 앱 진입점에서 한 번
import "@jiran/ds-react/style.css";

// 컴포넌트
import { Button, ButtonGroup } from "@jiran/ds-react";

HTML

<!-- 1) 토큰 + 전체 컴포넌트 스타일 한 번에 연결 -->
<link rel="stylesheet" href="assets/style.css">
<!-- 동작(드롭다운·탭·달력·표 정렬/선택·토스트 등) — 순수 JS 한 파일. React 제품은 불필요 -->
<script src="assets/ds.js" defer></script>

<!-- 2) 또는 아래 "Component CSS" 만 프로젝트 CSS 에 붙이고,
        :root 토큰은 Foundations › Design Token 에서 복사 -->

Component CSS .btn, .btn-group

/* JS Design System v0.6 · .btn, .btn-group · 토큰(var(--*))은 assets/style.css :root 참고 */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:44px;padding:0 20px;border-radius:var(--radius-md);border:1px solid transparent;font:inherit;font-size:14px;font-weight:600;line-height:1;cursor:pointer;white-space:nowrap;text-decoration:none;transition:background var(--motion-fast),border-color var(--motion-fast)}
.btn.sm{height:36px;padding:0 14px;font-size:12px}
.btn.md{height:44px;padding:0 20px;font-size:14px}
.btn.lg{height:52px;padding:0 26px;font-size:16px}
.btn.primary{background:var(--accent);color:var(--accent-on)}
.btn.primary:hover{background:var(--accent-hover)}
.btn.primary:active{background:var(--accent-pressed)}
.btn.secondary{background:transparent;border-color:var(--border-strong);color:var(--text-primary)}
.btn.secondary:hover{background:var(--fill-normal)}
.btn.secondary:active{background:var(--fill-strong)}
.btn.tertiary{background:var(--fill-alternative);color:var(--text-primary)}
.btn.tertiary:hover{background:var(--fill-normal)}
.btn.tertiary:active{background:var(--fill-strong)}
.btn.text{background:transparent;color:var(--accent-text);padding-left:8px;padding-right:8px}
.btn.text:hover{background:var(--accent-subtle)}
.btn.text:active{background:var(--fill-strong)}
.btn.danger{background:var(--danger);color:var(--danger-on)}
.btn.danger:hover{background:var(--danger-hover)}
.btn.danger:active{background:var(--danger-pressed)}
.btn.danger.secondary{background:transparent;color:var(--danger-text);border-color:var(--danger)}
.btn.danger.secondary:hover{background:var(--danger-subtle)}
.btn.danger.secondary:active{background:var(--fill-strong)}
.btn.icon{width:44px;padding:0}
.btn.icon.sm{width:36px}
.btn.icon.lg{width:52px}
.btn svg{width:18px;height:18px;flex:none}
.btn.sm svg{width:16px;height:16px}
.btn.lg svg{width:20px;height:20px}
.btn.block{width:100%}
.btn[disabled],.btn[aria-disabled="true"]{background:var(--interaction-disable);border-color:transparent;color:var(--text-disabled);cursor:not-allowed;pointer-events:none}
.btn.secondary[disabled],.btn.secondary[aria-disabled="true"]{background:transparent;border-color:var(--interaction-disable)}
.btn.danger.secondary[disabled],.btn.danger.secondary[aria-disabled="true"]{background:transparent;border-color:var(--interaction-disable);color:var(--text-disabled)}
.btn.text[disabled],.btn.text[aria-disabled="true"]{background:transparent}
.btn.loading{position:relative;color:transparent;pointer-events:none}
.btn.loading::after{content:"";position:absolute;width:16px;height:16px;border:2px solid var(--fill-strong);border-top-color:var(--accent-on);border-radius:50%;animation:spin .8s linear infinite}
.btn.secondary.loading::after,.btn.tertiary.loading::after{border-top-color:var(--accent)}
.btn.text.loading::after{border-top-color:var(--accent-text)}
.btn.danger.loading::after{border-top-color:var(--danger-on)}
.btn.danger.secondary.loading::after{border-top-color:var(--danger)}
.btn-group{display:inline-flex;gap:8px;align-items:center}
.btn-group.end{justify-content:flex-end;width:100%}
.banner .btn{margin-left:auto}
.tbl .actions .btn{height:32px;padding:0 10px;font-size:12px}
.action-bar .btn{height:32px;padding:0 12px;font-size:12px}
.empty .btn{margin-top:10px}
@keyframes spin{to{transform:rotate(360deg)}}