shadcn/ui는 링크할 수 있는 파일이 아니라 React 프로젝트용 라이브러리입니다. "shadcn ui" 검색은 버튼, 카드, 입력, 탭 등 이미 만들어진 구성 요소를 가리킵니다.
명령줄 도구는 이를 코드에 복사하지만 일반 HTML 파일에서는 이를 실행할 수 없습니다. 스타일링을 빌릴 수 있습니다.
다음은 CDN의 Tailwind를 사용하여 하나의 HTML 파일로 구축된 shadcn 스타일의 카드입니다. 이름을 입력하고 만들기를 누릅니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>shadcn-style card in plain HTML</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
<style type="text/tailwindcss">
/* theme tokens: the same names shadcn/ui uses */
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
/* map tokens to Tailwind utilities: bg-card, text-primary, ... */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-md: calc(var(--radius) * 0.8);
--radius-xl: calc(var(--radius) * 1.4);
}
@layer base { * { border-color: var(--border); } }
</style>
</head>
<body class="bg-background text-foreground font-sans p-4">
<!-- Card: the class string below is the one in shadcn/ui's card.tsx -->
<div class="flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm max-w-sm mx-auto">
<div class="grid gap-2 px-6">
<div class="leading-none font-semibold">Create project</div>
<div class="text-sm text-muted-foreground">Name it, then press Create.</div>
</div>
<form id="f" class="grid gap-3 px-6">
<label for="name" class="flex items-center gap-2 text-sm leading-none font-medium">Name</label>
<input id="name" name="name" value="my-first-page" required
class="h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none placeholder:text-muted-foreground md:text-sm focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50">
<div class="flex items-center gap-2 pt-1">
<button type="submit"
class="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium whitespace-nowrap text-primary-foreground hover:bg-primary/90 focus-visible:ring-[3px] focus-visible:ring-ring/50 outline-none">Create</button>
<span id="out" class="text-sm text-muted-foreground"></span>
</div>
</form>
</div>
<script>
// Plain JS: shadcn/ui's React code is not needed for a static card
document.getElementById('f').addEventListener('submit', (e) => {
e.preventDefault(); // stay on the page, no server here
const name = new FormData(e.target).get('name');
document.getElementById('out').textContent = 'Created "' + name + '"';
});
</script>
</body>
</html>
화면의 모든 것은 HTML, Tailwind 클래스 및 CSS 변수입니다. 반응도 없고 빌드 단계도 없습니다.
shadcn/ui가 무엇이며 스크립트 태그가 없는 이유는 무엇입니까?
shadcn/ui 문서에서는 이를 오픈 소스이자 오픈 코드인 "아름답게 디자인되고 접근 가능한 구성 요소 집합 및 코드 배포 플랫폼"이라고 설명합니다. 소개에는 "이것은 구성 요소 라이브러리가 아닙니다. 구성 요소 라이브러리를 구축하는 방법입니다."라고 나와 있습니다.
해당 문장은 누락된 스크립트 태그를 설명합니다. 서버에서 shadcn/ui를 로드하지 않습니다. 아래와 같은 명령은 구성 요소의 소스 파일을 편집할 수 있는 사용자의 프로젝트에 복사합니다.
pnpm dlx shadcn@latest add button
버튼 페이지에는 React 가져오기 @/components/ui/button와 함께 사용되는 결과가 표시됩니다. 프로젝트의 components.json 파일은 그런 종류의 별칭을 설정하므로 HTML 파일을 여는 브라우저에서는 이를 확인할 수 없습니다.
GitHub README에 따르면 이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다.
모양 복사: 토큰 및 클래스 문자열
두 가지가 shadcn 구성 요소를 실제처럼 보이게 만듭니다. 첫 번째는 --background, --primary 및 --radius와 같은 CSS 변수인 테마 토큰 세트입니다. 두 번째는 각 요소의 클래스 문자열입니다.
문서에는 각 토큰이 그 위에 텍스트를 색칠하는 -foreground 토큰과 쌍을 이룬다고 나와 있습니다.

