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

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

createSimpleComponent 가 하는 일

/**
 * - 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 셀렉터로 표현 가능한 범위만).

참고