Select / dropdown
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' }, ]} /> </> );}// Android (Jetpack Compose, canonical). The shipped single-select dropdown is a// DropdownAnchor (recessed control-shaped field + ArrowDropDown) over a Material3// DropdownMenu of DropdownMenuItem rows. See chat/ui/SessionCreationDropdowns.kt.import androidx.compose.material3.DropdownMenuimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.Textimport com.nockerl.app.chat.ui.DropdownAnchor
@Composablefun ProviderDropdown( providers: List<ProviderInfo>, selectedProvider: ProviderInfo?, isLoading: Boolean, onSelect: (ProviderInfo) -> Unit,) { var expanded by remember { mutableStateOf(false) }
Box { DropdownAnchor( label = selectedProvider?.name ?: "Select a provider", placeholder = selectedProvider == null, isLoading = isLoading, onClick = { if (!isLoading && providers.isNotEmpty()) expanded = true }, ) DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { providers.forEach { provider -> DropdownMenuItem( text = { Text(provider.name) }, onClick = { onSelect(provider); expanded = false }, ) } } }}// macOS: SwiftUI (canonical). The shipped single-select is a Menu of Buttons; the// chosen row carries a leading checkmark via Label(systemImage:). The trigger is a// chevron.up.chevron.down on a recessed accent-outlined field. See UI/AppSettingsView.swift.Menu { Button { settings.selectedMicUID = "" } label: { micMenuLabel("System Default", selected: settings.selectedMicUID.isEmpty) } ForEach(micMonitor.devices) { mic in Button { settings.selectedMicUID = mic.uid } label: { micMenuLabel(mic.name, selected: settings.selectedMicUID == mic.uid) } }} label: { HStack(spacing: 8) { Text(currentMicName).lineLimit(1).truncationMode(.middle) Spacer(minLength: 0) Image(systemName: "chevron.up.chevron.down") } .foregroundStyle(NockerlTheme.accent) .padding(.horizontal, 12).padding(.vertical, 8) .background(NockerlTheme.canvasAlt, in: RoundedRectangle(cornerRadius: 12)) .overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(NockerlTheme.accent, lineWidth: 1.5))}.menuStyle(.borderlessButton).menuIndicator(.hidden)
// A checkmark on the selected row; a plain Text otherwise.@ViewBuilderfunc micMenuLabel(_ name: String, selected: Bool) -> some View { if selected { Label(name, systemImage: "checkmark") } else { Text(name) }}Parameters
Section titled “Parameters”| Prop | Type | Default | Description |
|---|---|---|---|
label * | string | Persistent label above the trigger, never a placeholder. | |
options * | NockerlSelectOption[] | The fixed set to pick one from. | |
value * | string | null | The chosen value, or null for the empty/placeholder state. | |
onChange * | (value: string) => void | Commit a single selection (the option value), then close the popover. | |
placeholder | string | 'Select…' | Shown when nothing is selected (label still persists above). |
helperText | string | Quiet supporting line under the well. | |
errorText | string | When set, the field reads invalid (warm border + icon + this message). | |
size | NockerlSelectSize | 'md' | Control height + type: 'md' (44px) | 'sm' (34px). |
grouped | boolean | false | Bucket options under their group header. |
disabled | boolean | false | Inert + still legible (never invisible). |
The single-select is composed from DropdownAnchor (the field) + Material3
DropdownMenu / DropdownMenuItem (the list). Parameters are those of
DropdownAnchor plus the menu items the caller emits.
| Parameter | Type | Default | Description |
|---|---|---|---|
label * | String | The value (or placeholder) shown in the anchor. Rendered with bodyMedium. | |
placeholder * | Boolean | When true, the label is rendered as muted placeholder text (nothing chosen yet). | |
isLoading * | Boolean | Shows an inline CircularProgressIndicator before the chevron and (by convention) gates opening. | |
enabled | Boolean | true | When false, the anchor is dimmed + filled (surfaceVariant) and non-clickable. |
onClick * | () -> Unit | Opens the caller-owned DropdownMenu (typically sets expanded = true). | |
expanded / onDismissRequest | Boolean / -> Unit | On the wrapping DropdownMenu: open state + outside-dismiss. | |
DropdownMenuItem(text, onClick, enabled) | @Composable | One option row: a Text label; onClick selects + dismisses; enabled = false for an inert option. |
SwiftUI builds the picker as a Menu whose label is the trigger and whose
content is a Button per option. There is no single Nockerl wrapper type; these
are the load-bearing modifiers + the per-row selection idiom.
| Style | Type | Default | Description |
|---|---|---|---|
Menu { content } label: { trigger } | View | The select. content is the option Buttons; label is the field (value text + chevron.up.chevron.down). | |
.menuStyle(.borderlessButton) | MenuStyle | Renders the trigger as a flat field, not a bordered system button. | |
.menuIndicator(.hidden) | View modifier | Hides the default disclosure chevron so the custom one is the only indicator. | |
Button { select } label: { Label(name, systemImage: "checkmark") } | View | One option. The SELECTED row uses Label(_, systemImage: "checkmark"); others are a plain Text. | |
.foregroundStyle(NockerlTheme.accent) + RoundedRectangle(cornerRadius: 12) | View modifiers | The recessed accent-outlined field look (control radius 12, accent label + border). |