# Radio group

> Pick one option from a set, with a dot that hands off between choices and cards whose selection ring slides to the new pick.

Docs: https://hextaui.com/docs/radio-group
Markdown: https://hextaui.com/docs/radio-group.md

```tsx title="components/examples/radio-group/demo.tsx"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

const options = [
  { value: "all", label: "All new messages" },
  { value: "mentions", label: "Direct messages and mentions" },
  { value: "none", label: "Nothing" },
]

export function RadioGroupDemo() {
  return (
    <RadioGroup
      aria-label="Notify me about"
      defaultValue="mentions"
      className="w-fit"
    >
      {options.map((option) => (
        <label key={option.value} className="flex items-center gap-3 text-sm">
          <RadioGroupItem value={option.value} />
          {option.label}
        </label>
      ))}
    </RadioGroup>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/radio-group.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/radio-group.tsx"
"use client"

import * as React from "react"
import { Radio as RadioPrimitive } from "@base-ui/react/radio"
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
import { cn } from "cn"

import { useSlidingHighlight } from "@/lib/motion"

function mergeClassName<State>(
  base: string,
  className: string | ((state: State) => string | undefined) | undefined
) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

type RadioGroupVariant = "default" | "card"

const cardSelector =
  "[data-slot=radio-group-card]:has(> [data-slot=radio-group-item][data-checked])"

type RadioGroupProps<Value> = RadioGroupPrimitive.Props<Value> & {
  variant?: RadioGroupVariant
}

function RadioGroup<Value>({
  className,
  variant = "default",
  children,
  ref,
  ...props
}: RadioGroupProps<Value>) {
  const groupRef = React.useRef<HTMLDivElement | null>(null)
  const highlightRef = React.useRef<HTMLSpanElement | null>(null)
  useSlidingHighlight(groupRef, highlightRef, cardSelector, "data-checked")

  const setRef = React.useCallback(
    (node: HTMLDivElement | null) => {
      groupRef.current = node
      if (typeof ref === "function") {
        return ref(node)
      }
      if (ref) {
        ref.current = node
      }
      return undefined
    },
    [ref]
  )

  return (
    <RadioGroupPrimitive
      ref={setRef}
      className={mergeClassName(
        cn(
          "group/radio-group grid w-full min-w-0 gap-3",
          variant === "card" && "relative isolate gap-2"
        ),
        className
      )}
      {...props}
      data-slot="radio-group"
      data-variant={variant}
    >
      {variant === "card" ? (
        <span
          ref={highlightRef}
          aria-hidden="true"
          data-slot="radio-group-highlight"
          className="pointer-events-none absolute top-0 z-1 rounded-xl opacity-0 inset-ring-[1.5px] inset-ring-primary transition-[transform,width,height,opacity] duration-300 ease-out-quint data-instant:transition-opacity data-visible:opacity-100 motion-reduce:transition-opacity"
        />
      ) : null}
      {children}
    </RadioGroupPrimitive>
  )
}

const radioGroupItemClassName =
  "peer relative inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-background text-primary-foreground inset-ring-(length:--hairline) forced-colors:border inset-ring-muted-foreground/80 transition-[background-color,box-shadow,scale] duration-150 ease-out-quint outline-none focus-visible:outline-hidden select-none after:absolute after:-inset-2.5 pointer-coarse:after:-inset-3.5 hover:inset-ring-foreground/70 [@media(hover:hover)]:in-[label:hover]:inset-ring-foreground/70 focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring motion-safe:active:scale-90 motion-safe:in-[label:active]:scale-90 data-checked:bg-primary data-checked:inset-ring-primary data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:inset-ring-muted-foreground/80 data-disabled:[@media(hover:hover)]:in-[label:hover]:inset-ring-muted-foreground/80 motion-safe:data-disabled:active:scale-100 motion-safe:data-disabled:in-[label:active]:scale-100 data-readonly:cursor-default motion-safe:data-readonly:active:scale-100 motion-safe:data-readonly:in-[label:active]:scale-100 aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:inset-ring-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 data-invalid:inset-ring-destructive dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:aria-invalid:inset-ring-destructive/50 dark:data-invalid:ring-destructive/40 dark:data-invalid:inset-ring-destructive/50 data-checked:aria-invalid:inset-ring-primary data-checked:data-invalid:inset-ring-primary dark:data-checked:bg-primary"

function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
  return (
    <RadioPrimitive.Root
      className={mergeClassName(radioGroupItemClassName, className)}
      {...props}
      data-slot="radio-group-item"
    >
      <RadioPrimitive.Indicator
        keepMounted
        data-slot="radio-group-indicator"
        className="size-1.5 scale-0 rounded-full bg-current opacity-0 transition-[scale,opacity] duration-150 ease-out-quint motion-reduce:transition-none forced-colors:bg-canvas-text data-checked:scale-100 data-checked:opacity-100 data-checked:duration-300 data-checked:ease-spring"
      />
    </RadioPrimitive.Root>
  )
}

type RadioGroupCardProps = Omit<React.ComponentProps<"label">, "onChange"> &
  Pick<
    RadioPrimitive.Root.Props,
    "value" | "disabled" | "readOnly" | "required" | "inputRef" | "id"
  > & {
    radioClassName?: RadioPrimitive.Root.Props["className"]
  }

function RadioGroupCard({
  className,
  children,
  value,
  disabled,
  readOnly,
  required,
  inputRef,
  id,
  radioClassName,
  ...props
}: RadioGroupCardProps) {
  return (
    <label
      data-slot="radio-group-card"
      className={cn(
        "relative flex min-w-0 cursor-pointer items-start gap-3 rounded-xl bg-background p-4 text-sm inset-ring-(length:--hairline) inset-ring-border transition-[background-color,box-shadow] duration-200 ease-out-quint select-none has-focus-visible:ring-3 has-focus-visible:ring-focus-ring has-data-checked:bg-muted/50 has-data-checked:inset-ring-[1.5px] has-data-checked:inset-ring-primary group-has-[>[data-slot=radio-group-highlight][data-visible]]/radio-group:has-data-checked:inset-ring-(length:--hairline) group-has-[>[data-slot=radio-group-highlight][data-visible]]/radio-group:has-data-checked:inset-ring-border has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50 dark:bg-input/20 dark:has-data-checked:bg-muted/60 forced-colors:border [@media(hover:hover)]:hover:not-has-data-disabled:not-has-data-checked:bg-muted/30",
        className
      )}
      {...props}
    >
      <RadioGroupItem
        value={value}
        disabled={disabled}
        readOnly={readOnly}
        required={required}
        inputRef={inputRef}
        id={id}
        className={mergeClassName("mt-0.5", radioClassName)}
      />
      <span
        data-slot="radio-group-card-content"
        className="flex min-w-0 flex-1 flex-col gap-0.5 leading-snug wrap-break-word"
      >
        {children}
      </span>
    </label>
  )
}

function RadioGroupCardTitle({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="radio-group-card-title"
      className={cn("font-medium text-foreground", className)}
      {...props}
    />
  )
}

function RadioGroupCardDescription({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="radio-group-card-description"
      className={cn("text-muted-foreground", className)}
      {...props}
    />
  )
}

export {
  RadioGroup,
  RadioGroupCard,
  RadioGroupCardDescription,
  RadioGroupCardTitle,
  RadioGroupItem,
  radioGroupItemClassName,
}
export type { RadioGroupCardProps, RadioGroupProps, RadioGroupVariant }
```