하나의 HTML 파일에서 사용하려면:
cdn.jsdelivr.net의 Tailwind 브라우저 스크립트를 고정된 버전으로 추가합니다.type="text/tailwindcss"라고 표시된 스타일 블록에 토큰을 넣으세요.- 각 토큰을 Tailwind 색상에 매핑하는
@theme inline블록을 추가합니다. - 구성 요소의 클래스 문자열을 일반 요소에 복사합니다.
<script>에 모든 동작을 작성합니다.
위의 카드는 1~4단계입니다. shadcn/ui 소스에서 카드는 클래스 문자열이 있는 div이며 HTML 버전은 동일한 클래스를 사용합니다.
<div class="flex flex-col gap-6 rounded-xl border bg-card py-6
text-card-foreground shadow-sm">
Tailwind의 문서에서는 Play CDN용 스크립트 태그를 제공하고 Tailwind 기능이 필요한 사용자 정의 CSS가 type="text/tailwindcss"와 함께 스타일 태그에 포함된다고 나와 있습니다. 여기 데모처럼 버전을 고정하면 새 릴리스가 페이지를 변경할 수 없습니다.
빛과 어둠을 하나의 클래스로
모든 색상은 변수이기 때문에 어두운 모드는 동일한 이름에 대한 두 번째 값 집합입니다. shadcn/ui 문서에는 :root 옆에 .dark 블록이 표시되고 각 Tailwind 색상을 변수에서 가리키는 @theme inline 블록이 표시됩니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Button variants and a theme switch</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
<style type="text/tailwindcss">
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
/* dark mode = the same names with new values, no dark: classes needed */
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--color-ring: var(--ring);
--radius-md: calc(var(--radius) * 0.8);
}
@layer base { * { border-color: var(--border); } }
/* shared part of every button, simplified from shadcn/ui's button.tsx */
@utility btn {
display: inline-flex; align-items: center; justify-content: center;
height: 2.25rem; padding: 0 1rem; border-radius: var(--radius-md);
font-size: 0.875rem; font-weight: 500; white-space: nowrap; outline: none;
transition: all .15s;
}
</style>
</head>
<body class="bg-background text-foreground font-sans p-4 min-h-screen">
<div class="flex items-center justify-between gap-3 mb-5">
<h1 class="text-base font-semibold">Variants</h1>
<button id="theme" type="button" aria-pressed="false" class="btn border bg-background hover:bg-accent hover:text-accent-foreground">Dark</button>
</div>
<div class="flex flex-wrap gap-3">
<button class="btn bg-primary text-primary-foreground hover:bg-primary/90">Default</button>
<button class="btn bg-secondary text-secondary-foreground hover:bg-secondary/80">Secondary</button>
<button class="btn bg-destructive text-white hover:bg-destructive/90">Destructive</button>
<button class="btn border bg-background hover:bg-accent hover:text-accent-foreground">Outline</button>
<button class="btn hover:bg-accent hover:text-accent-foreground">Ghost</button>
<button class="btn text-primary underline-offset-4 hover:underline">Link</button>
</div>
<p class="mt-6 mb-2 text-sm text-muted-foreground">Badges use the same tokens.</p>
<div class="flex flex-wrap gap-2">
<span class="inline-flex items-center rounded-full border border-transparent bg-primary px-2 py-0.5 text-xs font-medium text-primary-foreground">Default</span>
<span class="inline-flex items-center rounded-full border border-transparent bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground">Secondary</span>
<span class="inline-flex items-center rounded-full border border-transparent bg-destructive px-2 py-0.5 text-xs font-medium text-white">Destructive</span>
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium text-foreground">Outline</span>
</div>
<script>
const btn = document.getElementById('theme');
btn.addEventListener('click', () => {
// flipping one class swaps every token value at once
const dark = document.documentElement.classList.toggle('dark');
btn.textContent = dark ? 'Light' : 'Dark';
btn.setAttribute('aria-pressed', dark);
});
</script>
</body>
</html>
버튼에는 dark: 클래스가 필요하지 않습니다. 버튼 클래스에는 bg-primary가 표시되고 bg-primary에는 --primary가 표시됩니다. 페이지 루트에서 클래스를 뒤집으면 모든 요소 아래에서 변수가 변경됩니다.
데모에서는 --radius, --background, --foreground 및 --primary에 대한 문서 목록의 기본 테마 값을 재사용합니다. 다른 값은 프로젝트의 정확한 팔레트가 아닌 시작점인 내 자신의 회색입니다.
패턴에 대한 자세한 내용은 CSS 변수 및 HTML의 Tailwind 다크 모드를 참조하세요.
일반 HTML에는 나오지 않는 것
shadcn/ui의 일부 부분은 페이지가 아닌 프로젝트에 속합니다.

