Saltar al contenido
VEXA
Formularios

Select

Hoja cliente

A single-choice control backed by a listbox.

Vista previa en vivo

Renderizado desde el código fuente instalado de VEXA — los mismos archivos que recibes.

Escritorio

Instalación

Añade el código del componente a tu proyecto. Las dependencias del registry se resuelven automáticamente.

npx shadcn@latest add https://vexa.valfiguer.com/r/select.json

Se instala en components/vexa/select.tsx

Dependencias npm

  • @radix-ui/react-select

Dependencias del registry

  • @vexa/utils

Otros items de VEXA que necesita este componente. Instálalos primero — la CLI los resuelve automáticamente.

Código

Cópialo a su ruta destino, o instálalo con el comando de arriba.

components/vexa/select.tsx
"use client";

import type * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";

import { cn } from "@/lib/utils";

function ChevronDownIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      aria-hidden="true"
      className={className}
    >
      <path
        d="m6 9 6 6 6-6"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function ChevronUpIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      aria-hidden="true"
      className={className}
    >
      <path
        d="m6 15 6-6 6 6"
        stroke="currentColor"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      aria-hidden="true"
      className={className}
    >
      <path
        d="M4 12.5 9 17.5 20 6.5"
        stroke="currentColor"
        strokeWidth="2.5"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

/**
 * Select — a single-choice control backed by a listbox.
 *
 * Client leaf. Radix provides the APG listbox model: type-ahead, Arrow/Home/End
 * navigation, Escape to close, and correct `combobox`/`listbox`/`option` roles
 * with `aria-selected`. Pair the `SelectTrigger` with a `<Label>` (or
 * `aria-label`). For long, free-text-filtered lists prefer the CommandMenu.
 */
export function Select(
  props: React.ComponentProps<typeof SelectPrimitive.Root>,
) {
  return <SelectPrimitive.Root data-slot="select" {...props} />;
}

export function SelectGroup(
  props: React.ComponentProps<typeof SelectPrimitive.Group>,
) {
  return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}

export function SelectValue(
  props: React.ComponentProps<typeof SelectPrimitive.Value>,
) {
  return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}

export function SelectTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger>) {
  return (
    <SelectPrimitive.Trigger
      data-slot="select-trigger"
      className={cn(
        "flex h-10 w-full items-center justify-between gap-2 rounded-vexa-md border border-vexa-input bg-vexa-surface-raised px-3 py-2 text-sm text-vexa-foreground shadow-vexa-sm",
        "transition-[color,box-shadow,border-color] duration-[var(--vexa-duration-fast)]",
        "data-[placeholder]:text-vexa-muted-foreground",
        "outline-none focus-visible:border-vexa-focus focus-visible:ring-2 focus-visible:ring-vexa-ring focus-visible:ring-offset-1 focus-visible:ring-offset-vexa-background",
        "disabled:cursor-not-allowed disabled:opacity-60",
        "aria-[invalid=true]:border-vexa-danger aria-[invalid=true]:focus-visible:ring-vexa-danger",
        "[&_svg]:size-4 [&_svg]:shrink-0",
        className,
      )}
      {...props}
    >
      {children}
      <SelectPrimitive.Icon asChild>
        <ChevronDownIcon className="size-4 text-vexa-muted-foreground" />
      </SelectPrimitive.Icon>
    </SelectPrimitive.Trigger>
  );
}

