Ir al contenido
uiify

Radio group

Grupo de botones de opción nativos con foco de teclado móvil.

Navegadores
Chrome 123+Firefox 121+Safari 17.5+
Nivel
0
Tamaño comprimido
946 B
Presupuesto de diseño forzado
0 ms
Presupuesto de axe
0
Compatible con SSR
Sí
Basado en
elements/choice
Contrato publicado
Entorno de pruebas

Zona de pruebas

Radio group

Predeterminado

import { RadioGroup } from "@gunkin/uiify";

export default function Example() {
  return (
    <RadioGroup
      className=""
      id="plan"
      aria-label="Billing plan"
      defaultValue="pro"
      items={[
        { value: "free", label: "Free", className: "" },
        { value: "pro", label: "Pro", className: "" },
      ]}
    />
  );
}
/* Checkbox, Radio, Switch — all native <input>s styled with appearance:none.
   State comes from :checked / :indeterminate (works uncontrolled) and the
   [data-state] / [data-disabled] attributes (set when controlled). Thumbs and
   checkmarks are pseudo-elements on the input itself. */

@layer components {
  :where([data-uiify-checkbox] > input, [data-uiify-radio]) {
    appearance: none;
    inline-size: 1.25rem;
    block-size: 1.25rem;
    margin: 0;
    border: var(--border-width) solid var(--input);
    background-color: var(--card);
    display: inline-grid;
    place-content: center;
    cursor: pointer;
    transition: var(--transition-colors);
  }

  [data-uiify-checkbox] {
    --uiify-checkbox-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.6 8.6l3 3L12.4 4.8' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")
      center / 100% 100% no-repeat;

    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    cursor: pointer;
    user-select: none;
    color: var(--foreground);
    font-size: var(--text-sm);
    line-height: var(--leading-normal);
  }

  [data-uiify-checkbox] > input {
    border-radius: var(--radius-xs);
    corner-shape: squircle;
  }

  [data-uiify-radio] {
    border-radius: var(--radius-full);
  }

  /* Checked fill (covers controlled [data-state] and uncontrolled :checked). */
  [data-uiify-checkbox] > input:checked,
  [data-uiify-checkbox] > input[data-state="checked"] {
    border-color: var(--primary);
    background-color: var(--primary);
  }

  /* Radio checked state is a thick accent ring around a white center, not a
     filled dot — matches the 2a/2b handoff's target/bullseye radio anatomy. */
  [data-uiify-radio]:checked,
  [data-uiify-radio][data-state="checked"] {
    border-width: 5px;
    border-color: var(--primary);
    background-color: var(--card);
  }

  /* Checkmark. */
  [data-uiify-checkbox] > input::before {
    content: "";
    inline-size: 1rem;
    block-size: 1rem;
    transform: scale(0);
    transition: transform var(--duration-fast) var(--ease-out);
    background-color: var(--primary-foreground);
    mask: var(--uiify-checkbox-mask);
  }

  [data-uiify-checkbox] > input:checked::before,
  [data-uiify-checkbox] > input[data-state="checked"]::before {
    transform: scale(1);
  }

  /* Indeterminate dash. */
  [data-uiify-checkbox] > input:indeterminate,
  [data-uiify-checkbox] > input[data-state="indeterminate"] {
    border-color: var(--primary);
    background-color: var(--primary);
  }

  [data-uiify-checkbox] > input:indeterminate::before,
  [data-uiify-checkbox] > input[data-state="indeterminate"]::before {
    transform: scale(1);
    mask: none;
    clip-path: inset(42% 12% 42% 12%);
  }

  :where([data-uiify-checkbox] > input, [data-uiify-radio]):disabled,
  :where([data-uiify-checkbox] > input, [data-uiify-radio])[data-disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  [data-uiify-checkbox]:has(> input:disabled) {
    opacity: 0.5;
    cursor: not-allowed;
  }

  [data-uiify-radio-group] {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
  }

  /* Switch — a pill track with a sliding thumb pseudo-element. */
  [data-uiify-switch] {
    appearance: none;
    position: relative;
    inline-size: 2.25rem;
    block-size: 1.25rem;
    margin: 0;
    flex: none;
    border: none;
    border-radius: var(--radius-full);
    background-color: var(--input);
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-out);
  }

  [data-uiify-switch]::before {
    content: "";
    position: absolute;
    inset-block-start: 2px;
    inset-inline-start: 2px;
    inline-size: calc(1.25rem - 4px);
    block-size: calc(1.25rem - 4px);
    border-radius: var(--radius-full);
    background-color: var(--gray-0);
    box-shadow: var(--shadow-1);
    transition: translate var(--duration-fast) var(--ease-out);
  }

  [data-uiify-switch]:checked,
  [data-uiify-switch][data-state="checked"] {
    background-color: var(--primary);
  }

  [data-uiify-switch]:checked::before,
  [data-uiify-switch][data-state="checked"]::before {
    translate: 1rem 0;
  }

  [data-uiify-switch]:disabled,
  [data-uiify-switch][data-disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }
}

