Skip to content

Select / dropdown

In review
Live · web

Placeholder vs. a value: click or press Enter / ↓ to open

Nothing selected yet, so the placeholder holds the empty state.

Leading status mark · the chosen option carries a cyan check · one disabled option.

Grouped options: group labels, leading + secondary text

Cloud and local engines, bucketed under headers.

Error / invalid + disabled

A workspace is required to continue.

Inert, but the chosen value stays legible.

Sizes: sm and md

Committed 0 selections. The island is live.

// Web: React, consuming @dizyx/nockerl-tokens. NockerlSelect ships from
// @dizyx/nockerl-react: the recessed WELL trigger + the lifted popover listbox +
// the selected-row check + the full keyboard model are all in the package.
import { useState } from 'react';
import { NockerlSelect } from '@dizyx/nockerl-react';
export function SessionConfig() {
const [provider, setProvider] = useState<string | null>('cloud-personal');
const [model, setModel] = useState<string | null>('large-2-0');
return (
<>
<NockerlSelect
label="Provider"
value={provider}
onChange={setProvider}
placeholder="Select a provider…"
options={[
{ value: 'cloud-personal', label: 'cloud-personal', secondary: 'Cloud Agent', status: 'success' },
{ value: 'local', label: 'local', secondary: 'Local runtime', status: 'info' },
{ value: 'openrouter', label: 'openrouter', secondary: 'Cloud · billable', status: 'warning' },
]}
/>
<NockerlSelect
label="Model"
value={model}
onChange={setModel}
grouped
options={[
{ value: 'large-2-0', label: 'Large 2.0', secondary: 'coding', group: 'Cloud' },
{ value: 'qwen3-32b', label: 'Qwen3 32B', secondary: 'local', group: 'Local' },
]}
/>
</>
);
}
PropTypeDefaultDescription
label *stringPersistent label above the trigger, never a placeholder.
options *NockerlSelectOption[]The fixed set to pick one from.
value *string | nullThe chosen value, or null for the empty/placeholder state.
onChange *(value: string) => voidCommit a single selection (the option value), then close the popover.
placeholderstring'Select…'Shown when nothing is selected (label still persists above).
helperTextstringQuiet supporting line under the well.
errorTextstringWhen set, the field reads invalid (warm border + icon + this message).
sizeNockerlSelectSize'md'Control height + type: 'md' (44px) | 'sm' (34px).
groupedbooleanfalseBucket options under their group header.
disabledbooleanfalseInert + still legible (never invisible).
Pick the right picker

Select is the plainest picker: one value from a fixed list, no typing. Reach for a neighbour when you need more: Combobox when the user should type to filter a long list (autocomplete); Multi-select when they pick several values (token chips, per-row checkboxes, select-all); Menu when the rows are actions, not a value (icons, shortcuts, submenus).

Cross-platform drift (tracked for reconciliation)

The spec is in review; these are the real divergences between the shipped platforms:

  • Selected-row check (settled): a trailing check on the selected row is the shared expression. The web package stamps it and the Swift idiom here uses Label(_, systemImage: "checkmark"); the Compose idiom documented on this page reads the selection in the anchor only, so a trailing check on DropdownMenuItem is the piece this page specifies.
  • Control radius: the field is the radius.control token everywhere, reached through NockerlControlShape on Compose and NockerlRadius.control in SwiftUI. A literal corner radius on a picker is a law §11 violation rather than a platform difference.
  • Grouping + secondary text: web supports group headers and a secondary line per option. The Android and Swift idioms documented here are flat label-only rows, so grouping and the secondary line are specified on this page rather than mirrored from an app.
  • Sizing: web has size (sm/md); Compose’s anchor is fixed at 48dp; Swift is intrinsic.
  • Error state: web has a first-class errorText (warm border + icon + message); the apps surface validation outside the control today.