```ts title="lib/motion.ts"
import * as React from "react"

const easeOut = "cubic-bezier(0.23, 1, 0.32, 1)"
const easeInOut = "cubic-bezier(0.77, 0, 0.175, 1)"
const easeSpring =
  "linear(0, 0.015 2%, 0.0532 4%, 0.1065 6%, 0.1686 8%, 0.2351 10%, 0.3363 13%, 0.4332 16%, 0.5495 20%, 0.648 24%, 0.7287 28%, 0.807 33%, 0.8646 38%, 0.9128 44%, 0.9447 50%, 0.968 57%, 0.9834 65%, 0.9924 74%, 0.9975 85%, 1)"

const duration = {
  press: 100,
  release: 200,
  hover: 150,
  enter: 200,
  exit: 150,
  morph: 300,
} as const

function prefersReducedMotion() {
  return (
    typeof window === "undefined" ||
    typeof window.matchMedia !== "function" ||
    window.matchMedia("(prefers-reduced-motion: reduce)").matches
  )
}

type SizeAxis = "width" | "height"

type SizeMorphOptions = {
  axis: SizeAxis
  enabled?: boolean
  duration?: number
  easing?: string
}

function readSize(element: HTMLElement, axis: SizeAxis) {
  const value = parseFloat(getComputedStyle(element)[axis])
  if (Number.isFinite(value)) {
    return value
  }
  const rect = element.getBoundingClientRect()
  return axis === "width" ? rect.width : rect.height
}

function attachSizeMorph(
  element: HTMLElement,
  axis: SizeAxis,
  time: number,
  easing: string
) {
  if (
    typeof MutationObserver === "undefined" ||
    typeof element.animate !== "function"
  ) {
    return undefined
  }

  let settled = readSize(element, axis)
  let animation: Animation | null = null

  const morph = () => {
    const from = animation ? readSize(element, axis) : settled
    animation?.cancel()
    animation = null
    const to = readSize(element, axis)
    settled = to

    if (
      Math.abs(from - to) < 0.5 ||
      prefersReducedMotion() ||
      !element.isConnected ||
      element.getClientRects().length === 0
    ) {
      element.removeAttribute("data-morphing")
      return
    }

    element.setAttribute("data-morphing", "")
    const running = element.animate(
      [{ [axis]: `${from}px` }, { [axis]: `${to}px` }],
      { duration: time, easing }
    )
    animation = running
    running.onfinish = () => {
      if (animation === running) {
        animation = null
        element.removeAttribute("data-morphing")
        settled = readSize(element, axis)
      }
    }
  }

  const mutations = new MutationObserver(morph)
  mutations.observe(element, {
    childList: true,
    subtree: true,
    characterData: true,
  })

  const resize =
    typeof ResizeObserver === "undefined"
      ? null
      : new ResizeObserver(() => {
          if (!animation) {
            settled = readSize(element, axis)
          }
        })
  resize?.observe(element)

  return () => {
    mutations.disconnect()
    resize?.disconnect()
    animation?.cancel()
    element.removeAttribute("data-morphing")
  }
}

function useSizeMorph<T extends HTMLElement>({
  axis,
  enabled = true,
  duration: time = duration.morph,
  easing = easeOut,
}: SizeMorphOptions): React.RefCallback<T> {
  return React.useCallback(
    (element: T | null) => {
      if (!element || !enabled) {
        return undefined
      }
      return attachSizeMorph(element, axis, time, easing)
    },
    [axis, enabled, time, easing]
  )
}

function useSlidingHighlight(
  barRef: React.RefObject<HTMLElement | null>,
  highlightRef: React.RefObject<HTMLElement | null>,
  selector: string,
  attribute = "data-popup-open"
) {
  React.useLayoutEffect(() => {
    const bar = barRef.current
    const highlight = highlightRef.current
    if (!bar || !highlight) {
      return
    }

    let current: HTMLElement | null = null

    const place = (trigger: HTMLElement, instant: boolean) => {
      if (instant || prefersReducedMotion()) {
        highlight.setAttribute("data-instant", "")
      } else {
        highlight.removeAttribute("data-instant")
      }
      const frame = bar.getBoundingClientRect()
      const box = trigger.getBoundingClientRect()
      const scale = bar.offsetWidth > 0 ? frame.width / bar.offsetWidth : 1
      highlight.style.left = "0px"
      highlight.style.width = `${box.width / scale}px`
      highlight.style.height = `${box.height / scale}px`
      highlight.style.transform = `translate(${(box.left - frame.left) / scale - bar.clientLeft}px, ${(box.top - frame.top) / scale - bar.clientTop}px)`
    }

    const sync = () => {
      const trigger = bar.querySelector<HTMLElement>(selector)
      if (trigger === current) {
        if (trigger) {
          place(trigger, true)
        }
        return
      }
      const appearing = current === null
      current = trigger
      if (!trigger) {
        highlight.removeAttribute("data-visible")
        return
      }
      place(trigger, appearing)
      if (appearing) {
        void highlight.offsetWidth
      }
      highlight.setAttribute("data-visible", "")
    }

    sync()
    const mutations = new MutationObserver(sync)
    mutations.observe(bar, {
      subtree: true,
      childList: true,
      attributes: true,
      attributeFilter: [attribute],
    })
    const resize =
      typeof ResizeObserver === "undefined"
        ? null
        : new ResizeObserver(() => {
            if (current) {
              place(current, true)
            }
          })
    resize?.observe(bar)
    const onScroll = () => {
      if (current) {
        place(current, true)
      }
    }
    bar.addEventListener("scroll", onScroll, { capture: true, passive: true })

    return () => {
      mutations.disconnect()
      resize?.disconnect()
      bar.removeEventListener("scroll", onScroll, { capture: true })
    }
  }, [barRef, highlightRef, selector, attribute])
}

export {
  duration,
  easeInOut,
  easeOut,
  easeSpring,
  prefersReducedMotion,
  useSizeMorph,
  useSlidingHighlight,
}
export type { SizeMorphOptions }
```