export function SelectScrollUpButton({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
  return (
    <SelectPrimitive.ScrollUpButton
      data-slot="select-scroll-up-button"
      className={cn(
        "flex cursor-default items-center justify-center py-1",
        className,
      )}
      {...props}
    >
      <ChevronUpIcon className="size-4" />
    </SelectPrimitive.ScrollUpButton>
  );
}

export function SelectScrollDownButton({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
  return (
    <SelectPrimitive.ScrollDownButton
      data-slot="select-scroll-down-button"
      className={cn(
        "flex cursor-default items-center justify-center py-1",
        className,
      )}
      {...props}
    >
      <ChevronDownIcon className="size-4" />
    </SelectPrimitive.ScrollDownButton>
  );
}

export function SelectContent({
  className,
  children,
  position = "popper",
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
  return (
    <SelectPrimitive.Portal>
      <SelectPrimitive.Content
        data-slot="select-content"
        position={position}
        className={cn(
          "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-vexa-md border border-vexa-border bg-vexa-surface-raised text-vexa-foreground shadow-vexa-lg",
          "transition-[opacity,transform] duration-[var(--vexa-duration-fast)] ease-[var(--vexa-ease-standard)] motion-reduce:transition-none",
          "data-[state=closed]:opacity-0 data-[state=open]:opacity-100",
          position === "popper" &&
            "data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1",
          className,
        )}
        {...props}
      >
        <SelectScrollUpButton />
        <SelectPrimitive.Viewport
          className={cn(
            "p-1",
            position === "popper" &&
              "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
          )}
        >
          {children}
        </SelectPrimitive.Viewport>
        <SelectScrollDownButton />
      </SelectPrimitive.Content>
    </SelectPrimitive.Portal>
  );
}

export function SelectLabel({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
  return (
    <SelectPrimitive.Label
      data-slot="select-label"
      className={cn(
        "px-2 py-1.5 text-xs font-medium text-vexa-muted-foreground",
        className,
      )}
      {...props}
    />
  );
}

export function SelectItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
  return (
    <SelectPrimitive.Item
      data-slot="select-item"
      className={cn(
        "relative flex w-full cursor-default select-none items-center rounded-vexa-sm py-1.5 pl-8 pr-2 text-sm outline-none",
        "focus:bg-vexa-accent focus:text-vexa-accent-foreground",
        "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
        className,
      )}
      {...props}
    >
      <span className="absolute left-2 flex size-4 items-center justify-center">
        <SelectPrimitive.ItemIndicator>
          <CheckIcon className="size-4" />
        </SelectPrimitive.ItemIndicator>
      </span>
      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
    </SelectPrimitive.Item>
  );
}

export function SelectSeparator({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
  return (
    <SelectPrimitive.Separator
      data-slot="select-separator"
      className={cn("-mx-1 my-1 h-px bg-vexa-border", className)}
      {...props}
    />
  );
}
JSON del registryr/select.json
r/select.json
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select",
  "type": "registry:ui",
  "title": "Select",
  "description": "A single-choice control backed by a listbox.",
  "dependencies": [
    "@radix-ui/react-select"
  ],
  "registryDependencies": [
    "@vexa/utils"
  ],
  "files": [
    {
      "path": "registry/default/ui/select.tsx",
      "type": "registry:ui",
      "target": "components/vexa/select.tsx",
      "content": "\"use client\";\n\nimport type * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction ChevronDownIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n      className={className}\n    >\n      <path\n        d=\"m6 9 6 6 6-6\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction ChevronUpIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n      className={className}\n    >\n      <path\n        d=\"m6 15 6-6 6 6\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction CheckIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n      className={className}\n    >\n      <path\n        d=\"M4 12.5 9 17.5 20 6.5\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\n/**\n * Select — a single-choice control backed by a listbox.\n *\n * Client leaf. Radix provides the APG listbox model: type-ahead, Arrow/Home/End\n * navigation, Escape to close, and correct `combobox`/`listbox`/`option` roles\n * with `aria-selected`. Pair the `SelectTrigger` with a `<Label>` (or\n * `aria-label`). For long, free-text-filtered lists prefer the CommandMenu.\n */\nexport function Select(\n  props: React.ComponentProps<typeof SelectPrimitive.Root>,\n) {\n  return <SelectPrimitive.Root data-slot=\"select\" {...props} />;\n}\n\nexport function SelectGroup(\n  props: React.ComponentProps<typeof SelectPrimitive.Group>,\n) {\n  return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />;\n}\n\nexport function SelectValue(\n  props: React.ComponentProps<typeof SelectPrimitive.Value>,\n) {\n  return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />;\n}\n\nexport function SelectTrigger({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger>) {\n  return (\n    <SelectPrimitive.Trigger\n      data-slot=\"select-trigger\"\n      className={cn(\n        \"flex h-10 w-full items-center justify-between gap-2 rounded-vexa-md border border-vexa-input bg-vexa-surface-raised px-3 py-2 text-sm text-vexa-foreground shadow-vexa-sm\",\n        \"transition-[color,box-shadow,border-color] duration-[var(--vexa-duration-fast)]\",\n        \"data-[placeholder]:text-vexa-muted-foreground\",\n        \"outline-none focus-visible:border-vexa-focus focus-visible:ring-2 focus-visible:ring-vexa-ring focus-visible:ring-offset-1 focus-visible:ring-offset-vexa-background\",\n        \"disabled:cursor-not-allowed disabled:opacity-60\",\n        \"aria-[invalid=true]:border-vexa-danger aria-[invalid=true]:focus-visible:ring-vexa-danger\",\n        \"[&_svg]:size-4 [&_svg]:shrink-0\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        <ChevronDownIcon className=\"size-4 text-vexa-muted-foreground\" />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\nexport function SelectScrollUpButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n  return (\n    <SelectPrimitive.ScrollUpButton\n      data-slot=\"select-scroll-up-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronUpIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollUpButton>\n  );\n}\n\nexport function SelectScrollDownButton({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n  return (\n    <SelectPrimitive.ScrollDownButton\n      data-slot=\"select-scroll-down-button\"\n      className={cn(\n        \"flex cursor-default items-center justify-center py-1\",\n        className,\n      )}\n      {...props}\n    >\n      <ChevronDownIcon className=\"size-4\" />\n    </SelectPrimitive.ScrollDownButton>\n  );\n}\n\nexport function SelectContent({\n  className,\n  children,\n  position = \"popper\",\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n  return (\n    <SelectPrimitive.Portal>\n      <SelectPrimitive.Content\n        data-slot=\"select-content\"\n        position={position}\n        className={cn(\n          \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-vexa-md border border-vexa-border bg-vexa-surface-raised text-vexa-foreground shadow-vexa-lg\",\n          \"transition-[opacity,transform] duration-[var(--vexa-duration-fast)] ease-[var(--vexa-ease-standard)] motion-reduce:transition-none\",\n          \"data-[state=closed]:opacity-0 data-[state=open]:opacity-100\",\n          position === \"popper\" &&\n            \"data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1\",\n          className,\n        )}\n        {...props}\n      >\n        <SelectScrollUpButton />\n        <SelectPrimitive.Viewport\n          className={cn(\n            \"p-1\",\n            position === \"popper\" &&\n              \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n          )}\n        >\n          {children}\n        </SelectPrimitive.Viewport>\n        <SelectScrollDownButton />\n      </SelectPrimitive.Content>\n    </SelectPrimitive.Portal>\n  );\n}\n\nexport function SelectLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Label>) {\n  return (\n    <SelectPrimitive.Label\n      data-slot=\"select-label\"\n      className={cn(\n        \"px-2 py-1.5 text-xs font-medium text-vexa-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function SelectItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Item>) {\n  return (\n    <SelectPrimitive.Item\n      data-slot=\"select-item\"\n      className={cn(\n        \"relative flex w-full cursor-default select-none items-center rounded-vexa-sm py-1.5 pl-8 pr-2 text-sm outline-none\",\n        \"focus:bg-vexa-accent focus:text-vexa-accent-foreground\",\n        \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"absolute left-2 flex size-4 items-center justify-center\">\n        <SelectPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    </SelectPrimitive.Item>\n  );\n}\n\nexport function SelectSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      className={cn(\"-mx-1 my-1 h-px bg-vexa-border\", className)}\n      {...props}\n    />\n  );\n}\n"
    }
  ],
  "meta": {
    "frameworks": [
      "react",
      "next"
    ],
    "rsc": "client",
    "exports": [
      "Select",
      "SelectGroup",
      "SelectValue",
      "SelectTrigger",
      "SelectContent",
      "SelectLabel",
      "SelectItem",
      "SelectSeparator",
      "SelectScrollUpButton",
      "SelectScrollDownButton"
    ],
    "cssVars": [
      "--vexa-input",
      "--vexa-surface-raised",
      "--vexa-foreground",
      "--vexa-muted-foreground",
      "--vexa-accent",
      "--vexa-accent-foreground",
      "--vexa-border",
      "--vexa-focus",
      "--vexa-ring",
      "--vexa-danger",
      "--vexa-background"
    ],
    "a11y": "APG listbox with type-ahead, Arrow/Home/End navigation, and Escape to close; combobox/listbox/option roles with aria-selected. Pair the trigger with a <Label>.",
    "example": "<Select><SelectTrigger aria-label=\"Theme\"><SelectValue placeholder=\"Select a theme\" /></SelectTrigger><SelectContent><SelectItem value=\"light\">Light</SelectItem></SelectContent></Select>",
    "tests": "planned — see docs/README.md#roadmap"
  }
}

Detalles

Accesibilidad

APG listbox with type-ahead, Arrow/Home/End navigation, and Escape to close; combobox/listbox/option roles with aria-selected. Pair the trigger with a <Label>.

Uso

Pick one option from a longer list where a native-feeling popup beats a row of radios.

Anatomía

Select
Root; owns the value and open state.
SelectTrigger
The button that opens the listbox.
SelectValue
Renders the current value in the trigger.
SelectContent
The popup listbox.
SelectItem
A selectable option.
SelectGroup / SelectLabel
Section a long list of options.
SelectSeparator
Divider between option groups.
SelectScrollUpButton / SelectScrollDownButton
Scroll affordances for an overflowing list.

Accesibilidad

APG combobox trigger opening a listbox; option roles with `aria-selected`. Pair the trigger with a `<Label>`.

Teclado

TeclaAcción
Arrow / Home / EndMove through options
Type-aheadJump to the matching option
EnterSelect the active option
EscapeClose without changing the value

Foco: Focus is held in the listbox while open and returns to the trigger on close; visible ring.

Buenas prácticas

Recomendado

  • Label the trigger.
  • Group long lists with SelectGroup / SelectLabel.
  • Keep option text short and scannable.

Evitar

  • Don't use a Select for 2–3 options — use RadioGroup.
  • Don't put interactive controls inside an option.
  • Don't rely on the placeholder as the label.
Editar esta página

¿Te ha resultado útil esta página?

Guardado localmente en este navegador. Sin cuenta ni servidor.