HextaUI

Radio group

Elige una opción de un conjunto, con un punto que pasa de una opción a otra y tarjetas cuyo anillo de selección se desliza hasta la nueva elección.

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>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/radio-group.json

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
<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>

Cuando cambia la elección, el punto anterior se encoge mientras el nuevo aparece con un spring, de modo que la vista sigue la elección en lugar de ver dos cosas parpadear a la vez. Envuelve cada elemento en un <label> para que toda la fila sea clicable.

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

Tarjetas

Con variant="card", usa RadioGroupCard para opciones con más que decir. El anillo de selección se desliza hasta la nueva tarjeta mientras su superficie aparece con un fundido.

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

Las tarjetas pueden ir en una fila o en cualquier cuadrícula. El anillo las sigue estén donde estén.

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

Dentro de Field y FieldSet, el grupo recibe una leyenda, validación de obligatorio y un mensaje de error.

"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>
  )
}

Deshabilitado y de solo lectura

Deshabilita una sola opción, o pon todo el grupo en readOnly para mostrar un valor que no puede cambiar.

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>
  )
}

Controlado

Pasa value y onValueChange. Los cambios externos se animan igual que los clics.

"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>
  )
}

De derecha a izquierda

Los radios y las tarjetas siguen la dirección de lectura.

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>
  )
}
KeyAcción
TabMueve el foco al grupo, sobre la opción elegida o la primera.
↑↓←→Pasa a la opción anterior o siguiente y la elige, dando la vuelta en los extremos.
SpaceElige la opción enfocada.
  • Da nombre al grupo con aria-label, aria-labelledby o un FieldLegend.
  • Cada radio tiene un área de toque de 36px, y de 44px en pantallas táctiles, aunque se ve de 16px.
  • El punto y el anillo de la tarjeta no se mueven con movimiento reducido.
PropTipoPredeterminado
variantcard añade el anillo de selección deslizante.
"default" | "card""default"
value
Value–
defaultValue
Value–
onValueChange
(value, details) => void–
name
string–
required
booleanfalse
disabled
booleanfalse
readOnly
booleanfalse
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="radio-group"El grupo, con data-variant.
data-slot="radio-group-highlight"El anillo que se desliza entre tarjetas.
data-disabledEl grupo está deshabilitado.
PropTipoPredeterminado
value
Value–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement<span>
AtributoDescripción
data-slot="radio-group-item"El radio.
data-checked / data-uncheckedSi es la opción elegida.
data-disabled / data-readonlyPresente cuando no se puede cambiar.
PropTipoPredeterminado
value
Value–
disabled
booleanfalse
radioClassNameClases para el radio dentro de la tarjeta.
string–

Un <label> con el radio integrado. Pon dentro RadioGroupCardTitle y RadioGroupCardDescription, o cualquier otra cosa.