Update the import paths to match your project setup.

## Usage

```tsx
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
```

```tsx
<RadioGroup aria-label="Notify me about" defaultValue="mentions">
  <label className="flex items-center gap-3">
    <RadioGroupItem value="all" />
    All new messages
  </label>
  <label className="flex items-center gap-3">
    <RadioGroupItem value="mentions" />
    Direct messages and mentions
  </label>
</RadioGroup>
```

When the choice changes, the old dot shrinks away as the new one springs in, so the eye follows the pick instead of two things blinking at once. Wrap each item in a `<label>` so the whole row is clickable.

## Composition

```text
RadioGroup
├── RadioGroupItem
└── RadioGroupCard
    ├── RadioGroupCardTitle
    └── RadioGroupCardDescription
```

## Examples

### Cards

With `variant="card"`, use `RadioGroupCard` for options with more to say. The selection ring slides to the new card while its surface fades in.

```tsx title="components/examples/radio-group/cards.tsx"
import {
  RadioGroup,
  RadioGroupCard,
  RadioGroupCardDescription,
  RadioGroupCardTitle,
} from "@/components/ui/radio-group"

const plans = [
  {
    value: "hobby",
    title: "Hobby",
    price: "Free",
    description: "For side projects and trying things out.",
  },
  {
    value: "pro",
    title: "Pro",
    price: "$20/mo",
    description: "Unlimited projects, analytics and priority builds.",
  },
  {
    value: "team",
    title: "Team",
    price: "$60/mo",
    description: "Everything in Pro, plus roles and audit logs.",
  },
]

export function RadioGroupCards() {
  return (
    <RadioGroup
      variant="card"
      aria-label="Plan"
      defaultValue="pro"
      className="max-w-md"
    >
      {plans.map((plan) => (
        <RadioGroupCard key={plan.value} value={plan.value}>
          <span className="flex items-baseline justify-between gap-3">
            <RadioGroupCardTitle>{plan.title}</RadioGroupCardTitle>
            <span className="text-muted-foreground tabular-nums">
              {plan.price}
            </span>
          </span>
          <RadioGroupCardDescription>
            {plan.description}
          </RadioGroupCardDescription>
        </RadioGroupCard>
      ))}
    </RadioGroup>
  )
}
```

