Files
2nd/10_Wiki/Topics/Coding/Frontend_shadcn_Radix_Patterns.md
T
2026-05-10 22:08:15 +09:00

9.0 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
frontend-shadcn-radix-patterns shadcn/ui / Radix / Headless component patterns Coding draft B conceptual 2026-05-09 2026-05-09
frontend
ui
vibe-coding
language applicable_to
TS / React
Frontend
shadcn/ui
Radix
headless component
accessible component
Aria-Kit
Ark UI

shadcn/ui / Radix Patterns

"Component library install" → "Component code copy". Radix (a11y primitive) + Tailwind = shadcn/ui. Customizable. Modern React 의 default.

📖 핵심 개념

  • Headless: behavior + a11y, style 없음.
  • shadcn: copy code (npm install X).
  • Radix / Aria / Ark = primitive.
  • Tailwind = style.

💻 코드 패턴

shadcn/ui setup

npx shadcn@latest init
npx shadcn@latest add button dialog dropdown-menu

→ Code 가 components/ui/ 에 추가. Yours 가 됨.

Generated button

// components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium ...',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground',
        outline: 'border border-input bg-background',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 rounded-md px-3',
        lg: 'h-11 rounded-md px-8',
      },
    },
    defaultVariants: { variant: 'default', size: 'default' },
  }
);

export const Button = React.forwardRef<
  HTMLButtonElement,
  React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>
>(({ className, variant, size, ...props }, ref) => (
  <button ref={ref} className={cn(buttonVariants({ variant, size }), className)} {...props} />
));

CVA (class-variance-authority)

import { cva } from 'class-variance-authority';

const styles = cva('base-class', {
  variants: {
    color: { red: 'bg-red-500', blue: 'bg-blue-500' },
    size: { sm: 'h-8', md: 'h-10', lg: 'h-12' },
  },
  compoundVariants: [
    { color: 'red', size: 'lg', class: 'shadow-lg' },
  ],
  defaultVariants: { color: 'blue', size: 'md' },
});

styles({ color: 'red' });
// 'base-class bg-red-500 h-10'

Radix Dialog

import * as Dialog from '@radix-ui/react-dialog';

<Dialog.Root>
  <Dialog.Trigger asChild>
    <button>Open</button>
  </Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Overlay className="fixed inset-0 bg-black/50" />
    <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded p-6">
      <Dialog.Title>Title</Dialog.Title>
      <Dialog.Description>Description</Dialog.Description>
      <Dialog.Close>Close</Dialog.Close>
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

→ A11y (focus trap, escape, aria) 자동.

asChild pattern

// 자기 component 사용
<Dialog.Trigger asChild>
  <Button variant="outline">Open</Button>
</Dialog.Trigger>

// → Trigger 가 button render X. 자식 의 button 에 props 머지.

→ Style + a11y 공유 — wrapper 없음.

Radix primitives list

- Accordion / Alert Dialog / Avatar / Checkbox
- Collapsible / Context Menu / Dialog / Dropdown Menu
- Hover Card / Label / Menubar / Navigation Menu
- Popover / Progress / Radio Group / Scroll Area
- Select / Separator / Slider / Switch / Tabs
- Toast / Toggle / Tooltip

→ A11y + keyboard 자동.

Headless UI (Tailwind 의)

import { Menu } from '@headlessui/react';

<Menu>
  <Menu.Button>Options</Menu.Button>
  <Menu.Items>
    <Menu.Item>{({ active }) => <a className={active ? 'bg-blue-500' : ''}>Profile</a>}</Menu.Item>
    <Menu.Item>...</Menu.Item>
  </Menu.Items>
</Menu>

Ark UI (modern, multi-framework)

import { Dialog } from '@ark-ui/react';

<Dialog.Root>
  <Dialog.Trigger>Open</Dialog.Trigger>
  <Dialog.Backdrop />
  <Dialog.Positioner>
    <Dialog.Content>...</Dialog.Content>
  </Dialog.Positioner>
</Dialog.Root>

→ React + Vue + Solid + Svelte 같은 API.

React Aria

import { useButton } from '@react-aria/button';

function Button(props) {
  const ref = useRef();
  const { buttonProps } = useButton(props, ref);
  return <button {...buttonProps} ref={ref}>{props.children}</button>;
}