| 부품 | shadcn 프로젝트에서 | 하나의 HTML 파일에서 |
|---|---|---|
| 버튼이나 카드의 모습 | .tsx 구성요소 |
div 또는 button의 동일한 클래스 |
| 색상 | 사이트 CSS 파일의 변수 | 스타일 블록의 변수 |
| 구성 요소 추가 | CLI 명령 | 마크업 붙여넣기 |
| 탭, 스위치 | 상태를 추적하는 구성요소 | 마크업과 나만의 JavaScript |
| 수입품 | @/components/... 경로 |
사용할 수 없음 |
버튼 소스는 스타일 지정 외에 구성 요소가 수행하는 작업도 보여줍니다. 이는 기본, 보조, 파괴, 개요, 고스트 및 링크 등 각 변형에 대해 클래스 세트를 선택하는 React 함수입니다. HTML에서는 변형 데모와 마찬가지로 클래스 세트를 직접 선택합니다.
실제 구성 요소를 원한다면 Next.js, Vite, TanStack Start, Laravel, React Router 및 Astro에 대한 문서 목록 프레임워크 가이드를 참조하세요. React 앱이 페이지가 되려면 먼저 빌드가 필요합니다. 페이지로 React 앱 호스팅에서 해당 경로를 다룹니다.
행동을 직접 추가하세요
shadcn/ui 탭과 스위치는 radix-ui 패키지에 구축되었으며 해당 클래스는 속성을 읽어 페이지 스타일을 지정합니다. 탭 클래스는 data-[state=active]를 찾고, 스위치 클래스는 data-[state=checked]를 찾습니다.
Tailwind의 문서에서는 이를 가능하게 하는 data-* 및 aria-* 변형에 대해 설명합니다. 따라서 HTML에서 속성을 설정하면 동일한 스타일 규칙이 적용됩니다.

