Select
Hoja clienteA 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.
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.jsonSe 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.
"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
{
"$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
| Tecla | Acción |
|---|---|
| Arrow / Home / End | Move through options |
| Type-ahead | Jump to the matching option |
| Enter | Select the active option |
| Escape | Close 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.
¿Te ha resultado útil esta página?
Guardado localmente en este navegador. Sin cuenta ni servidor.