All components outlinesubtlexssmmdlg
Select
Custom listbox-style select with trigger, indicator, and item slots in outline and subtle styles.
Astro
Next.js
Remix
Vite
1234567891011121314151617181920212223242526272829303132333435import { Select, createListCollection } from "#components/ui/ark-ui/select.tsx";
const frameworks = createListCollection({ items: ["Astro", "Next.js", "Remix", "Vite"],});
export function Example() { return ( // variant/size resolve once on Root; every part inherits them via // context — Indicator and ItemIndicator ship default icons <Select.Root collection={frameworks} defaultValue={["Next.js"]} variant="outline" size="md"> <Select.Label>Framework</Select.Label> <Select.Control> <Select.Trigger> <Select.ValueText /> </Select.Trigger> <Select.IndicatorGroup> <Select.Indicator /> </Select.IndicatorGroup> </Select.Control> {/* Positioner portals the panel and positions it under the trigger */} <Select.Positioner> <Select.Content> {frameworks.items.map((item) => ( <Select.Item key={item} item={item}> <Select.ItemText>{item}</Select.ItemText> <Select.ItemIndicator /> </Select.Item> ))} </Select.Content> </Select.Positioner> <Select.HiddenSelect /> </Select.Root> );}Variants
Astro
Next.js
Remix
Vite
Astro
Next.js
Remix
Vite
Sizes
Astro
Next.js
Remix
Vite
Astro
Next.js
Remix
Vite
Astro
Next.js
Remix
Vite
Astro
Next.js
Remix
Vite
Install
1npx stylexblocks add selectSource
components/ui/ark-ui/select.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596import * as stylex from "@stylexjs/stylex";import { Select as ArkSelect, type CollectionItem } from "@ark-ui/react/select";import { Portal } from "@ark-ui/react/portal";import { createSlotRecipeContext } from "#lib/ark-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles, type SlotRecipeVariants, type StyleOverrideProps, type WithStyleOverrides,} from "#lib/recipe-props.ts";import { semanticShadows } from "@stylexblocks/ui/semantic-tokens/shadows.stylex.ts";import { selectIndicatorIconVars, selectItemIconVars } from '@stylexblocks/ui/slot-recipes/select.stylex.ts'
import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { cursor } from '@stylexblocks/ui/tokens/cursor.stylex.ts'import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'import { zIndices } from '@stylexblocks/ui/tokens/z-indices.stylex.ts'
const selectRoot = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._1_5, width: sizes.full, },})
const selectRootSizes = stylex.create({ xs: { '--select-trigger-height': sizes.controlXs, '--select-trigger-padding-x': spacing._2, }, sm: { '--select-trigger-height': sizes.controlSm, '--select-trigger-padding-x': spacing._2_5, }, md: { '--select-trigger-height': sizes.controlMd, '--select-trigger-padding-x': spacing._3, }, lg: { '--select-trigger-height': sizes.controlLg, '--select-trigger-padding-x': spacing._4, },})
const selectTrigger = stylex.create({ base: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: sizes.full, minHeight: 'var(--select-trigger-height)', paddingInline: 'var(--select-trigger-padding-x)', userSelect: 'none', textAlign: 'start', outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, focusRingWidth: 0, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, cursor: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, borderColor: { default: null, ':is([data-invalid], [aria-invalid="true"])': semanticColors.borderError, ':hover': { default: semanticColors.borderEmphasized, ':focus-visible': 'var(--focus-ring-color)', }, }, },})
const selectTriggerSizes = stylex.create({ xs: { borderRadius: semanticRadii.control, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, gap: spacing._1, }, sm: { borderRadius: semanticRadii.control, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, gap: spacing._1, }, md: { borderRadius: semanticRadii.control, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, gap: spacing._2, }, lg: { borderRadius: semanticRadii.control, fontSize: fontSizes.md, lineHeight: lineHeights.md, paddingBlock: spacing._3, gap: spacing._2, },})
const selectTriggerVariants = stylex.create({ outline: { backgroundColor: semanticColors.bg, borderWidth: '1px', borderStyle: 'solid', borderColor: { default: semanticColors.border, ':is([data-invalid], [aria-invalid="true"])': semanticColors.borderError, ':hover': { default: semanticColors.borderEmphasized, ':focus-visible': 'var(--focus-ring-color)', }, }, }, subtle: { borderWidth: '1px', borderStyle: 'solid', borderColor: { default: 'transparent', ':is([data-invalid], [aria-invalid="true"])': semanticColors.borderError, ':hover': { default: semanticColors.borderEmphasized, ':focus-visible': 'var(--focus-ring-color)', }, }, backgroundColor: semanticColors.bgMuted, },})
const selectIndicatorGroup = stylex.create({ base: { display: 'flex', alignItems: 'center', gap: spacing._1, position: 'absolute', right: 0, top: 0, bottom: 0, paddingInline: 'var(--select-trigger-padding-x)', pointerEvents: 'none', },})
const selectIndicator = stylex.create({ base: { display: 'flex', alignItems: 'center', justifyContent: 'center', [selectIndicatorIconVars.width]: sizes._3_5, [selectIndicatorIconVars.height]: sizes._3_5, },})
const selectIndicatorSizes = stylex.create({ xs: { [selectIndicatorIconVars.width]: sizes._3_5, [selectIndicatorIconVars.height]: sizes._3_5, }, sm: { [selectIndicatorIconVars.width]: sizes._4, [selectIndicatorIconVars.height]: sizes._4, }, md: { [selectIndicatorIconVars.width]: sizes._4, [selectIndicatorIconVars.height]: sizes._4, }, lg: { [selectIndicatorIconVars.width]: sizes._5, [selectIndicatorIconVars.height]: sizes._5, },})
const selectIndicatorIcon = stylex.create({ base: { width: selectIndicatorIconVars.width, height: selectIndicatorIconVars.height, },})
const selectContent = stylex.create({ base: { outline: 'none', color: semanticColors.fg, maxHeight: 'var(--available-height)', '--menu-z-index': zIndices.layer3, zIndex: 'calc(var(--menu-z-index) + var(--layer-index, 0))', overflowY: 'auto', scrollbarWidth: 'thin', transformOrigin: 'var(--transform-origin)', animationName: { default: null, ':is([data-open], [data-state="open"])': 'fade-in, slide-from-top', ':is([data-closed], [data-state="closed"])': 'fade-out', }, animationDuration: { default: null, ':is([data-open], [data-state="open"])': durations.fast, ':is([data-closed], [data-state="closed"])': durations.faster, }, },})
const selectContentSizes = stylex.create({ xs: { padding: spacing._1, gap: spacing._1, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, }, sm: { padding: spacing._1, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, md: { padding: spacing._1, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, lg: { padding: spacing._1_5, fontSize: fontSizes.md, lineHeight: lineHeights.md, },})
const selectItem = stylex.create({ base: { position: 'relative', userSelect: 'none', display: 'flex', alignItems: 'center', gap: spacing._2, cursor: cursor.option, justifyContent: 'space-between', flex: 1, textAlign: 'start', borderRadius: semanticRadii.control, pointerEvents: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'none', }, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, [selectItemIconVars.width]: sizes._4, [selectItemIconVars.height]: sizes._4, },})
const selectItemSizes = stylex.create({ xs: { paddingBlock: spacing._1, paddingInline: spacing._2, }, sm: { paddingBlock: spacing._1, paddingInline: spacing._1_5, }, md: { paddingBlock: spacing._1_5, paddingInline: spacing._2, }, lg: { paddingBlock: spacing._2, paddingInline: spacing._3, },})
const selectItemIcon = stylex.create({ base: { width: selectItemIconVars.width, height: selectItemIconVars.height, },})
const selectControl = stylex.create({ base: { position: 'relative', },})
const selectItemText = stylex.create({ base: { flex: 1, },})
const selectItemGroup = stylex.create({ base: { marginTop: { default: null, ':first-child': 0, }, },})
const selectItemGroupSizes = stylex.create({ sm: { marginTop: { default: spacing._1, ':first-child': 0, }, }, md: { marginTop: { default: spacing._1_5, ':first-child': 0, }, }, lg: { marginTop: { default: spacing._2, ':first-child': 0, }, },})
const selectItemGroupLabel = stylex.create({ base: { paddingBlock: spacing._1, fontWeight: fontWeights.medium, },})
const selectItemGroupLabelSizes = stylex.create({ xs: { paddingBlock: spacing._1, paddingInline: spacing._2, }, sm: { paddingBlock: spacing._1, paddingInline: spacing._1_5, }, md: { paddingBlock: spacing._1_5, paddingInline: spacing._2, }, lg: { paddingBlock: spacing._2, paddingInline: spacing._3, },})
const selectLabel = stylex.create({ base: { fontWeight: fontWeights.medium, userSelect: 'none', fontSize: fontSizes.sm, lineHeight: lineHeights.sm, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, cursor: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, },})
const selectValueText = stylex.create({ base: { lineClamp: 1, maxWidth: '80%', },})
const selectItemIndicator = stylex.create({ base: {},})
const selectItemIndicatorSizes = stylex.create({ md: { display: 'flex', alignItems: 'center', justifyContent: 'center', },})
export type SelectVariant = keyof typeof selectTriggerVariants
export type SelectSize = keyof typeof selectRootSizes
const selectRecipe = defineSlotRecipe({ slots: { root: { styles: selectRoot, sizes: selectRootSizes }, trigger: { styles: selectTrigger, sizes: selectTriggerSizes, variants: selectTriggerVariants, }, indicatorGroup: { styles: selectIndicatorGroup }, indicator: { styles: selectIndicator, sizes: selectIndicatorSizes }, content: { styles: selectContent, sizes: selectContentSizes }, item: { styles: selectItem, sizes: selectItemSizes }, control: { styles: selectControl }, itemText: { styles: selectItemText }, itemGroup: { styles: selectItemGroup, sizes: selectItemGroupSizes }, itemGroupLabel: { styles: selectItemGroupLabel, sizes: selectItemGroupLabelSizes }, label: { styles: selectLabel }, valueText: { styles: selectValueText }, itemIndicator: { styles: selectItemIndicator, sizes: selectItemIndicatorSizes }, }, defaultVariants: { size: "md", variant: "outline" },});
// chrome the generated recipe leaves to the composition layer: unstyled// trigger button, popup panel surface, and item hover feedbackconst chrome = stylex.create({ btnReset: { appearance: "none", backgroundColor: "transparent", borderWidth: 0, borderStyle: "none", padding: 0, margin: 0, fontFamily: "inherit", fontSize: "inherit", color: "inherit", cursor: "pointer", textAlign: "inherit", }, anchor: { position: "relative" }, indicatorGroup: { position: "absolute", insetInlineEnd: 0, top: 0, bottom: 0, pointerEvents: "none", }, panel: { display: "flex", flexDirection: "column", backgroundColor: semanticColors.bgPanel, borderRadius: semanticRadii.panel, boxShadow: semanticShadows.overlay, minWidth: "var(--reference-width)", }, itemHover: { ":hover": { backgroundColor: semanticColors.bgMuted } },});
function ChevronDownIcon(props: React.ComponentProps<"svg">) { return ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}> <path d="m4 6 4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
function CheckIcon(props: React.ComponentProps<"svg">) { return ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}> <path d="m3.5 8.5 3 3 6-7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
const { withProvider, withContext } = createSlotRecipeContext(selectRecipe);
type SelectVariants = SlotRecipeVariants<typeof selectRecipe>;export type SelectVariantProps = Partial<SelectVariants>;
const StyledRoot = withProvider(ArkSelect.Root, "root", "SelectRoot");
function Root<T extends CollectionItem>( props: WithStyleOverrides<ArkSelect.RootProps<T>> & Partial<SelectVariants>,) { return <StyledRoot {...(props as React.ComponentProps<typeof StyledRoot>)} />;}Root.displayName = "SelectRoot";
const StyledIndicator = withContext(ArkSelect.Indicator, "indicator", "SelectIndicator");
function Indicator( props: React.ComponentProps<typeof ArkSelect.Indicator> & StyleOverrideProps,) { const { children, ...rest } = props; return ( <StyledIndicator {...rest}> {children ?? <ChevronDownIcon {...stylex.props(selectIndicatorIcon.base)} />} </StyledIndicator> );}Indicator.displayName = "SelectIndicator";
const StyledItemIndicator = withContext( ArkSelect.ItemIndicator, (v) => [ ...resolveSlotStyles(selectRecipe.slots.itemIndicator, v), selectItemIndicatorSizes.md, ], "SelectItemIndicator",);
function ItemIndicator( props: React.ComponentProps<typeof ArkSelect.ItemIndicator> & StyleOverrideProps,) { const { children, ...rest } = props; return ( <StyledItemIndicator {...rest}> {children ?? <CheckIcon {...stylex.props(selectItemIcon.base)} />} </StyledItemIndicator> );}ItemIndicator.displayName = "SelectItemIndicator";
function Positioner({ children, ...rest }: ArkSelect.PositionerProps) { return ( <Portal> <ArkSelect.Positioner {...rest}>{children}</ArkSelect.Positioner> </Portal> );}
/** * Select — Ark UI's select machine parts styled with the StyleX slot recipe. * `<Select.Root variant size>` resolves the recipe once; every part inherits * it through context. Content portals through `Select.Positioner`, and * Indicator / ItemIndicator ship default chevron / check icons. */export const Select = { Root, Label: withContext(ArkSelect.Label, "label", "SelectLabel"), Control: withContext( ArkSelect.Control, (v) => [...resolveSlotStyles(selectRecipe.slots.control, v), chrome.anchor], "SelectControl", ), Trigger: withContext( ArkSelect.Trigger, (v) => [chrome.btnReset, ...resolveSlotStyles(selectRecipe.slots.trigger, v)], "SelectTrigger", ), ValueText: withContext(ArkSelect.ValueText, "valueText", "SelectValueText"), IndicatorGroup: withContext( "div", (v) => [...resolveSlotStyles(selectRecipe.slots.indicatorGroup, v), chrome.indicatorGroup], "SelectIndicatorGroup", ), Indicator, Positioner, Content: withContext( ArkSelect.Content, (v) => [...resolveSlotStyles(selectRecipe.slots.content, v), chrome.panel], "SelectContent", ), Item: withContext( ArkSelect.Item, (v) => [...resolveSlotStyles(selectRecipe.slots.item, v), chrome.itemHover], "SelectItem", ), ItemText: withContext(ArkSelect.ItemText, "itemText", "SelectItemText"), ItemIndicator, ItemGroup: withContext(ArkSelect.ItemGroup, "itemGroup", "SelectItemGroup"), ItemGroupLabel: withContext( ArkSelect.ItemGroupLabel, "itemGroupLabel", "SelectItemGroupLabel", ), HiddenSelect: ArkSelect.HiddenSelect, Context: ArkSelect.Context,};
export { createListCollection } from "@ark-ui/react/select";
Examples
Installable blocks built with Select — real use cases, delivered as source you own.
Language Select
Locale picker with native language names, English hints, and BCP-47 values — ready for i18n and profile settings forms.
EnglishEnglish
简体中文Chinese (Simplified)
NederlandsDutch
FrançaisFrench
DeutschGerman
ItalianoItalian
日本語Japanese
한국어Korean
Português (Brasil)Portuguese (Brazil)
EspañolSpanish