→ Adobe 의 a11y primitive.

Tailwind Variants (alternative)

import { tv } from 'tailwind-variants';

const button = tv({
  base: 'rounded px-4 py-2',
  variants: {
    color: { red: 'bg-red-500', blue: 'bg-blue-500' },
  },
});

<button className={button({ color: 'red' })} />

→ CVA 와 비슷.

Form (react-hook-form + Zod + shadcn)

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({ email: z.string().email(), password: z.string().min(8) });
type FormData = z.infer<typeof schema>;

function LoginForm() {
  const form = useForm<FormData>({ resolver: zodResolver(schema) });
  
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl><Input {...field} /></FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}

→ shadcn 의 Form / FormField 가 wrapper.

Theme (CSS variables)

/* globals.css */
:root {
  --primary: 222 47% 11%;
  --primary-foreground: 210 40% 98%;
  --background: 0 0% 100%;
  --foreground: 222 47% 11%;
}

.dark {
  --primary: 210 40% 98%;
  --primary-foreground: 222 47% 11%;
  --background: 222 47% 11%;
  --foreground: 210 40% 98%;
}
// tailwind.config.js
{
  theme: {
    extend: {
      colors: {
        primary: 'hsl(var(--primary))',
        background: 'hsl(var(--background))',
      },
    },
  },
}

→ Light / dark 가 CSS var 만.

Dark mode

import { ThemeProvider, useTheme } from 'next-themes';

<ThemeProvider attribute="class">
  <App />
</ThemeProvider>

function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return <Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>Toggle</Button>;
}

MUI / Chakra / Mantine 비교

MUI:
- Material Design
- 큰 ecosystem
- Bundle 큰
- 변경 어려움 (theme 만)

Chakra:
- Style props (sx={{...}})
- 작은 bundle
- 합리

Mantine:
- 깊은 component
- 좋은 hook
- TypeScript 친화

shadcn / Radix:
- Code own
- 변경 자유
- 작은 bundle
- 모던 default

→ Modern React = shadcn 가 default.

Bundle size

shadcn (Radix + Tailwind): only what 사용.
MUI: full library (3+ MB).

→ shadcn 가 더 작음.

Server component (RSC) 친화

// shadcn primitives 가 'use client' 명시.
// Server component 가 import 가능 (자동 client).

Storybook

// Button.stories.tsx
export default { component: Button };
export const Primary = { args: { variant: 'default' } };
export const Destructive = { args: { variant: 'destructive' } };

→ shadcn 의 component 가 component, storybook 친화.

A11y testing

import { axe } from 'jest-axe';

test('button is accessible', async () => {
  const { container } = render(<Button>Click</Button>);
  expect(await axe(container)).toHaveNoViolations();
});

Custom variant

const buttonVariants = cva('...', {
  variants: {
    variant: {
      // ... existing
      success: 'bg-green-500 text-white hover:bg-green-600',  // 추가
    },
  },
});

→ 기존 file 수정. Library upgrade 영향 X (own code).

Update strategy

# shadcn 의 새 version
npx shadcn@latest diff button
# → 변경점 보임

# Manual merge

→ Library upgrade 가 자동 X — but freedom.

🤔 의사결정 기준

상황 추천
Modern React shadcn/ui + Radix
빠른 prototype shadcn (모든 거 즉시)
깊이 customize Radix only
작은 bundle Headless UI / Radix
Multi-framework Ark UI
Material Design MUI
shadcn Form + RHF + Zod
Style props Chakra / Mantine

안티패턴

  • MUI + Tailwind: 2 css system 충돌.
  • Radix 없이 dialog 작성: a11y / focus / esc 깨짐.
  • shadcn 가 그대로 + customize 안: own code 의 가치 X.
  • CSS variable 없이 dark mode: class name 폭발.
  • A11y test 없음: keyboard / screen reader 깨짐.
  • Bundle analyze 없음: 큰 component lib 잠입.

🤖 LLM 활용 힌트

  • shadcn/ui = "code 가 own".
  • Radix = a11y primitive.
  • CVA / Tailwind Variants 가 variant 패턴.
  • React Aria / Headless UI / Ark UI = alternative.

🔗 관련 문서