스위치에는 올바른 의미 체계도 필요합니다. MDN은 switch 역할을 aria-checked를 전달해야 하는 켜기/끄기 제어로 설명합니다. 여기서 true는 켜짐을 의미합니다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Settings card: tabs, switch and form</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
<style type="text/tailwindcss">
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-md: calc(var(--radius) * 0.8);
--radius-xl: calc(var(--radius) * 1.4);
}
@layer base { * { border-color: var(--border); } }
</style>
</head>
<body class="bg-background text-foreground font-sans p-4">
<div class="flex flex-col gap-5 rounded-xl border bg-card py-6 text-card-foreground shadow-sm max-w-md mx-auto">
<div class="px-6">
<div class="leading-none font-semibold">Settings</div>
<div class="mt-2 text-sm text-muted-foreground">Tabs and a switch, written without React.</div>
</div>
<!-- Tabs: data-state is the hook, the same idea Radix uses -->
<div class="px-6">
<div role="tablist" class="inline-flex h-9 w-full items-center rounded-lg bg-muted p-[3px]">
<button role="tab" id="t-account" aria-controls="account" data-state="active"
class="tab h-[calc(100%-1px)] flex-1 rounded-md px-2 text-sm font-medium text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm">Account</button>
<button role="tab" id="t-notify" aria-controls="notify" data-state="inactive"
class="tab h-[calc(100%-1px)] flex-1 rounded-md px-2 text-sm font-medium text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm">Alerts</button>
</div>
</div>
<form id="account" role="tabpanel" class="grid gap-3 px-6">
<label for="email" class="text-sm leading-none font-medium">Email</label>
<input id="email" name="email" type="email" required placeholder="you@example.com"
class="h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none placeholder:text-muted-foreground md:text-sm focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50">
<div class="flex items-center gap-3">
<button type="submit" class="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90">Save</button>
<span id="saved" class="text-sm text-muted-foreground" role="status"></span>
</div>
</form>
<div id="notify" hidden class="grid gap-3 px-6">
<div class="flex items-center justify-between gap-3">
<span id="sw-label" class="text-sm font-medium">Email me about changes</span>
<!-- Switch: a button with role="switch" -->
<button id="sw" type="button" role="switch" aria-checked="false" aria-labelledby="sw-label"
class="inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent bg-input p-0 transition-colors aria-checked:bg-primary focus-visible:ring-[3px] focus-visible:ring-ring/50 outline-none">
<span class="pointer-events-none block size-4 rounded-full bg-background shadow transition-transform translate-x-0 in-aria-checked:translate-x-[calc(100%-2px)]"></span>
</button>
</div>
<p id="state" class="text-sm text-muted-foreground">Alerts are off.</p>
</div>
</div>
<script>
const tabs = document.querySelectorAll('.tab');
const account = document.getElementById('account');
const notify = document.getElementById('notify');
tabs.forEach((tab) => tab.addEventListener('click', () => {
tabs.forEach((t) => t.setAttribute('data-state', t === tab ? 'active' : 'inactive'));
const showAccount = tab.id === 't-account';
account.hidden = !showAccount;
notify.hidden = showAccount;
}));
const sw = document.getElementById('sw');
sw.addEventListener('click', () => {
const on = sw.getAttribute('aria-checked') !== 'true';
sw.setAttribute('aria-checked', on);
document.getElementById('state').textContent = 'Alerts are ' + (on ? 'on.' : 'off.');
});
account.addEventListener('submit', (e) => {
e.preventDefault(); // handled inside the page, nothing is sent
document.getElementById('saved').textContent = 'Saved ' + new FormData(account).get('email');
});
</script>
</body>
</html>
양식은 페이지 내부에서 처리됩니다. 제출 리스너는 preventDefault()를 호출하고 MDN이 양식 필드에 대한 키 및 값 쌍으로 정의하는 FormData를 사용하여 필드를 읽습니다. 아무데도 전송되지 않습니다. 패턴은 작업 없는 양식을 참조하세요.
작동하지 않을 때
| 당신이 보는 것 | 원인 | 수정 |
|---|---|---|
| 페이지에 스타일이 전혀 없습니다. | Tailwind 스크립트가 로드되지 않았습니다. | 스크립트 태그를 확인하고 CDN 주소의 철자가 올바른지 확인하세요. |
bg-primary에는 색상이 표시되지 않습니다. |
토큰이 Tailwind에 매핑되지 않았습니다. | @theme inline 블록에 --color-primary 추가 |
@theme 블록이 무시됩니다 |
일반 스타일 태그에 위치합니다. | 해당 스타일 태그에 type="text/tailwindcss"를 사용하세요. |
| 다크 버튼은 아무것도 바꾸지 않습니다 | .dark 블록에서 일부 토큰이 누락되었습니다. |
모든 토큰을 재정의하고 클래스를 html 요소에 넣습니다. |
| 콘솔에 가져오기를 해결할 수 없다고 표시됩니다. | 프로젝트 가져오기 경로가 페이지에 있습니다. | 구성 요소를 일반 마크업 및 클래스로 교체 |
| 탭이나 스위치가 멈춘 것 같습니다. | 아무것도 상태 속성을 업데이트하지 않습니다. | 클릭 리스너에서 data-state 또는 aria-checked 설정 |
링크로 공유하세요
shadcn 스타일 페이지는 설명하는 것보다 보여주는 것이 더 쉽습니다. 왜냐하면 탭, 스위치 및 어두운 버튼은 누를 때만 의미가 있기 때문입니다. 스크린샷으로는 그렇게 할 수 없으며 .html 첨부 파일은 전화기에서 일반 코드로 열릴 수 있습니다.
작업 버전을 보내려면 페이지를 NOS 문서에 붙여넣고 공유 링크 만들기를 선택하세요. 링크할 HTML이 이를 안내합니다.
스크립트가 실행되고 CDN의 Tailwind 스크립트가 로드되므로 스크립트를 받은 사람은 계정 없이도 버튼을 직접 누를 수 있습니다. 나중에 코드를 변경하면 동일한 링크에 새 버전이 표시됩니다.
Tailwind 스크립트가 이미 있는 시작 파일은 Tailwind HTML 템플릿을 참조하세요.