Card
관련 정보와 액션을 한 덩어리로 묶는 컨테이너입니다. 보더 대신 Surface 배경 + shadow/1 로 구획합니다.
1. Default
head(제목 + 상태) · desc · foot(액션 우측). 보더 없이 Surface 배경과 shadow/1 로 구획합니다.
라이선스
정상1,000석 중 842석 사용 중. 2026-12-31 만료.
<div class="card" style="width:360px">
<div class="card-head"><h3 class="title">라이선스</h3><span class="tag ok">정상</span></div>
<p class="desc">1,000석 중 842석 사용 중. 2026-12-31 만료.</p>
<div class="card-foot"><button type="button" class="btn sm secondary">상세</button><button type="button" class="btn sm primary">갱신</button></div>
</div>/* JS Design System v0.6 · .card, .card-grid · 토큰(var(--*))은 assets/style.css :root 참고 */
.card{background:var(--bg-surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);padding:var(--card-pad);display:flex;flex-direction:column;gap:12px;min-width:260px;position:relative}
.card .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.card .title{font-size:16px;line-height:24px;font-weight:600;margin:0}
.card .desc{font-size:14px;color:var(--text-secondary);margin:0;line-height:1.6}
.card .card-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:auto;padding-top:4px}
.card.clickable{appearance:none;border:0;font:inherit;color:inherit;text-align:left;text-decoration:none;cursor:pointer;transition:box-shadow var(--motion-fast)}
.card.bordered{box-shadow:none;border:1px solid var(--border-default)}
.card.clickable:hover{box-shadow:var(--shadow-2)}
.card.on{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-1);background:var(--accent-subtle)}
.card.clickable.on:hover{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-2)}
.card.bordered.on{box-shadow:inset 0 0 0 2px var(--accent)}
.card.compact{padding:16px;gap:8px}
.card.compact .title{font-size:14px;line-height:20px}
.card.flush{padding:0;gap:0;overflow:hidden}
.card.flush .card-head{padding:16px var(--card-pad)}
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--card-gap);width:100%}
.onboard .card{width:100%}import { Button, Card, CardDesc, CardFoot, CardHead, CardTitle, Tag } from "@jiran/ds-react";
export default function Example() {
return (
<Card style={{ width: 360 }}>
<CardHead>
<CardTitle>라이선스</CardTitle>
<Tag tone="ok">정상</Tag>
</CardHead>
<CardDesc>1,000석 중 842석 사용 중. 2026-12-31 만료.</CardDesc>
<CardFoot>
<Button size="sm" variant="secondary">상세</Button>
<Button size="sm">갱신</Button>
</CardFoot>
</Card>
);
}2. Header Actions
헤더 우측에 기간 선택(Select Button) 또는 더 보기 메뉴. 패널이 없으니 탭이 아니라 role=group + aria-pressed 를 씁니다.
주간 탐지 추이
이번 주 탐지 412건, 지난주보다 18% 증가
<div class="card" style="width:480px">
<div class="card-head"><h3 class="title">주간 탐지 추이</h3><div class="select-btn" role="group" aria-label="기간"><button type="button" class="on" aria-pressed="true">7일</button><button type="button" aria-pressed="false">30일</button></div></div>
<div class="sparkline" aria-hidden="true"><i style="height:30%"></i><i style="height:45%"></i><i style="height:38%"></i><i style="height:70%"></i><i style="height:55%"></i><i style="height:90%" class="hi"></i><i style="height:62%"></i></div>
<p class="desc">이번 주 탐지 <b>412</b>건, 지난주보다 18% 증가</p>
</div>/* JS Design System v0.6 · .card, .card-grid · 토큰(var(--*))은 assets/style.css :root 참고 */
.card{background:var(--bg-surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);padding:var(--card-pad);display:flex;flex-direction:column;gap:12px;min-width:260px;position:relative}
.card .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.card .title{font-size:16px;line-height:24px;font-weight:600;margin:0}
.card .desc{font-size:14px;color:var(--text-secondary);margin:0;line-height:1.6}
.card .card-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:auto;padding-top:4px}
.card.clickable{appearance:none;border:0;font:inherit;color:inherit;text-align:left;text-decoration:none;cursor:pointer;transition:box-shadow var(--motion-fast)}
.card.bordered{box-shadow:none;border:1px solid var(--border-default)}
.card.clickable:hover{box-shadow:var(--shadow-2)}
.card.on{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-1);background:var(--accent-subtle)}
.card.clickable.on:hover{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-2)}
.card.bordered.on{box-shadow:inset 0 0 0 2px var(--accent)}
.card.compact{padding:16px;gap:8px}
.card.compact .title{font-size:14px;line-height:20px}
.card.flush{padding:0;gap:0;overflow:hidden}
.card.flush .card-head{padding:16px var(--card-pad)}
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--card-gap);width:100%}
.onboard .card{width:100%}import { Card, CardDesc, CardHead, CardTitle, SelectButton, Sparkline } from "@jiran/ds-react";
export default function Example() {
return (
<Card style={{ width: 480 }}>
<CardHead>
<CardTitle>주간 탐지 추이</CardTitle>
{/* 패널이 없으니 탭이 아니라 role=group + aria-pressed */}
<SelectButton label="기간" defaultValue="7d" options={[{ value: "7d", label: "7일" }, { value: "30d", label: "30일" }]} />
</CardHead>
<Sparkline values={[30, 45, 38, 70, 55, 90, 62]} max={100} />
<CardDesc>이번 주 탐지 <b>412</b>건, 지난주보다 18% 증가</CardDesc>
</Card>
);
}3. Bordered / Compact
카드 위에 카드를 겹칠 때는 .bordered, 밀도 높은 그리드는 .compact.
Bordered
그림자 대신 1px 보더
Compact
padding 16 · 제목 14
<div class="card bordered" style="width:280px"><h3 class="title">Bordered</h3><p class="desc">그림자 대신 1px 보더</p></div>
<div class="card compact" style="width:280px"><h3 class="title">Compact</h3><p class="desc">padding 16 · 제목 14</p></div>/* JS Design System v0.6 · .card, .card-grid · 토큰(var(--*))은 assets/style.css :root 참고 */
.card{background:var(--bg-surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);padding:var(--card-pad);display:flex;flex-direction:column;gap:12px;min-width:260px;position:relative}
.card .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.card .title{font-size:16px;line-height:24px;font-weight:600;margin:0}
.card .desc{font-size:14px;color:var(--text-secondary);margin:0;line-height:1.6}
.card .card-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:auto;padding-top:4px}
.card.clickable{appearance:none;border:0;font:inherit;color:inherit;text-align:left;text-decoration:none;cursor:pointer;transition:box-shadow var(--motion-fast)}
.card.bordered{box-shadow:none;border:1px solid var(--border-default)}
.card.clickable:hover{box-shadow:var(--shadow-2)}
.card.on{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-1);background:var(--accent-subtle)}
.card.clickable.on:hover{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-2)}
.card.bordered.on{box-shadow:inset 0 0 0 2px var(--accent)}
.card.compact{padding:16px;gap:8px}
.card.compact .title{font-size:14px;line-height:20px}
.card.flush{padding:0;gap:0;overflow:hidden}
.card.flush .card-head{padding:16px var(--card-pad)}
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--card-gap);width:100%}
.onboard .card{width:100%}import { Card, CardDesc, CardTitle } from "@jiran/ds-react";
export default function Example() {
return (
<>
<Card bordered style={{ width: 280 }}>
<CardTitle>Bordered</CardTitle>
<CardDesc>그림자 대신 1px 보더</CardDesc>
</Card>
<Card compact style={{ width: 280 }}>
<CardTitle>Compact</CardTitle>
<CardDesc>padding 16 · 제목 14</CardDesc>
</Card>
</>
);
}4. Clickable / Selected
카드 전체가 눌리면 .clickable. 이동이면 a, 선택이면 button + aria-pressed 로 두고 선택된 것에 .on 을 줍니다.
<div class="card-grid" style="max-width:600px">
<button type="button" class="card clickable" aria-pressed="false"><h3 class="title">USB 제어</h3><p class="desc">정책 12개</p></button>
<button type="button" class="card clickable on" aria-pressed="true"><h3 class="title">네트워크</h3><p class="desc">정책 8개 · 선택됨</p></button>
</div>/* JS Design System v0.6 · .card, .card-grid · 토큰(var(--*))은 assets/style.css :root 참고 */
.card{background:var(--bg-surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);padding:var(--card-pad);display:flex;flex-direction:column;gap:12px;min-width:260px;position:relative}
.card .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.card .title{font-size:16px;line-height:24px;font-weight:600;margin:0}
.card .desc{font-size:14px;color:var(--text-secondary);margin:0;line-height:1.6}
.card .card-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:auto;padding-top:4px}
.card.clickable{appearance:none;border:0;font:inherit;color:inherit;text-align:left;text-decoration:none;cursor:pointer;transition:box-shadow var(--motion-fast)}
.card.bordered{box-shadow:none;border:1px solid var(--border-default)}
.card.clickable:hover{box-shadow:var(--shadow-2)}
.card.on{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-1);background:var(--accent-subtle)}
.card.clickable.on:hover{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-2)}
.card.bordered.on{box-shadow:inset 0 0 0 2px var(--accent)}
.card.compact{padding:16px;gap:8px}
.card.compact .title{font-size:14px;line-height:20px}
.card.flush{padding:0;gap:0;overflow:hidden}
.card.flush .card-head{padding:16px var(--card-pad)}
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--card-gap);width:100%}
.onboard .card{width:100%}import { Card, CardDesc, CardGrid, CardTitle } from "@jiran/ds-react";
export default function Example() {
return (
<CardGrid style={{ maxWidth: 600 }}>
<Card as="button" type="button" clickable selected={false}>
<CardTitle>USB 제어</CardTitle>
<CardDesc>정책 12개</CardDesc>
</Card>
<Card as="button" type="button" clickable selected>
<CardTitle>네트워크</CardTitle>
<CardDesc>정책 8개 · 선택됨</CardDesc>
</Card>
</CardGrid>
);
}5. Loading
데이터를 기다리는 동안 내용 자리에 Skeleton 을 넣고 컨테이너에 aria-busy 를 둡니다. 로드되면 같은 자리를 실제 내용으로 바꿥니다.
<div class="card" style="width:360px" aria-busy="true">
<span class="skeleton title"></span>
<span class="skeleton text"></span>
<span class="skeleton text short"></span>
</div>/* JS Design System v0.6 · .card, .card-grid · 토큰(var(--*))은 assets/style.css :root 참고 */
.card{background:var(--bg-surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);padding:var(--card-pad);display:flex;flex-direction:column;gap:12px;min-width:260px;position:relative}
.card .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.card .title{font-size:16px;line-height:24px;font-weight:600;margin:0}
.card .desc{font-size:14px;color:var(--text-secondary);margin:0;line-height:1.6}
.card .card-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:auto;padding-top:4px}
.card.clickable{appearance:none;border:0;font:inherit;color:inherit;text-align:left;text-decoration:none;cursor:pointer;transition:box-shadow var(--motion-fast)}
.card.bordered{box-shadow:none;border:1px solid var(--border-default)}
.card.clickable:hover{box-shadow:var(--shadow-2)}
.card.on{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-1);background:var(--accent-subtle)}
.card.clickable.on:hover{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-2)}
.card.bordered.on{box-shadow:inset 0 0 0 2px var(--accent)}
.card.compact{padding:16px;gap:8px}
.card.compact .title{font-size:14px;line-height:20px}
.card.flush{padding:0;gap:0;overflow:hidden}
.card.flush .card-head{padding:16px var(--card-pad)}
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--card-gap);width:100%}
.onboard .card{width:100%}import { Card } from "@jiran/ds-react";
export default function Example() {
return <Card loading style={{ width: 360 }} />;
}Props
HTML 에서는 클래스와 속성으로, React(@jiran/ds-react)에서는 같은 이름의 prop(variant · size …)으로 변형·상태를 지정합니다.
| Prop / Class | Type · Values | Default | Description |
|---|---|---|---|
| .card | container | — | flex column · gap 12 · padding 24 · 최소 폭 260. React Card |
| .card-head / .title / .desc / .card-foot | elements | — | 헤더(양끝 정렬) / 제목 Title/1 16·24 / 설명 Body/2 14 / 푸터 우측 정렬 — React CardHead·CardTitle·CardDesc·CardFoot |
| .bordered | boolean | false | shadow 대신 1px 보더. 카드 위에 카드를 겹칠 때 |
| .compact | boolean | false | padding 16 · gap 8 · 제목 14(Title/2) |
| .flush | boolean | false | padding·gap 0 — 표나 목록을 카드 폭까지 꽉 채울 때. 헤더만 자체 여백을 갖습니다 |
| .clickable | boolean | false | 카드 전체가 눌림. 이동이면 a, 선택이면 button + aria-pressed. hover 에 shadow/2. React clickable + as |
| .card-grid .card.clickable | behavior | — | ds.js 가 단일 선택으로 묶어 .on 과 aria-pressed 를 함께 갱신하고 ds:select 를 발행합니다 |
| .on | boolean | false | 선택됨 — 안쪽 2px accent 링 + --accent-subtle 배경. 포커스 링(outline)과 겹치지 않아 둘이 동시에 보입니다. React selected |
| aria-busy | attribute | — | 불러오는 중 — 내용 대신 .skeleton 을 넣고 컨테이너에 둡니다. React loading 이 둘을 함께 처리합니다 |
| .card-grid | container | — | auto-fill minmax(260px,1fr) 그리드 · gap 20. React CardGrid |
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 { Card, CardHead, CardTitle, CardDesc, CardFoot, CardGrid } 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 .card, .card-grid
/* JS Design System v0.6 · .card, .card-grid · 토큰(var(--*))은 assets/style.css :root 참고 */
.card{background:var(--bg-surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-1);padding:var(--card-pad);display:flex;flex-direction:column;gap:12px;min-width:260px;position:relative}
.card .card-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
.card .title{font-size:16px;line-height:24px;font-weight:600;margin:0}
.card .desc{font-size:14px;color:var(--text-secondary);margin:0;line-height:1.6}
.card .card-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:auto;padding-top:4px}
.card.clickable{appearance:none;border:0;font:inherit;color:inherit;text-align:left;text-decoration:none;cursor:pointer;transition:box-shadow var(--motion-fast)}
.card.bordered{box-shadow:none;border:1px solid var(--border-default)}
.card.clickable:hover{box-shadow:var(--shadow-2)}
.card.on{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-1);background:var(--accent-subtle)}
.card.clickable.on:hover{box-shadow:inset 0 0 0 2px var(--accent),var(--shadow-2)}
.card.bordered.on{box-shadow:inset 0 0 0 2px var(--accent)}
.card.compact{padding:16px;gap:8px}
.card.compact .title{font-size:14px;line-height:20px}
.card.flush{padding:0;gap:0;overflow:hidden}
.card.flush .card-head{padding:16px var(--card-pad)}
.card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:var(--card-gap);width:100%}
.onboard .card{width:100%}