@layer components {
  label:has([data-uiify-radio]),
  [data-uiify-radio-group] label {
    user-select: none;
    cursor: pointer;
  }
}
import { RadioGroup } from "@gunkin/uiify";

export default function Example() {
  return (
    <RadioGroup
      className="
        flex flex-col gap-md p-md data-[orientation=horizontal]:flex-row [&>[data-part=item]]:flex
        [&>[data-part=item]]:items-center [&>[data-part=item]]:gap-sm [&_[data-part=item]>label]:flex-auto
        [&_[data-part=item]>label]:cursor-pointer [&_[data-part=item]>label]:select-none
      "
      id="plan"
      aria-label="Billing plan"
      defaultValue="pro"
      items={[
        {
          value: "free",
          label: "Free",
          className:
            "appearance-none inline-grid size-5 flex-none place-content-center m-0 " +
            "border-[length:var(--border-width)] border-solid border-input rounded-full " +
            "bg-card cursor-pointer [transition:var(--transition-colors)] " +
            "checked:border-[5px] checked:border-primary data-[state=checked]:border-[5px] " +
            "data-[state=checked]:border-primary focus-visible:outline-none " +
            "focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_16%,_transparent)] " +
            "disabled:opacity-50 disabled:cursor-not-allowed data-[disabled]:opacity-50 " +
            "data-[disabled]:cursor-not-allowed",
        },
        {
          value: "pro",
          label: "Pro",
          className:
            "appearance-none inline-grid size-5 flex-none place-content-center m-0 " +
            "border-[length:var(--border-width)] border-solid border-input rounded-full " +
            "bg-card cursor-pointer [transition:var(--transition-colors)] " +
            "checked:border-[5px] checked:border-primary data-[state=checked]:border-[5px] " +
            "data-[state=checked]:border-primary focus-visible:outline-none " +
            "focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_16%,_transparent)] " +
            "disabled:opacity-50 disabled:cursor-not-allowed data-[disabled]:opacity-50 " +
            "data-[disabled]:cursor-not-allowed",
        },
      ]}
    />
  );
}
/* app.css */

@layer reset, ui, app, tokens, base, demo, components, utilities, theme;

@import "@gunkin/uiify/styles/reset";

@import "@gunkin/uiify/components/behavior.css";

@import "@gunkin/uiify/styles/tokens";

@import "@gunkin/uiify/styles/tailwind";

@import "tailwindcss/utilities.css";

@theme { --spacing: 0.25rem; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; }