### Horizontal

Cards can sit in a row or any grid. The ring follows them wherever they are.

```tsx title="components/examples/radio-group/horizontal.tsx"
import {
  RadioGroup,
  RadioGroupCard,
  RadioGroupCardTitle,
} from "@/components/ui/radio-group"

const sizes = ["S", "M", "L", "XL"]

export function RadioGroupHorizontal() {
  return (
    <RadioGroup
      variant="card"
      aria-label="Size"
      defaultValue="M"
      className="w-full max-w-sm grid-cols-2 sm:grid-cols-4"
    >
      {sizes.map((size) => (
        <RadioGroupCard key={size} value={size}>
          <RadioGroupCardTitle>{size}</RadioGroupCardTitle>
        </RadioGroupCard>
      ))}
    </RadioGroup>
  )
}
```

### Form

Inside `Field` and `FieldSet`, the group gets a legend, required validation and an error message.

```tsx title="components/examples/radio-group/form.tsx"
"use client"

import * as React from "react"
import { Form } from "@base-ui/react/form"

import { Button } from "@/components/ui/button"
import {
  Field,
  FieldError,
  FieldItem,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

export function RadioGroupForm() {
  const [submitted, setSubmitted] = React.useState<string>()

  return (
    <Form
      className="flex w-full max-w-xs flex-col items-start gap-4"
      onSubmit={(event) => {
        event.preventDefault()
        setSubmitted(String(new FormData(event.currentTarget).get("shipping")))
      }}
    >
      <Field name="shipping">
        <FieldSet render={<RadioGroup required />}>
          <FieldLegend variant="label">Shipping</FieldLegend>
          {["Standard", "Express", "Overnight"].map((option) => (
            <FieldItem key={option}>
              <RadioGroupItem value={option.toLowerCase()} />
              <FieldLabel>{option}</FieldLabel>
            </FieldItem>
          ))}
        </FieldSet>
        <FieldError match="valueMissing">Choose a shipping speed.</FieldError>
      </Field>
      <Button type="submit" size="sm">
        Continue
      </Button>
      <output className="text-sm text-muted-foreground">
        {submitted ? `shipping=${submitted}` : "Nothing submitted yet."}
      </output>
    </Form>
  )
}
```

### Disabled and read-only

Disable a single option, or make the whole group `readOnly` to show a value that can't change.

