A select: a field-shaped trigger opens a floating listbox of options — with grouped sections, optional leading icons, a placeholder, and a trailing check marking the current value. The trigger wears the field look (a calm, ring-less accent border on focus); the popup is an elevated surface.
Basic
A Select.Trigger (holding a Select.Value and a Select.Icon) opens the Select.Popup. Pass items on Select.Root so Select.Value renders the chosen item's label rather than its raw value, and defaultValue for an initial selection.
"use client";
import { Select } from "@stridge/noctis/select";
const FRUIT = {
apple: "Apple",
banana: "Banana",
cherry: "Cherry",
grape: "Grape",
};
export default function SelectBasic() {
return (
<Select.Root items={FRUIT} defaultValue="apple">
<Select.Trigger aria-label="Fruit" className="w-48">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
<Select.Item value="apple">Apple</Select.Item>
<Select.Item value="banana">Banana</Select.Item>
<Select.Item value="cherry">Cherry</Select.Item>
<Select.Item value="grape">Grape</Select.Item>
</Select.Popup>
</Select.Root>
);
}
Sizes
Select.Root takes a size (md | lg, default md) shared with the trigger — it sets the trigger's control height, inline padding, and value type size off the shared, density-aware control scale, matching the rest of the field family. The popup's rows lift their label type to match, so an open menu reads at the same size as the value that opened it; the other row metrics keep one comfortable density.
"use client";
import { Select } from "@stridge/noctis/select";
const SPEED = { slow: "Slow", normal: "Normal", fast: "Fast" };
export default function SelectSizes() {
return (
<div className="flex items-center gap-4">
<Select.Root items={SPEED} defaultValue="normal" size="md">
<Select.Trigger aria-label="Speed (medium)" className="w-36">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
<Select.Item value="slow">Slow</Select.Item>
<Select.Item value="normal">Normal</Select.Item>
<Select.Item value="fast">Fast</Select.Item>
</Select.Popup>
</Select.Root>
<Select.Root items={SPEED} defaultValue="normal" size="lg">
<Select.Trigger aria-label="Speed (large)" className="w-40">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
<Select.Item value="slow">Slow</Select.Item>
<Select.Item value="normal">Normal</Select.Item>
<Select.Item value="fast">Fast</Select.Item>
</Select.Popup>
</Select.Root>
</div>
);
}
Leading content
Give a Select.Item an icon — a glyph, a colour dot, or an avatar — and it sits in a reserved leading column so labels stay aligned whether or not a row has one. The selected row's check trails the row, so unselected labels stay flush to the leading edge. This is the status/priority/assignee-picker shape.
"use client";
import { Select } from "@stridge/noctis/select";
/** A status picker: every row carries a leading colour dot, the selected row a trailing check. */
const STATUS = [
{ value: "backlog", label: "Backlog", dot: "bg-chart-1" },
{ value: "todo", label: "Todo", dot: "bg-chart-2" },
{ value: "in-progress", label: "In progress", dot: "bg-chart-3" },
{ value: "in-review", label: "In review", dot: "bg-chart-4" },
{ value: "done", label: "Done", dot: "bg-chart-5" },
];
const ITEMS = Object.fromEntries(STATUS.map((s) => [s.value, s.label]));
export default function SelectWithIcons() {
return (
<Select.Root items={ITEMS} defaultValue="in-progress">
<Select.Trigger aria-label="Status" className="w-48">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
{STATUS.map((s) => (
<Select.Item key={s.value} value={s.value} icon={<span className={`size-2.5 rounded-full ${s.dot}`} />}>
{s.label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
}
Wide leading content
The leading column is a square minimum, not a fixed square, so the leading element can be wider than a glyph — a preview chip, a badge, an avatar-with-text. The column grows to fit it (no squish), and the overlay placement keeps aligning the row's label under the trigger's value because the row and the trigger lead with the same element. This theme picker leads every row with a wide swatch (canvas, accent dot, and a legible Aa sample).
"use client";
import { Select } from "@stridge/noctis/select";
import { getTextColor, parseColor, toCss } from "@stridge/noctis/theme";
import { useState } from "react";
/** The legible text colour the engine would put on `bg` — mirrors how a real app previews a theme. */
function legibleOn(bg: string): string {
try {
return toCss(getTextColor(parseColor(bg)));
} catch {
return "currentColor";
}
}
const THEMES = [
{ id: "system", label: "System preference", bg: "oklch(0.13 0 0)", accent: "oklch(0.62 0.19 277)" },
{ id: "light", label: "Light", bg: "oklch(0.99 0 0)", accent: "oklch(0.62 0.19 277)" },
{ id: "dark", label: "Dark", bg: "oklch(0.13 0 0)", accent: "oklch(0.62 0.19 277)" },
{ id: "hc-light", label: "Light High Contrast", bg: "oklch(1 0 0)", accent: "oklch(0.55 0.24 277)" },
{ id: "hc-dark", label: "Dark High Contrast", bg: "oklch(0.1 0 0)", accent: "oklch(0.72 0.18 277)" },
{ id: "custom", label: "Custom", bg: "oklch(0.18 0.03 277)", accent: "oklch(0.7 0.2 330)" },
];
/** A WIDE leading swatch (canvas + accent dot + legible `Aa`) — deliberately not a square glyph. */
function Swatch({ bg, accent }: { bg: string; accent: string }) {
return (
<span
aria-hidden
className="flex shrink-0 items-center gap-1.5 rounded-xs border border-field px-1.5 py-0.5"
style={{ background: bg }}
>
<span className="size-2 rounded-full" style={{ background: accent }} />
<span className="text-mini leading-none font-semibold" style={{ color: legibleOn(bg) }}>
Aa
</span>
</span>
);
}
/**
* The trigger and every row lead with the same wide preview chip, and the popup uses the default overlay
* placement: the leading column grows to the chip's width (no squish), and the selected row's label still
* lines up under the trigger's value because the row column and the trigger chip are the same element.
*/
export default function SelectWideLeadingContent() {
const [value, setValue] = useState("dark");
const current = THEMES.find((t) => t.id === value)!;
return (
<Select.Root
value={value}
onValueChange={(v) => v && setValue(v)}
items={THEMES.map((t) => ({ value: t.id, label: t.label }))}
>
<Select.Trigger aria-label="Theme" className="w-64">
<Swatch bg={current.bg} accent={current.accent} />
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
{THEMES.map((t) => (
<Select.Item key={t.id} value={t.id} icon={<Swatch bg={t.bg} accent={t.accent} />}>
{t.label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
}
Multiple
Pass multiple on Select.Root for a multi-select: picking a row toggles it without closing the popup, every selected row keeps its check, and Select.Value summarises the selection as a localized "N selected" in the trigger (override it with a child function for a custom display). Seed it with an array defaultValue.
"use client";
import { Select } from "@stridge/noctis/select";
const TOPPINGS = {
cheese: "Cheese",
pepperoni: "Pepperoni",
mushroom: "Mushroom",
onion: "Onion",
olive: "Olive",
};
export default function SelectMultiple() {
return (
<Select.Root items={TOPPINGS} multiple defaultValue={["cheese", "mushroom"]}>
<Select.Trigger aria-label="Toppings" className="w-56">
<Select.Value placeholder="Select toppings" />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
<Select.Item value="cheese">Cheese</Select.Item>
<Select.Item value="pepperoni">Pepperoni</Select.Item>
<Select.Item value="mushroom">Mushroom</Select.Item>
<Select.Item value="onion">Onion</Select.Item>
<Select.Item value="olive">Olive</Select.Item>
</Select.Popup>
</Select.Root>
);
}
Placement
The popup opens in one of two placements. By default it overlays the trigger (item-aligned, so the selected row lines up over the trigger's value — the macOS-native feel; mouse input only, and auto-disabled when there isn't room). Pass alignItemWithTrigger={false} on Select.Popup to anchor it below the trigger instead, like a standard dropdown.
"use client";
import { Select } from "@stridge/noctis/select";
const VIEW = { board: "Board", list: "List", timeline: "Timeline" };
/** The two popup placements: overlay (item-aligned, paints over the trigger) and below (anchored under it). */
export default function SelectPlacement() {
return (
<div className="flex items-center gap-4">
<Select.Root items={VIEW} defaultValue="list">
<Select.Trigger aria-label="View (overlay)" className="w-40">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
<Select.Item value="board">Board</Select.Item>
<Select.Item value="list">List</Select.Item>
<Select.Item value="timeline">Timeline</Select.Item>
</Select.Popup>
</Select.Root>
<Select.Root items={VIEW} defaultValue="list">
<Select.Trigger aria-label="View (below)" className="w-40">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup alignItemWithTrigger={false}>
<Select.Item value="board">Board</Select.Item>
<Select.Item value="list">List</Select.Item>
<Select.Item value="timeline">Timeline</Select.Item>
</Select.Popup>
</Select.Root>
</div>
);
}
Content width
By default the popup tracks the trigger — at least its width, growing to fit the longest row. Pass width="content" on Select.Popup to size the menu to its longest row instead, independent of the trigger: the menu hugs its widest icon + label + check, and every row reserves the trailing check column so the width never shifts as it opens, scrolls, or the selection moves between a short and a long row. This is the tidy-picker shape — a narrow trigger opening a menu sized to its content, with a fixed check gutter. It composes with both placements; under the overlay the selected row still lines up under the trigger value, the popup just no longer stretches to the trigger's trailing edge.
"use client";
import { Select } from "@stridge/noctis/select";
/**
* A timezone picker with options of deliberately different lengths and a leading dot — including one
* very long row to show the menu cap at the sensible max-width (the row then ellipsizes, keeping its
* reserved check column).
*/
const ZONES = [
{ value: "utc", label: "UTC" },
{ value: "lisbon", label: "Lisbon" },
{ value: "sao-paulo", label: "São Paulo" },
{ value: "kolkata", label: "Kolkata (UTC+5:30)" },
{ value: "auckland", label: "Auckland — Chatham" },
{ value: "kiritimati", label: "Kiritimati / Christmas Island, Line Islands (UTC+14:00)" },
];
const ITEMS = Object.fromEntries(ZONES.map((z) => [z.value, z.label]));
/**
* `width="content"` sizes the popup to its longest row rather than to the trigger. The trigger is narrower
* than the menu and simply truncates the selected value, yet the menu opens as wide as its longest row plus
* the reserved trailing check column — and that width never shifts as the selection moves between a short
* row (UTC) and a long one, because every row reserves the check gutter. The menu still caps at a sensible
* max-width, so an over-long row ellipsizes (keeping its check column) instead of growing without bound.
*/
export default function SelectContentWidth() {
return (
<Select.Root items={ITEMS} defaultValue="auckland">
<Select.Trigger aria-label="Timezone" className="w-40">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup width="content">
{ZONES.map((z) => (
<Select.Item key={z.value} value={z.value} icon={<span className="size-2 rounded-full bg-chart-3" />}>
{z.label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
}
Long lists
A long list caps at the available viewport height and scrolls. The list's scrollbar is hidden — sticky scroll arrows appear at the popup's top and bottom edges and scroll on hover (the Radix/macOS pattern), while the wheel, trackpad, and arrow keys scroll as usual. The keyboard highlight always stays in view.
"use client";
import { Select } from "@stridge/noctis/select";
const TIMEZONES = Array.from({ length: 25 }, (_, i) => {
const offset = i - 12;
const sign = offset >= 0 ? "+" : "-";
const label = `UTC${sign}${String(Math.abs(offset)).padStart(2, "0")}:00`;
return { value: label, label };
});
const ITEMS = Object.fromEntries(TIMEZONES.map((tz) => [tz.value, tz.label]));
export default function SelectScrollable() {
return (
<Select.Root items={ITEMS} defaultValue="UTC+00:00">
<Select.Trigger aria-label="Timezone" className="w-48">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
{TIMEZONES.map((tz) => (
<Select.Item key={tz.value} value={tz.value}>
{tz.label}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
);
}
Groups
Wrap related options in a Select.Group with a Select.GroupLabel — the label is muted, non-interactive, and announced as the group's name — and divide groups with a Select.Separator.
"use client";
import { Select } from "@stridge/noctis/select";
const FOOD = {
apple: "Apple",
banana: "Banana",
carrot: "Carrot",
potato: "Potato",
};
export default function SelectGroups() {
return (
<Select.Root items={FOOD}>
<Select.Trigger aria-label="Food" className="w-48">
<Select.Value placeholder="Choose a food" />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
<Select.Group>
<Select.GroupLabel>Fruit</Select.GroupLabel>
<Select.Item value="apple">Apple</Select.Item>
<Select.Item value="banana">Banana</Select.Item>
</Select.Group>
<Select.Separator />
<Select.Group>
<Select.GroupLabel>Vegetable</Select.GroupLabel>
<Select.Item value="carrot">Carrot</Select.Item>
<Select.Item value="potato" disabled>
Potato
</Select.Item>
</Select.Group>
</Select.Popup>
</Select.Root>
);
}
In a field
Drop a Select straight into a Field.Root — its trigger auto-wires to the field, so the Field.Label, Field.Description, and Field.Error are associated for assistive tech and the trigger turns invalid (a danger border) when the field does. Render the label as a <span> with nativeLabel={false}, since it labels a button rather than a native control, mark the field required, and write the placeholder as a prompt to act ("Select a framework"), not a fake value.
Used to scaffold the starter project.
"use client";
import { Field } from "@stridge/noctis/field";
import { Select } from "@stridge/noctis/select";
const FRAMEWORK = {
next: "Next.js",
remix: "Remix",
astro: "Astro",
nuxt: "Nuxt",
};
export default function SelectField() {
return (
<Field.Root name="framework" className="w-full max-w-sm">
{/* A `<label>` would promise native click-to-focus behaviour that a button-triggered
select can't honour, so the label renders as a span and wires up through ARIA instead. */}
<Field.Label nativeLabel={false} render={<span />}>
Framework{" "}
<span aria-hidden="true" className="text-danger">
*
</span>
</Field.Label>
<Select.Root items={FRAMEWORK} required>
<Select.Trigger className="w-full">
<Select.Value placeholder="Select a framework" />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
<Select.Item value="next">Next.js</Select.Item>
<Select.Item value="remix">Remix</Select.Item>
<Select.Item value="astro">Astro</Select.Item>
<Select.Item value="nuxt">Nuxt</Select.Item>
</Select.Popup>
</Select.Root>
<Field.Description>Used to scaffold the starter project.</Field.Description>
<Field.Error match="valueMissing">Please select a framework.</Field.Error>
</Field.Root>
);
}
Controlled
Drive the selection yourself with value + onValueChange on Select.Root (set value to null to clear it).
Selected: apple
"use client";
import { Button } from "@stridge/noctis/button";
import { Select } from "@stridge/noctis/select";
import { useState } from "react";
const FRUIT = { apple: "Apple", banana: "Banana", cherry: "Cherry" };
export default function SelectControlled() {
const [value, setValue] = useState<string | null>("apple");
return (
<div className="flex w-full max-w-xs flex-col items-start gap-3">
<Select.Root items={FRUIT} value={value} onValueChange={setValue}>
<Select.Trigger aria-label="Fruit" className="w-full">
<Select.Value placeholder="Select a fruit" />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
<Select.Item value="apple">Apple</Select.Item>
<Select.Item value="banana">Banana</Select.Item>
<Select.Item value="cherry">Cherry</Select.Item>
</Select.Popup>
</Select.Root>
<p className="text-sm text-secondary">
Selected: <span className="font-medium text-foreground">{value ?? "none"}</span>
</p>
<Button variant="link" size="sm" className="self-start" onClick={() => setValue(null)}>
Reset
</Button>
</div>
);
}
Select or Combobox?
Reach for Select when the options are a short, fixed list (roughly ten or fewer) the user picks from — it shows only the chosen value and never accepts typed text. When the list is long or the user needs to filter by typing, reach for Combobox instead.
Keyboard
| Key | Action |
|---|---|
| Enter / Space / ↓ / ↑ | On the trigger: open the popup, highlighting the selected option (or the first / last). |
| Alt + ↓ | Open the popup without moving the highlight. |
| ↓ / ↑ | Move the highlight to the next / previous option. |
| PageDown / PageUp | Jump the highlight by a page (about ten options). |
| Home / End | First / last option. |
| Enter / Space | Select the highlighted option and close the popup, returning focus to the trigger. |
| Alt + ↑ | Select the highlighted option and close the popup. |
| Esc | Close the popup without changing the value; return focus to the trigger. |
| Tab | Close the popup and move focus on. |
| Characters | Typeahead — jump to the option whose label matches what you type; a non-matching key never moves the highlight, and repeats of one character cycle the matches. |
Accessibility
- The trigger follows the APG select-only combobox pattern: it is a
role="combobox"button witharia-expanded, focus stays on it (the popup is arole="listbox"ofrole="option"s driven byaria-activedescendant), and the chosen option carriesaria-selected. - Disabled options stay reachable by the keyboard and are announced as disabled — they just can't be selected. Removing them from navigation would hide why a choice is unavailable from screen-reader users.
- The scroll arrows are a pointer affordance (they scroll on hover and never render for touch), so they are
aria-hidden— keyboard users scroll with the arrow keys. - In a
Field, the trigger wiresaria-describedbyto the description and error, takesaria-invalidwhile invalid, andaria-requiredwhen the field isrequired; the visual required marker is decorative (aria-hidden). - Multiple selects convey their count through the trigger's "N selected" summary, and every selected option keeps
aria-selected. - The chevron, the leading column, and the popup alignment are all direction-aware, so the select mirrors under RTL by construction.
Anatomy
Compose a select from its parts. Select.Root owns the value and open state (it accepts every Base UI Select.Root prop — value, defaultValue, onValueChange, items, multiple, readOnly, required, modal, plus the name/form props — and adds size and an additive invalid override).
Select.Root— owns the value and open state and shares the controlsize,multiple, andinvalid; renders no element of its own.Select.Trigger— the field-shaped control that opens the popup; reads the root'ssizeand paints the danger border while invalid, the quiet chrome whilereadOnly.Select.Value— renders the selected label inside the trigger (or a localized "N selected" undermultiple), falling back toplaceholder.Select.Icon— the trailing chevron marking the control as a select.Select.Backdrop— the modal scrim; rendered for you bySelect.Popuponly when the select ismodal(off by default, so the page keeps scrolling while the popup is open), transparent.Select.Popup— the floating, elevated, animated listbox. Props:side(defaultbottom),align(defaultstart),sideOffset,alignItemWithTrigger(defaulttrue),collisionPadding,width(trigger|content, defaulttrigger—contentsizes the menu to its longest row and reserves the trailing check column on every row).Select.ScrollUpArrow+Select.ScrollDownArrow— the sticky scroll affordances; rendered for you bySelect.Popup, appearing only while the list can scroll.Select.List— the scrollable list inside the popup; rendered for you.Select.Item— a selectable option carrying itsvalue. In its convenience mode it takes labelchildrenplus optionalicon/descriptionprops and renders the parts (and the selected-check) for you. Hand-compose the parts instead for full control: when any item part appears as a direct child,Select.Itemrenders your children verbatim and skips its own scaffold, so composing never doubles the label or check (theicon/descriptionprops are ignored in that mode).Select.ItemIcon/Select.ItemText/Select.ItemDescription/Select.ItemIndicator— the row's leading column, label, muted second line, and selected-check. Rendered for you in convenience mode; compose them by hand for custom inner structure. A bareSelect.ItemIndicatorrenders the lucide check (pass children to override the glyph).Select.Group+Select.GroupLabel— a labelled section of related options.Select.Separator— a presentational hairline between groups.
Every rendered part carries a data-slot (select-trigger, select-value, select-icon, select-backdrop, select-popup, select-scroll-up-arrow, select-scroll-down-arrow, select-list, select-item, select-item-icon, select-item-text, select-item-description, select-item-indicator, select-group, select-group-label, select-separator) for host-side styling — pair it with the Base UI state attributes (data-open, data-placeholder, data-invalid, data-readonly, data-highlighted, data-selected, data-disabled).
Theme scope
ThemeScope re-tunes a whole region's shape — corner radius, density, and type scale — through the cascade, with no per-component prop and without touching the app-wide theme. Tweak the knobs below and watch every Select inside the scope retune together. See Customization for nesting, reset, and the portal caveat.
"use client";
import { DENSITY_PRESETS, FONT_SCALE_PRESETS, RADIUS_PRESETS, ThemeScope } from "@stridge/noctis";
import { Select } from "@stridge/noctis/select";
import { useState } from "react";
/** Title-case a preset key for its select label. */
const label = (key: string) => key.charAt(0).toUpperCase() + key.slice(1);
/** One labelled preset picker — its options are a shipped `ThemeScope` preset map, so the playground's
* vocab is exactly what the primitive accepts. */
function Knob({
name,
presets,
value,
onChange,
}: {
name: string;
presets: Record<string, number>;
value: string;
onChange: (value: string) => void;
}) {
const keys = Object.keys(presets);
const items = Object.fromEntries(keys.map((key) => [key, label(key)]));
return (
<label className="flex flex-col gap-1.5">
<span className="text-mini font-medium text-subtle">{name}</span>
<Select.Root items={items} value={value} onValueChange={(next) => onChange(String(next))}>
<Select.Trigger aria-label={name} className="w-40">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
{keys.map((key) => (
<Select.Item key={key} value={key}>
{label(key)}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
</label>
);
}
/**
* A live `ThemeScope` playground: the three seed knobs (radius, density, type scale) drive a scope
* wrapping a select cluster, so every control inside re-tunes together — corners, spacing, and text
* size — with no per-select prop and without touching the app-wide theme.
*/
export default function SelectThemeScope() {
const [radius, setRadius] = useState<keyof typeof RADIUS_PRESETS>("pill");
const [density, setDensity] = useState<keyof typeof DENSITY_PRESETS>("default");
const [fontScale, setFontScale] = useState<keyof typeof FONT_SCALE_PRESETS>("default");
const FRUIT = { apple: "Apple", banana: "Banana", cherry: "Cherry", grape: "Grape" };
return (
<div className="flex flex-col gap-5">
<div className="flex flex-wrap gap-3">
<Knob name="Radius" presets={RADIUS_PRESETS} value={radius} onChange={(v) => setRadius(v as typeof radius)} />
<Knob
name="Density"
presets={DENSITY_PRESETS}
value={density}
onChange={(v) => setDensity(v as typeof density)}
/>
<Knob
name="Font scale"
presets={FONT_SCALE_PRESETS}
value={fontScale}
onChange={(v) => setFontScale(v as typeof fontScale)}
/>
</div>
<div className="rounded-md border border-dashed border-border p-4">
<ThemeScope radius={radius} density={density} fontScale={fontScale}>
<div className="flex flex-wrap items-center gap-3">
<Select.Root items={FRUIT} defaultValue="apple">
<Select.Trigger aria-label="Fruit" className="w-40">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
<Select.Item value="apple">Apple</Select.Item>
<Select.Item value="banana">Banana</Select.Item>
<Select.Item value="cherry">Cherry</Select.Item>
<Select.Item value="grape">Grape</Select.Item>
</Select.Popup>
</Select.Root>
<Select.Root items={{ sm: "Small", md: "Medium", lg: "Large" }} defaultValue="md" size="lg">
<Select.Trigger aria-label="Size" className="w-40">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
<Select.Item value="sm">Small</Select.Item>
<Select.Item value="md">Medium</Select.Item>
<Select.Item value="lg">Large</Select.Item>
</Select.Popup>
</Select.Root>
</div>
</ThemeScope>
</div>
</div>
);
}
On surfaces
The same control re-tuned across the elevation scopes — the root canvas, an elevated panel, a menu, and a sunken well. It stays legible on every layer.
Design tokens
Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every select in that region retunes — e.g. .dense { --noctis-select-item-height: 1.75rem; } tightens every popup opened beneath it. Knobs that aren't minted are reached through the part's data-slot. See Customization for the full override ladder and Tokens for the whole graph.
API reference
Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table; parts that only forward to Base UI list just the props they pass through.