All components
Select
Custom listbox-style select with trigger, indicator, and item slots in outline and subtle styles.
Framework
1234567891011121314151617181920212223242526272829303132import { Select } from "#components/ui/base-ui/select.tsx";
const frameworks = ["Astro", "Next.js", "Remix", "Vite"];
export function Example() { return ( // variant/size resolve once on Root; every part inherits them via // context — Icon and ItemIndicator ship default icons <Select.Root defaultValue="Next.js" variant="outline" size="md"> <Select.Label>Framework</Select.Label> <Select.Control> <Select.Trigger> <Select.Value /> </Select.Trigger> <Select.IndicatorGroup> <Select.Icon /> </Select.IndicatorGroup> </Select.Control> {/* Positioner portals the popup and positions it under the trigger */} <Select.Positioner> <Select.Popup> {frameworks.map((item) => ( <Select.Item key={item} value={item}> <Select.ItemText>{item}</Select.ItemText> <Select.ItemIndicator /> </Select.Item> ))} </Select.Popup> </Select.Positioner> </Select.Root> );}Variants
Framework
outlineFramework
subtleSizes
Framework
xsFramework
smFramework
mdFramework
lgInstall
1npx stylexblocks add selectSource
components/ui/base-ui/select.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605import * as stylex from "@stylexjs/stylex";import { Select as BaseSelect, type SelectRootProps } from "@base-ui-components/react/select";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles, type SlotRecipeVariants, type StyleOverrideProps,} 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(--anchor-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>;
function SelectRootNode( props: SelectRootProps<unknown> & { className?: string; style?: React.CSSProperties },) { const { className, style, children, ...rest } = props; return ( <div className={className} style={style}> <BaseSelect.Root {...rest}>{children}</BaseSelect.Root> </div> );}
const StyledRoot = withProvider(SelectRootNode, "root", "SelectRoot");
/** * Select — Base UI's select parts styled with the StyleX slot recipe. * `<Select.Root variant size>` renders the recipe's root layout element * around Base UI's context-only Root; every part inherits the variants * through context. Popup portals through `Select.Positioner`, and Icon / * ItemIndicator ship default chevron / check icons. */function Root<Value, Multiple extends boolean | undefined = false>( props: SelectRootProps<Value, Multiple> & Partial<SelectVariants> & StyleOverrideProps & { className?: string },) { return <StyledRoot {...(props as React.ComponentProps<typeof StyledRoot>)} />;}Root.displayName = "SelectRoot";
const StyledIcon = withContext(BaseSelect.Icon, "indicator", "SelectIcon");
function Icon( props: React.ComponentProps<typeof BaseSelect.Icon> & StyleOverrideProps,) { const { children, ...rest } = props; return ( <StyledIcon {...rest}> {children ?? <ChevronDownIcon {...stylex.props(selectIndicatorIcon.base)} />} </StyledIcon> );}Icon.displayName = "SelectIcon";
const StyledItemIndicator = withContext( BaseSelect.ItemIndicator, (v) => [ ...resolveSlotStyles(selectRecipe.slots.itemIndicator, v), selectItemIndicatorSizes.md, ], "SelectItemIndicator",);
function ItemIndicator( props: React.ComponentProps<typeof BaseSelect.ItemIndicator> & StyleOverrideProps,) { const { children, ...rest } = props; return ( <StyledItemIndicator {...rest}> {children ?? <CheckIcon {...stylex.props(selectItemIcon.base)} />} </StyledItemIndicator> );}ItemIndicator.displayName = "SelectItemIndicator";
function Positioner({ children, ...rest}: React.ComponentProps<typeof BaseSelect.Positioner>) { return ( <BaseSelect.Portal> <BaseSelect.Positioner sideOffset={4} alignItemWithTrigger={false} {...rest}> {children} </BaseSelect.Positioner> </BaseSelect.Portal> );}
export const Select = { Root, Label: withContext("span", "label", "SelectLabel"), Control: withContext( "div", (v) => [...resolveSlotStyles(selectRecipe.slots.control, v), chrome.anchor], "SelectControl", ), Trigger: withContext( BaseSelect.Trigger, (v) => [chrome.btnReset, ...resolveSlotStyles(selectRecipe.slots.trigger, v)], "SelectTrigger", ), Value: withContext(BaseSelect.Value, "valueText", "SelectValue"), IndicatorGroup: withContext( "div", (v) => [...resolveSlotStyles(selectRecipe.slots.indicatorGroup, v), chrome.indicatorGroup], "SelectIndicatorGroup", ), Icon, Positioner, Popup: withContext( BaseSelect.Popup, (v) => [...resolveSlotStyles(selectRecipe.slots.content, v), chrome.panel], "SelectPopup", ), Item: withContext( BaseSelect.Item, (v) => [...resolveSlotStyles(selectRecipe.slots.item, v), chrome.itemHover], "SelectItem", ), ItemText: withContext(BaseSelect.ItemText, "itemText", "SelectItemText"), ItemIndicator, Group: withContext(BaseSelect.Group, "itemGroup", "SelectGroup"), GroupLabel: withContext(BaseSelect.GroupLabel, "itemGroupLabel", "SelectGroupLabel"),};
Examples
Installable blocks built with Select — real use cases, delivered as source you own.
Language
Language Select
Locale picker with native language names, English hints, and BCP-47 values — ready for i18n and profile settings forms.