```tsx title="components/examples/radio-group/states.tsx"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

export function RadioGroupStates() {
  return (
    <div className="flex flex-wrap gap-12 text-sm">
      <RadioGroup aria-label="Theme" defaultValue="system" className="w-fit">
        <label className="flex items-center gap-3">
          <RadioGroupItem value="light" />
          Light
        </label>
        <label className="flex items-center gap-3">
          <RadioGroupItem value="dark" disabled />
          Dark (coming soon)
        </label>
        <label className="flex items-center gap-3">
          <RadioGroupItem value="system" />
          System
        </label>
      </RadioGroup>
      <RadioGroup
        aria-label="Region"
        defaultValue="eu"
        readOnly
        className="w-fit"
      >
        <label className="flex items-center gap-3">
          <RadioGroupItem value="us" />
          United States
        </label>
        <label className="flex items-center gap-3">
          <RadioGroupItem value="eu" />
          Europe (locked)
        </label>
      </RadioGroup>
    </div>
  )
}
```

### Controlled

Pass `value` and `onValueChange`. Outside changes animate the same way as clicks.

```tsx title="components/examples/radio-group/controlled.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

const speeds = ["0.5×", "1×", "1.5×", "2×"]

export function RadioGroupControlled() {
  const [speed, setSpeed] = React.useState("1×")

  return (
    <div className="flex flex-col items-start gap-4 text-sm">
      <RadioGroup
        aria-label="Playback speed"
        value={speed}
        onValueChange={setSpeed}
        className="flex w-fit flex-wrap"
      >
        {speeds.map((value) => (
          <label key={value} className="flex items-center gap-2 tabular-nums">
            <RadioGroupItem value={value} />
            {value}
          </label>
        ))}
      </RadioGroup>
      <Button
        variant="outline"
        size="sm"
        onClick={() =>
          setSpeed(speeds[(speeds.indexOf(speed) + 1) % speeds.length])
        }
      >
        Next speed
      </Button>
    </div>
  )
}
```

### Right to left

Radios and cards follow the reading direction.

```tsx title="components/examples/radio-group/rtl.tsx"
import {
  RadioGroup,
  RadioGroupCard,
  RadioGroupCardDescription,
  RadioGroupCardTitle,
} from "@/components/ui/radio-group"

export function RadioGroupRtl() {
  return (
    <div dir="rtl" className="w-full max-w-sm">
      <RadioGroup variant="card" aria-label="طريقة الدفع" defaultValue="card">
        <RadioGroupCard value="card">
          <RadioGroupCardTitle>بطاقة ائتمان</RadioGroupCardTitle>
          <RadioGroupCardDescription>
            فيزا أو ماستركارد.
          </RadioGroupCardDescription>
        </RadioGroupCard>
        <RadioGroupCard value="cash">
          <RadioGroupCardTitle>الدفع عند الاستلام</RadioGroupCardTitle>
          <RadioGroupCardDescription>
            ادفع نقدًا عند التوصيل.
          </RadioGroupCardDescription>
        </RadioGroupCard>
      </RadioGroup>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Tab` | Moves focus into the group, onto the picked option or the first one. |
| `↑` `↓` `←` `→` | Moves to the previous or next option and picks it, wrapping at the ends. |
| `Space` | Picks the focused option. |

## Accessibility

- Name the group with `aria-label`, `aria-labelledby` or a `FieldLegend`.
- Each radio has a 36px hit area, and 44px on touch screens, while staying 16px on screen.
- The dot and card ring don't move with reduced motion.

## API reference

### RadioGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "card"` | `"default"` | card adds the sliding selection ring. |
| `value` | `Value` | – |  |
| `defaultValue` | `Value` | – |  |
| `onValueChange` | `(value, details) => void` | – |  |
| `name` | `string` | – |  |
| `required` | `boolean` | `false` |  |
| `disabled` | `boolean` | `false` |  |
| `readOnly` | `boolean` | `false` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="radio-group"` | The group, with data-variant. |
| `data-slot="radio-group-highlight"` | The ring that slides between cards. |
| `data-disabled` | The group is disabled. |

### RadioGroupItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Value` | – |  |
| `disabled` | `boolean` | `false` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<span>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="radio-group-item"` | The radio. |
| `data-checked / data-unchecked` | Whether it's the picked option. |
| `data-disabled / data-readonly` | Present when it can't be changed. |

### RadioGroupCard

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Value` | – |  |
| `disabled` | `boolean` | `false` |  |
| `radioClassName` | `string` | – | Classes for the radio inside the card. |

A `<label>` with the radio built in. Put `RadioGroupCardTitle` and `RadioGroupCardDescription`, or anything else, inside.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
