---
title: NakedSelect
description: Headless single-select dropdown with typed trigger and option builders
keywords: [flutter, select, dropdown, listbox, headless, overlay]
---

Headless select dropdown component. Handles keyboard navigation, focus management, and accessibility. Use builder pattern for custom trigger and option styling.

## When to use this

- **Form fields**: Choose from a list of options (country, category, etc.)
- **Filters**: Select filter criteria from predefined options
- **Settings**: Choose preferences from dropdown lists
- **Data selection**: Pick items from large sets with search/grouping

<Info>
  Working samples are in [`packages/example/lib/api/naked_select.0.dart`](https://github.com/btwld/naked_ui/blob/main/packages/example/lib/api/naked_select.0.dart) (plus `.1`/`.2` for searchable and grouped variants).
</Info>

## Basic implementation

```dart
import 'package:flutter/material.dart';
import 'package:naked_ui/naked_ui.dart';

class BasicSelectExample extends StatefulWidget {
  const BasicSelectExample({super.key});

  @override
  State<BasicSelectExample> createState() => _BasicSelectExampleState();
}

class _BasicSelectExampleState extends State<BasicSelectExample> {
  String? _value = 'apple';

  @override
  Widget build(BuildContext context) {
    return NakedSelect<String>(
      value: _value,
      onChanged: (next) => setState(() => _value = next),
      builder: (context, state, _) {
        final bool isOpen = state.isOpen;
        final bool hovered = state.isHovered;
        final bool focused = state.isFocused;

        return Container(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(12),
            color: hovered ? Colors.grey.shade100 : Colors.white,
            border: Border.all(
              color: focused ? Colors.blue : Colors.grey.shade300,
              width: focused ? 2 : 1,
            ),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(state.value ?? 'Select fruit'),
              const SizedBox(width: 12),
              Icon(isOpen ? Icons.expand_less : Icons.expand_more, size: 18),
            ],
          ),
        );
      },
      overlayBuilder: (context, info) {
        return Material(
          elevation: 6,
          borderRadius: BorderRadius.circular(12),
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxHeight: 220, minWidth: 220),
            child: ListView(
              padding: const EdgeInsets.symmetric(vertical: 6),
              shrinkWrap: true,
              children: const [
                _SelectOption(value: 'apple', label: 'Apple'),
                _SelectOption(value: 'banana', label: 'Banana'),
                _SelectOption(value: 'mango', label: 'Mango'),
              ],
            ),
          ),
        );
      },
    );
  }
}

class _SelectOption extends StatelessWidget {
  const _SelectOption({required this.value, required this.label});

  final String value;
  final String label;

  @override
  Widget build(BuildContext context) {
    return NakedSelect.Option<String>(
      value: value,
      builder: (context, optionState, _) {
        final bool isSelected = optionState.isSelected;
        final bool isHovered = optionState.isHovered;

        return Container(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
          color: isSelected
              ? Colors.blue.withOpacity(0.12)
              : isHovered
                  ? Colors.grey.shade100
                  : null,
          child: Row(
            children: [
              Text(label),
              if (isSelected) ...[
                const Spacer(),
                const Icon(Icons.check, size: 16, color: Colors.blue),
              ],
            ],
          ),
        );
      },
    );
  }
}
```

## State Snapshots

### Trigger – `NakedSelectState<T>`
- `isOpen` → overlay visibility
- `value` → currently selected value (nullable)
- `hasValue` helper plus raw `widgetStates` from the trigger builder

### Options – `NakedSelectOptionState<T>`
- `value` → option value
- `isSelected` → convenience getter supplied via `WidgetState.selected`
- `widgetStates` → hover/focus/pressed/disabled information

### Access from Context

- `NakedSelectState.of<T>(context)` / `maybeOf<T>(context)` → read the trigger state
- `NakedSelectOptionState.of<T>(context)` / `maybeOf<T>(context)` → read the option state inside overlays
- `NakedSelectState.controllerOf(context)` / `maybeControllerOf(context)` → obtain the shared `WidgetStatesController`

## NakedSelect Constructor

```dart
const NakedSelect({
  Key? key,
  this.child,
  this.builder,
  required this.overlayBuilder,
  this.value,
  this.onChanged,
  this.closeOnSelect = true,
  this.closeOnClickOutside = true,
  this.enabled = true,
  this.mouseCursor = SystemMouseCursors.click,
  this.triggerFocusNode,
  this.semanticLabel,
  this.positioning = const OverlayPositionConfig(
    targetAnchor: Alignment.bottomCenter,
    followerAnchor: Alignment.topCenter,
  ),
  this.onOpen,
  this.onClose,
  this.onCanceled,
  this.onOpenRequested,
  this.onCloseRequested,
  this.consumeOutsideTaps = true,
  this.useRootOverlay = false,
  this.excludeSemantics = false,
})
```

### Key Parameters

- `child` / `builder` → provide static trigger content or a `ValueWidgetBuilder<NakedSelectState<T>>`
- `overlayBuilder` → `(BuildContext, RawMenuAnchorOverlayInfo)` – build any list/panel/search UI
- `value` / `onChanged` → manage the selected value externally (falls back to internal state when `value` is null)
- `positioning` → tune `OverlayPositionConfig` alignment, fallback, and offset for the dropdown shell
- `closeOnSelect` / `closeOnClickOutside` → opt into dismissal after selection and pointer taps outside
- `consumeOutsideTaps` / `useRootOverlay` → control how outside gestures are captured and which overlay stack is used
- `enabled` → disables trigger & option activation when false
- `mouseCursor` → set the trigger cursor when the select is enabled
- `triggerFocusNode` → reuse an external focus node; focus is restored here automatically on close
- `semanticLabel` → override trigger semantics; the current `value?.toString()` is announced when provided
- `excludeSemantics` → hide the select trigger subtree from the semantic tree; opened overlay content remains exposed
- Lifecycle hooks: `onOpen`, `onClose`, `onCanceled` fire around overlay visibility; `onOpenRequested`, `onCloseRequested` intercept RawMenuAnchor requests (e.g. for animations)

## Options via `NakedSelect.Option`

`NakedSelect.Option<T>` is a factory for `NakedSelectOption<T>`:

```dart
const NakedSelect.Option({
  Key? key,
  required T value,
  Widget? child,
  ValueWidgetBuilder<NakedSelectOptionState<T>>? builder,
  bool enabled = true,
  String? semanticLabel,
})
```

Use the builder to react to `optionState.isSelected` or `optionState.widgetStates` for hover styling. Options respect the select's `enabled` and `closeOnSelect` settings automatically.

## Accessibility Notes

- Each option is focusable and keyboard navigable; provide textual labels
- **PageUp/PageDown**: Move focus by ~10 traversal steps for faster navigation (policy-driven)
- Trigger announces `semanticLabel` and selected value; set both for clarity
- Respect `closeOnSelect` for quick dismissal or keep it `false` for multi-step flows
- Keep options at least 44×44 logical pixels for touch friendliness
