Naked UI

NakedCombobox

Editable selection with filtered options

Use a combobox when users should type to filter choices. Keep the text controller, selected value and filtering in the caller. NakedCombobox<T> manages the option overlay, highlight, keyboard selection and semantics. NakedComboboxOption<T> supplies each option’s interaction state.

Keep option values stable across filtering. Dispose your text and focus controllers. Provide an accessible label for the editable field and a visible label for every option.

Basic implementation

apps/naked_ui_example/lib/api/naked_combobox.0.dart
import 'package:flutter/material.dart';
import 'package:naked_ui/naked_ui.dart';

import '../src/example_app.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const ExampleApp(child: Center(child: ComboboxExample()));
  }
}

class ComboboxExample extends StatelessWidget {
  const ComboboxExample({super.key});

  static const fruits = ['Apple', 'Apricot', 'Banana', 'Blueberry', 'Cherry'];

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 240,
      child: NakedCombobox<String>(
        optionsBuilder: (value) {
          final query = value.text.toLowerCase();
          return fruits.where((fruit) => fruit.toLowerCase().startsWith(query));
        },
        onSelected: (_) {},
        fieldBuilder:
            (context, state, controller, focusNode, onFieldSubmitted) {
              return NakedTextField(
                controller: controller,
                focusNode: focusNode,
                onSubmitted: (_) => onFieldSubmitted(),
                builder: (context, fieldState, child) {
                  return Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 12,
                      vertical: 10,
                    ),
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(8),
                      border: Border.all(
                        color: fieldState.isFocused
                            ? Colors.blue
                            : Colors.grey.shade300,
                      ),
                    ),
                    child: child,
                  );
                },
              );
            },
        overlayBuilder: (context, options) {
          return Padding(
            padding: const .only(top: 4),
            child: Material(
              elevation: 4,
              shadowColor: Colors.black.withAlpha(50),
              clipBehavior: .antiAlias,
              color: Colors.white,
              shape: RoundedRectangleBorder(
                borderRadius: .circular(10),
                side: BorderSide(color: Colors.grey.shade300),
              ),
              child: Column(
                mainAxisSize: .min,
                children: [
                  for (final fruit in options)
                    NakedCombobox.Option<String>(
                      value: fruit,
                      builder: (context, state, _) {
                        return Container(
                          width: .infinity,
                          color: state.isHighlighted
                              ? Colors.blue.shade50
                              : null,
                          padding: const .symmetric(
                            horizontal: 12,
                            vertical: 8,
                          ),
                          child: Text(fruit),
                        );
                      },
                    ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

Source and accessibility

See the package implementation for the complete API. Keep visible labels, focus indication and sufficient contrast in your presentation layer.

View page source on GitHub