Select
Select lets users pick one option from a styled dropdown. Use it when the list needs a styled trigger and menu that match the rest of the UI. For native form submission and OS pickers with plain text options, use NativeSelect.
'use client';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/nxds/components/select/select';
export default function SelectDefault() {
return (
<Select>
<SelectTrigger className="nx:w-[180px]" aria-label="Select a fruit">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
<SelectItem value="grape">Grape</SelectItem>
<SelectItem value="mango">Mango</SelectItem>
</SelectContent>
</Select>
);
}
Installation
npx nxds-cli@latest add select
Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.
Usage
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '@/components/nxds/components/select/select';
Composition
Select
├── SelectTrigger
│ └── SelectValue
└── SelectContent
├── SelectItem
├── SelectGroup
│ ├── SelectLabel
│ └── SelectItem
└── SelectSeparator
Examples
With default value
Open with an option already selected by setting defaultValue.
'use client';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/nxds/components/select/select';
export default function SelectWithDefaultValue() {
return (
<Select defaultValue="banana">
<SelectTrigger className="nx:w-[180px]" aria-label="Select a fruit">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
</SelectContent>
</Select>
);
}
Invalid trigger
Set aria-invalid on the trigger to show an error border.
'use client';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/nxds/components/select/select';
export default function SelectInvalidTrigger() {
return (
<Select defaultValue="apple">
<SelectTrigger
className="nx:w-[180px]"
aria-label="Invalid select"
aria-invalid
>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectContent>
</Select>
);
}
With disabled items
Disable single items so users can see but not pick them.
'use client';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/nxds/components/select/select';
export default function SelectWithDisabledItems() {
return (
<Select>
<SelectTrigger className="nx:w-[180px]" aria-label="Select an option">
<SelectValue placeholder="Select option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Available</SelectItem>
<SelectItem value="option2" disabled>
Unavailable
</SelectItem>
<SelectItem value="option3">Available</SelectItem>
</SelectContent>
</Select>
);
}
With groups
Group items under labels and divide the groups with a separator.
'use client';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '@/components/nxds/components/select/select';
export default function SelectWithGroups() {
return (
<Select>
<SelectTrigger className="nx:w-[200px]" aria-label="Select a food">
<SelectValue placeholder="Select a food" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Vegetables</SelectLabel>
<SelectItem value="carrot">Carrot</SelectItem>
<SelectItem value="broccoli">Broccoli</SelectItem>
<SelectItem value="spinach">Spinach</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
);
}
Sizes
Each trigger size matches the height of an Input at the same size.
'use client';
import { Input } from '@/components/nxds/components/input/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/nxds/components/select/select';
const SIZES = ['sm', 'default', 'lg'] as const;
export default function SelectSizes() {
return (
<div className="nx:flex nx:w-[420px] nx:flex-col nx:gap-3">
{SIZES.map((size) => (
<div key={size} className="nx:flex nx:items-center nx:gap-3">
<Select>
<SelectTrigger size={size} aria-label={`${size} select`}>
<SelectValue placeholder={size} />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
</SelectContent>
</Select>
<Input size={size} placeholder={size} aria-label={`${size} input`} />
</div>
))}
</div>
);
}
This example also needs: Input.
Props
Select
Select adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectContent
SelectContent adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectGroup
SelectGroup adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectItem
SelectItem adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectLabel
SelectLabel adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectScrollDownButton
SelectScrollDownButton adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectScrollUpButton
SelectScrollUpButton adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectSeparator
SelectSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
size | "default" | "sm" | "lg" | null | "default" | —No description |
variant | "borderless" | "bordered" | null | "bordered" | —No description |
SelectValue
SelectValue adds no props of its own; it accepts the props of the element or primitive it wraps.
Select lets users pick one option from a styled dropdown. Use it when the list needs a styled trigger and menu that match the rest of the UI. For native form submission and OS pickers with plain text options, use NativeSelect.
<script setup lang="ts">
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/select';
</script>
<template>
<Select>
<SelectTrigger class="nx:w-[180px]" aria-label="Select a fruit">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
<SelectItem value="grape">Grape</SelectItem>
<SelectItem value="mango">Mango</SelectItem>
</SelectContent>
</Select>
</template>
Installation
- 1
Install the following dependencies:
npm install @phosphor-icons/vue@^2.2.1 @vueuse/core@^14.1.0 class-variance-authority@^0.7.1 reka-ui@^2.11.0 - 2
Copy and paste the following code into your project.
components/overlay-layout/overlay-layout.ts import { cva, type VariantProps } from 'class-variance-authority'; /** * Popover-family surfaces share the translucent/blurred recipe; Tooltip uses a * solid carve-out because its small, brief text appears over arbitrary content. */ export const popoverSurfaceClassName = [ 'nx:rounded-md', 'nx:border-default', 'nx:border-border-default', 'nx:bg-popover-alpha', 'nx:backdrop-blur-md', 'nx:text-popover-foreground', 'nx:typography-body-default', 'nx:shadow-lg', ].join(' '); export const tooltipSurfaceClassName = [ 'nx:rounded-md', 'nx:border-default', 'nx:border-border-default', 'nx:bg-popover', 'nx:text-popover-foreground', 'nx:typography-body-default', 'nx:shadow-lg', ].join(' '); export const navigationMenuInlinePopoverSurfaceClassName = [ 'nx:group-data-[viewport=false]/navigation-menu:rounded-md', 'nx:group-data-[viewport=false]/navigation-menu:border-default', 'nx:group-data-[viewport=false]/navigation-menu:border-border-default', 'nx:group-data-[viewport=false]/navigation-menu:bg-popover-alpha', 'nx:group-data-[viewport=false]/navigation-menu:backdrop-blur-md', 'nx:group-data-[viewport=false]/navigation-menu:text-popover-foreground', 'nx:group-data-[viewport=false]/navigation-menu:shadow-lg', ].join(' '); export const navigationMenuInlinePopoverTransitionClassName = [ 'nx:group-data-[viewport=false]/navigation-menu:transition-[opacity,scale]', 'nx:group-data-[viewport=false]/navigation-menu:duration-fast', 'nx:group-data-[viewport=false]/navigation-menu:ease-move', 'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:scale-95', 'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:opacity-0', 'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-overlay-presence-exit', ].join(' '); // `animate-overlay-presence-exit` is a non-visual "presence bridge": Reka's // Presence only waits on `animationName`, not `transitionend`, so this inert // animation keeps a closing overlay mounted while the opacity/scale/translate // TRANSITIONS below run the visible exit. It animates an unread custom property (see // generateMotionUtilitiesCSS in @nxds/core), never a transitioned property, so it // cannot override those transitions. const overlayPresenceExitClassName = 'nx:data-[state=closed]:animate-overlay-presence-exit'; export const overlayFloatingTransitionClassName = [ 'nx:transition-[opacity,scale]', 'nx:duration-fast', 'nx:ease-move', 'nx:data-[state=closed]:scale-95', 'nx:data-[state=closed]:opacity-0', overlayPresenceExitClassName, ].join(' '); const overlayContentTransitionClassName = [ 'nx:transition-[opacity,scale]', 'nx:duration-default', 'nx:ease-move', 'nx:data-[state=closed]:scale-95', 'nx:data-[state=closed]:opacity-0', overlayPresenceExitClassName, ].join(' '); export const overlayScrimTransitionClassName = [ 'nx:transition-opacity', 'nx:duration-default', 'nx:ease-move', 'nx:data-[state=closed]:opacity-0', overlayPresenceExitClassName, ].join(' '); export const overlayPanelTransitionClassName = [ 'nx:transition-[translate]', 'nx:duration-slow', 'nx:ease-move', overlayPresenceExitClassName, ].join(' '); export const overlayContentVariants = cva( [ // Centred with margins, not a translate: a transform would make this the // containing block for fixed-position popups a dialog holds. 'nx:fixed nx:inset-0 nx:z-modal nx:m-auto nx:flex nx:h-fit nx:w-full nx:max-w-lg nx:flex-col', 'nx:max-h-[calc(100svh-2rem)] nx:overflow-hidden', 'nx:gap-4 nx:border-default nx:border-border-default nx:bg-container nx:py-6 nx:shadow-lg', overlayContentTransitionClassName, 'nx:sm:rounded-lg', ].join(' ') ); export const overlayScrimVariants = cva( [ 'nx:fixed nx:inset-0 nx:z-modal nx:bg-overlay', overlayScrimTransitionClassName, ].join(' ') ); export const overlayHeaderVariants = cva( 'nx:flex nx:shrink-0 nx:flex-col nx:gap-1 nx:px-6', { variants: { variant: { default: 'nx:text-center nx:sm:text-left', center: 'nx:items-center nx:text-center', }, }, defaultVariants: { variant: 'default', }, } ); export const overlayBodyClassName = [ 'nx:min-h-0', 'nx:overflow-y-auto', 'nx:px-6', 'nx:focus-visible:outline-2', 'nx:focus-visible:outline-focus-default', 'nx:focus-visible:[outline-offset:-2px]', ].join(' '); export const overlayFooterVariants = cva( 'nx:flex nx:shrink-0 nx:gap-2 nx:px-6', { variants: { orientation: { horizontal: 'nx:flex-col nx:sm:flex-row nx:sm:items-center nx:sm:justify-end', vertical: 'nx:flex-col nx:items-stretch nx:*:w-full', }, }, defaultVariants: { orientation: 'horizontal', }, } ); export const overlayCloseButtonClassName = [ 'nx:absolute nx:right-6 nx:top-6 nx:rounded-sm nx:border-0 nx:bg-transparent nx:p-1 nx:text-muted-foreground-subtle', 'nx:transition-control', 'nx:hover:bg-container-hover nx:hover:text-foreground', 'nx:focus-visible:bg-container-hover nx:focus-visible:text-foreground', 'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default', 'nx:disabled:pointer-events-none', ].join(' '); export type OverlayHeaderVariants = VariantProps<typeof overlayHeaderVariants>; export type OverlayVariant = NonNullable< VariantProps<typeof overlayHeaderVariants>['variant'] >; export type OverlayButtonOrientation = NonNullable< VariantProps<typeof overlayFooterVariants>['orientation'] >; export type OverlayLayoutContextValue = { variant: OverlayVariant; buttonOrientation: OverlayButtonOrientation; }; export const defaultOverlayLayout: OverlayLayoutContextValue = { variant: 'default', buttonOrientation: 'horizontal', }; export function resolveOverlayButtonOrientation( variant: OverlayVariant ): OverlayButtonOrientation { return variant === 'center' ? 'vertical' : 'horizontal'; }components/select/SelectContent.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { SelectContent, type SelectContentEmits, type SelectContentProps as RekaSelectContentProps, SelectPortal, SelectViewport, useForwardPropsEmits, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { overlayFloatingTransitionClassName, popoverSurfaceClassName, } from '../overlay-layout/overlay-layout'; import SelectScrollDownButton from './SelectScrollDownButton.vue'; import SelectScrollUpButton from './SelectScrollUpButton.vue'; export interface SelectContentProps extends RekaSelectContentProps { class?: HTMLAttributes['class']; } defineOptions({ inheritAttrs: false }); const props = withDefaults(defineProps<SelectContentProps>(), { position: 'popper', }); const emits = defineEmits<SelectContentEmits>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <SelectPortal> <SelectContent v-bind="{ ...$attrs, ...forwarded }" data-slot="select-content" :class=" cn( 'nx:relative nx:z-popover nx:max-h-96 nx:min-w-32 nx:overflow-hidden', popoverSurfaceClassName, overlayFloatingTransitionClassName, position === 'popper' && 'nx:data-[side=bottom]:translate-y-1 nx:data-[side=left]:-translate-x-1 nx:data-[side=right]:translate-x-1 nx:data-[side=top]:-translate-y-1', props.class ) " > <SelectScrollUpButton /> <SelectViewport :class=" cn( 'nx:p-1', position === 'popper' && 'nx:h-(--reka-select-trigger-height) nx:w-full nx:min-w-(--reka-select-trigger-width)' ) " > <slot /> </SelectViewport> <SelectScrollDownButton /> </SelectContent> </SelectPortal> </template>components/select/SelectItem.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { SelectItem, type SelectItemEmits, type SelectItemProps as RekaSelectItemProps, SelectItemText, useForwardPropsEmits, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { IconCheck } from '../../lib/icons'; import { selectionIndicatorMotionClassName } from '../../lib/motion'; import { cn } from '../../lib/utils'; export interface SelectItemProps extends RekaSelectItemProps { class?: HTMLAttributes['class']; } const props = defineProps<SelectItemProps>(); const emits = defineEmits<SelectItemEmits>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <SelectItem v-bind="forwarded" data-slot="select-item" :class=" cn( 'nx:group nx:relative nx:flex nx:w-full nx:cursor-default nx:select-none nx:items-center', 'nx:rounded-sm nx:py-1.5 nx:pl-8 nx:pr-2 nx:typography-body-default nx:outline-none', 'nx:focus:bg-popover-hover nx:focus:text-popover-foreground', 'nx:data-disabled:pointer-events-none nx:data-disabled:text-disabled-foreground', props.class ) " > <span class="nx:pointer-events-none nx:absolute nx:left-2 nx:flex nx:size-3.5 nx:items-center nx:justify-center" > <IconCheck data-slot="select-item-indicator-icon" aria-hidden="true" :class=" cn( 'nx:size-4', selectionIndicatorMotionClassName, 'nx:group-data-[state=checked]:scale-100 nx:group-data-[state=checked]:opacity-100' ) " /> </span> <SelectItemText><slot /></SelectItemText> </SelectItem> </template>components/select/SelectLabel.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { SelectLabel, type SelectLabelProps as RekaSelectLabelProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface SelectLabelProps extends RekaSelectLabelProps { class?: HTMLAttributes['class']; } const props = defineProps<SelectLabelProps>(); const delegated = reactiveOmit(props, 'class'); </script> <template> <SelectLabel v-bind="delegated" data-slot="select-label" :class="cn('nx:px-2 nx:py-1.5 nx:typography-label-default', props.class)" > <slot /> </SelectLabel> </template>components/select/SelectScrollDownButton.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { SelectScrollDownButton, type SelectScrollDownButtonProps as RekaSelectScrollDownButtonProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { IconChevronDown } from '../../lib/icons'; import { cn } from '../../lib/utils'; export interface SelectScrollDownButtonProps extends RekaSelectScrollDownButtonProps { class?: HTMLAttributes['class']; } const props = defineProps<SelectScrollDownButtonProps>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardProps(delegated); </script> <template> <SelectScrollDownButton v-bind="forwarded" data-slot="select-scroll-down-button" :class=" cn( 'nx:flex nx:cursor-default nx:items-center nx:justify-center nx:py-1', props.class ) " > <IconChevronDown class="nx:size-4" /> </SelectScrollDownButton> </template>components/select/SelectScrollUpButton.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { SelectScrollUpButton, type SelectScrollUpButtonProps as RekaSelectScrollUpButtonProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { IconChevronUp } from '../../lib/icons'; import { cn } from '../../lib/utils'; export interface SelectScrollUpButtonProps extends RekaSelectScrollUpButtonProps { class?: HTMLAttributes['class']; } const props = defineProps<SelectScrollUpButtonProps>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardProps(delegated); </script> <template> <SelectScrollUpButton v-bind="forwarded" data-slot="select-scroll-up-button" :class=" cn( 'nx:flex nx:cursor-default nx:items-center nx:justify-center nx:py-1', props.class ) " > <IconChevronUp class="nx:size-4" /> </SelectScrollUpButton> </template>components/select/SelectSeparator.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { SelectSeparator, type SelectSeparatorProps as RekaSelectSeparatorProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface SelectSeparatorProps extends RekaSelectSeparatorProps { class?: HTMLAttributes['class']; } const props = defineProps<SelectSeparatorProps>(); const delegated = reactiveOmit(props, 'class'); </script> <template> <SelectSeparator v-bind="delegated" data-slot="select-separator" :class=" cn('nx:-mx-1 nx:my-1 nx:h-px nx:bg-border-default-alpha', props.class) " /> </template>components/select/SelectTrigger.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { SelectIcon, SelectTrigger, type SelectTriggerProps as RekaSelectTriggerProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { IconChevronDown } from '../../lib/icons'; import { cn } from '../../lib/utils'; import { type SelectTriggerVariants, selectTriggerVariants } from '.'; export interface SelectTriggerProps extends RekaSelectTriggerProps { size?: SelectTriggerVariants['size']; variant?: SelectTriggerVariants['variant']; class?: HTMLAttributes['class']; } const props = defineProps<SelectTriggerProps>(); const delegated = reactiveOmit(props, 'class', 'size', 'variant'); const forwarded = useForwardProps(delegated); </script> <template> <SelectTrigger v-bind="forwarded" data-slot="select-trigger" :data-size="size ?? 'default'" :data-variant="variant ?? 'bordered'" :class="cn(selectTriggerVariants({ size, variant }), props.class)" > <slot /> <SelectIcon as-child> <IconChevronDown class="nx:size-4 nx:text-muted-foreground nx:group-disabled/select-trigger:text-disabled-foreground" /> </SelectIcon> </SelectTrigger> </template>components/select/index.ts import { cva, type VariantProps } from 'class-variance-authority'; import { fieldSizeVariants } from '../../lib/field-size'; export { default as SelectContent, type SelectContentProps, } from './SelectContent.vue'; export { default as SelectItem, type SelectItemProps } from './SelectItem.vue'; export { default as SelectLabel, type SelectLabelProps, } from './SelectLabel.vue'; export { default as SelectScrollDownButton, type SelectScrollDownButtonProps, } from './SelectScrollDownButton.vue'; export { default as SelectScrollUpButton, type SelectScrollUpButtonProps, } from './SelectScrollUpButton.vue'; export { default as SelectSeparator, type SelectSeparatorProps, } from './SelectSeparator.vue'; export { default as SelectTrigger, type SelectTriggerProps, } from './SelectTrigger.vue'; export { SelectRoot as Select, SelectGroup, type SelectGroupProps, type SelectRootProps as SelectProps, SelectValue, type SelectValueProps, } from 'reka-ui'; export const selectTriggerVariants = cva( [ 'nx:group/select-trigger nx:flex nx:box-border nx:w-full nx:items-center nx:justify-between nx:gap-2', 'nx:rounded-md nx:border-default nx:transition-field', 'nx:whitespace-nowrap nx:text-inherit', 'nx:data-[placeholder]:text-muted-foreground', 'nx:focus-visible:outline-default nx:focus-visible:outline-focus-default nx:focus-visible:border-focus-default', 'nx:aria-invalid:border-error-border nx:aria-invalid:focus-visible:outline-focus-error nx:aria-invalid:focus-visible:border-focus-error', 'nx:disabled:cursor-not-allowed nx:disabled:bg-disabled nx:disabled:text-disabled-foreground', 'nx:[&>span]:line-clamp-1', ], { variants: { size: fieldSizeVariants, variant: { bordered: 'nx:border-border-default nx:bg-container nx:enabled:hover:bg-container-hover nx:disabled:border-border-disabled', borderless: 'nx:border-transparent nx:bg-control-background nx:enabled:hover:bg-control-background-hover', }, }, defaultVariants: { size: 'default', variant: 'bordered', }, } ); export type SelectTriggerVariants = VariantProps<typeof selectTriggerVariants>;lib/field-size.ts export const fieldSizeVariants = { default: 'nx:h-10 nx:px-3 nx:py-0 nx:typography-body-default', sm: 'nx:h-8 nx:px-2.5 nx:py-0 nx:typography-body-small', lg: 'nx:h-12 nx:px-3.5 nx:py-0 nx:typography-body-default', };lib/icons.ts /** * Internal Icons * * This barrel file re-exports icons used internally by the design system. * These are hardcoded icons for component states (loading, validation, navigation). * * User-facing icons should be passed via props by consumers. * * This set draws Phosphor. `nxds init --icons` installs the same names drawn * by another library. */ import { PhCircle } from '@phosphor-icons/vue'; import { type FunctionalComponent, h } from 'vue'; // Loading states export { PhCircleNotch as IconLoader2 } from '@phosphor-icons/vue'; // Navigation export { PhArrowDown as IconArrowDown, PhArrowLeft as IconArrowLeft, PhArrowRight as IconArrowRight, PhArrowUp as IconArrowUp, PhCaretDown as IconChevronDown, PhCaretLeft as IconChevronLeft, PhCaretRight as IconChevronRight, PhCaretUp as IconChevronUp, PhDotsThree as IconDots, PhSidebarSimple as IconLayoutSidebar, PhCaretUpDown as IconSelector, } from '@phosphor-icons/vue'; // Actions export { PhCheck as IconCheck, PhCopy as IconCopy, PhMinus as IconMinus, PhPencilSimple as IconPencil, PhPlus as IconPlus, PhX as IconX, } from '@phosphor-icons/vue'; export const IconCircleFilled: FunctionalComponent = (_props, { attrs }) => h(PhCircle, { weight: 'fill', ...attrs }); // Filter field types export { PhCalendarBlank as IconCalendar, PhHash as IconHash, PhTextAa as IconLetterCase, PhListBullets as IconList, } from '@phosphor-icons/vue'; // Search / input export { PhMagnifyingGlass as IconSearch } from '@phosphor-icons/vue'; // Alerts/Status export { PhWarningCircle as IconAlertCircle, PhWarning as IconAlertTriangle, PhCheckCircle as IconCircleCheck, PhInfo as IconInfoCircle, } from '@phosphor-icons/vue'; // Layout export { PhDotsSixVertical as IconGripVertical, PhPalette as IconPalette, } from '@phosphor-icons/vue';lib/motion.ts export const selectionIndicatorMotionClassName = 'nx:scale-50 nx:opacity-0 nx:transition-[opacity,scale] nx:duration-fast nx:ease-enter'; - 3
Keep each file at the path shown above it.
Paths are relative to
packages/vue/src/. Keep the same folders under your@/root, and the imports between files work without edits.
Usage
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from '@/components/select';
Examples
With default value
Open with an option already selected by setting defaultValue.
<script setup lang="ts">
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/select';
</script>
<template>
<Select default-value="banana">
<SelectTrigger class="nx:w-[180px]" aria-label="Select a fruit">
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
</SelectContent>
</Select>
</template>
Invalid trigger
Set aria-invalid on the trigger to show an error border.
<script setup lang="ts">
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/select';
</script>
<template>
<Select default-value="apple">
<SelectTrigger
class="nx:w-[180px]"
aria-label="Invalid select"
aria-invalid="true"
>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectContent>
</Select>
</template>
With disabled items
Disable single items so users can see but not pick them.
<script setup lang="ts">
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/select';
</script>
<template>
<Select>
<SelectTrigger class="nx:w-[180px]" aria-label="Select an option">
<SelectValue placeholder="Select option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Available</SelectItem>
<SelectItem value="option2" disabled>Unavailable</SelectItem>
<SelectItem value="option3">Available</SelectItem>
</SelectContent>
</Select>
</template>
With groups
Group items under labels and divide the groups with a separator.
<script setup lang="ts">
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '@/components/select';
</script>
<template>
<Select>
<SelectTrigger class="nx:w-[200px]" aria-label="Select a food">
<SelectValue placeholder="Select a food" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
<SelectItem value="orange">Orange</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Vegetables</SelectLabel>
<SelectItem value="carrot">Carrot</SelectItem>
<SelectItem value="broccoli">Broccoli</SelectItem>
<SelectItem value="spinach">Spinach</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</template>
Sizes
Each trigger size matches the height of an Input at the same size.
<script setup lang="ts">
import { Input } from '@/components/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/select';
const sizes = ['sm', 'default', 'lg'] as const;
</script>
<template>
<div class="nx:flex nx:w-[420px] nx:flex-col nx:gap-3">
<div
v-for="size in sizes"
:key="size"
class="nx:flex nx:items-center nx:gap-3"
>
<Select>
<SelectTrigger :size="size" :aria-label="size + ' select'">
<SelectValue :placeholder="size" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
</SelectContent>
</Select>
<Input :size="size" :placeholder="size" :aria-label="size + ' input'" />
</div>
</div>
</template>
This example also needs: Input.
Props
SelectContent
SelectContent adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectItem
SelectItem adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectLabel
SelectLabel adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectScrollDownButton
SelectScrollDownButton adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectScrollUpButton
SelectScrollUpButton adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectSeparator
SelectSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.
SelectTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
size | "default" | "sm" | "lg" | null | "default" | —No description |
variant | "bordered" | "borderless" | null | "bordered" | —No description |