---
type: snippet
tags:
  [
    'css-attribute-selector',
    'prop-as-attribute',
    'headless-ui',
    'preact',
    'cva',
    'declarative-markup',
  ]
status: release
ctime: 2026-06-03
mtime: 2026-06-03
---

컴포넌트는 className을 안 만든다 — prop을 그대로 DOM 어트리뷰트로 흘리고, variant 분기는 CSS 어트리뷰트 셀렉터가 담당한다.

```tsx
// 컴포넌트 전체 = h(tag, { ...props, k: name }) 한 줄
export const Badge = createSimpleComponent<'span', BadgeOwnProps>(
  'badge',
  'span',
)
// → <span k="badge" variant="secondary">   (variant가 class가 아니라 attr로 그대로 나감)
```

```css
[k="badge"] { /* default */ }
[k="badge"][variant="secondary"] { … }
[k="badge"][variant="destructive"]:hover { … }
```

## createSimpleComponent 가 하는 일

```ts
/**
 * - defaults를 props 아래에 깔고
 * - k={name} 할당하고
 * - h(tag, normalizedProps)
 */
createSimpleComponent<T, P>(name, tag)
```

- `k?: never` 로 소비자가 셀렉터 키 `k` 를 덮어쓰는 걸 **타입 레벨**에서 차단.
- `Props = Omit<JSX.IntrinsicElements[T], keyof P> & P` → own prop이 동명 intrinsic 어트리뷰트를 덮어씀 (Badge의 `variant` 가 우선).
- `tag` 를 `(props) => T` 함수로도 받아 폴리모픽 (`href` 있으면 `<a>`, 없으면 `<button>`). 단 분기에 들어가는 건 defaults 적용 _전_ 원본 props.

대가: 런타임 동적 variant 계산·조건부 로직은 포기 (CSS 셀렉터로 표현 가능한 범위만).

## 참고

- [developit/kinu](https://github.com/developit/kinu)