@source inline('flex flex-col gap-md p-md data-[orientation=horizontal]:flex-row [&>[data-part=item]]:flex [&>[data-part=item]]:items-center [&>[data-part=item]]:gap-sm [&_[data-part=item]>label]:flex-auto [&_[data-part=item]>label]:cursor-pointer [&_[data-part=item]>label]:select-none appearance-none inline-grid size-5 flex-none place-content-center m-0 border-[length:var(--border-width)] border-solid border-input rounded-full bg-card cursor-pointer [transition:var(--transition-colors)] checked:border-[5px] checked:border-primary data-[state=checked]:border-[5px] data-[state=checked]:border-primary focus-visible:outline-none focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_16%,_transparent)] disabled:opacity-50 disabled:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed');

@layer base { :root { --uiify-control-text: var(--text-sm); } @media (pointer: coarse) { :root { --uiify-control-text: var(--text-base); } } }

/* Authored animation and registered properties used by the Tailwind skin.
 * Visual rules belong in utility recipes; only at-rule definitions live here. */
@layer components {
  @keyframes uiify-presence-in {
    from {
      opacity: 0;
      scale: var(--presence-scale);
    }
  }
}

@layer components {
  @keyframes uiify-presence-out {
    to {
      opacity: 0;
      scale: var(--presence-scale);
    }
  }
}

@property --uiify-spinner-tail {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --uiify-spinner-length {
  syntax: "<angle>";
  inherits: false;
  initial-value: 36deg;
}

@layer components {
  @keyframes uiify-spinner-rotate {
    to {
      transform: rotate(360deg);
    }
  }
}

@layer components {
  @keyframes uiify-spinner-orbit {
    0% {
      --uiify-spinner-tail: 0deg;
      --uiify-spinner-length: 36deg;
    }

    50% {
      --uiify-spinner-tail: 90deg;
      --uiify-spinner-length: 324deg;
    }

    100% {
      --uiify-spinner-tail: 360deg;
      --uiify-spinner-length: 36deg;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-pulse {
    50% {
      opacity: 0.45;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-shimmer {
    to {
      background-position: -200% 0;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-reveal {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }
}

@property --candle-body-height {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-body-top {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --candle-slot {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 100%;
}

@property --candle-wick-height {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-wick-top {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --candle-x {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-scale {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

@property --line-y {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --line-prev-y {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --line-count {
  syntax: "<integer>";
  inherits: false;
  initial-value: 1;
}

@property --line-scale {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

@layer components {
  @keyframes uiify-otp-input-caret-blink {
    50% {
      opacity: 0;
    }
  }
}

API

RadioGroup

PropTipoPredeterminadoDescripción
id*string—Prefijo de id para el grupo y para cada input de botón de opción generado.
items*RadioGroupItem[]—Opciones que se renderizan; el grupo se construye a partir de datos, no mediante partes compuestas.
defaultValuestring—Valor marcado en el primer renderizado; luego el navegador gestiona la selección, por lo que el grupo funciona sin JavaScript.
namestring—Nombre de campo de formulario compartido por todos los botones de opción del grupo.
disabledbooleanfalseDesactiva todos los botones de opción del grupo.
orientation"horizontal" | "vertical""vertical"Dirección de la distribución que se comunica a la tecnología de asistencia y se usa para aplicar estilos.
classNamestring—Clase aplicada al elemento del grupo.
aria-labelstring—Nombre accesible del grupo de botones de opción.

RadioGroupItem

PropTipoPredeterminadoDescripción
value*string—Valor que se envía cuando se selecciona este botón de opción.
label*ReactNode—Contenido de etiqueta renderizado junto a este botón de opción.
disabledboolean—Desactiva solo este botón de opción.
classNamestring—Clase aplicada a la envoltura de este elemento.

Teclado

TeclaAcción
TabLlevar el foco al grupo
Arrow keysMover el foco y seleccionar entre opciones

Accesibilidad

Botones de opción nativos en un grupo con foco móvil.

Puntos de estilo

data-statedata-disabled
Esta página se tradujo automáticamente y aún no se ha revisado.