A cmd+k style search overlay for Flutter web — similar to Notion's search
popup.
Drop it in, wire up a keyboard shortcut, and your users get instant,
keyboard-navigable search.
- 🔍 Static & async data sources — filter a local list or call an API
- ⌨️ Full keyboard navigation — ↑↓ to move, Enter to select, Escape to close
- 🎨 Fully themeable — via
ThemeExtension, zero default opinions - ✨ Animated entry/exit —
ScaleTransition+FadeTransition(150 ms) - 🌐 Flutter web primary, mobile compatible
- 📦 Zero external dependencies — only the Flutter SDK
dependencies:
quick_search_flutter_web: ^0.1.0flutter pub getfinal controller = SpotlightController();
SpotlightSearch(
controller: controller,
delegate: StaticSearchDelegate(items: [
SpotlightItem(title: 'Home', onSelected: () => goHome()),
SpotlightItem(title: 'Settings', onSelected: () => goSettings()),
]),
child: MyApp(),
)The package does not register any global keyboard shortcuts internally.
Register your own handler in initState and call controller.toggle():
@override
void initState() {
super.initState();
HardwareKeyboard.instance.addHandler(_onKey);
}
bool _onKey(KeyEvent event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.keyK &&
HardwareKeyboard.instance.isMetaPressed) {
controller.toggle();
return true; // consume the event
}
return false;
}
@override
void dispose() {
HardwareKeyboard.instance.removeHandler(_onKey);
controller.dispose();
super.dispose();
}Tip — Windows / Linux: swap
isMetaPressedforisControlPressedto support Ctrl+K.
final delegate = AsyncSearchDelegate(
onSearch: (query) async {
final hits = await myApi.search(query);
return hits.map((h) => SpotlightItem(
title: h.name,
subtitle: h.description,
onSelected: () => openItem(h.id),
)).toList();
},
debounceDuration: const Duration(milliseconds: 300), // default
);Register SpotlightTheme as a ThemeExtension in your MaterialApp.theme:
MaterialApp(
theme: ThemeData(
extensions: [SpotlightTheme(surfaceColor: Color(0xFF1E1E2E))],
),
)All fields are nullable — unset fields fall back to ambient Theme values.
| Field | Type | Default | Description |
|---|---|---|---|
overlayColor |
Color? |
Colors.black54 |
Modal barrier color |
surfaceColor |
Color? |
ColorScheme.surface |
Card background |
surfaceElevation |
double? |
12 |
Material shadow depth |
borderRadius |
BorderRadius? |
circular(12) |
Card corner radius |
inputTextStyle |
TextStyle? |
— | Query text style |
inputHintStyle |
TextStyle? |
— | Placeholder text style |
inputDecoration |
InputDecoration? |
— | Full input decoration override |
dividerColor |
Color? |
Colors.white12 |
Line between input and results |
itemHoverColor |
Color? |
Colors.white10 |
Hovered tile background |
selectedItemColor |
Color? |
primaryContainer |
Keyboard-selected tile background |
titleStyle |
TextStyle? |
— | Result title text style |
subtitleStyle |
TextStyle? |
— | Result subtitle text style |
highlightStyle |
TextStyle? |
FontWeight.bold |
Matched-substring style |
loadingIndicatorColor |
Color? |
— | Spinner color |
emptyStateWidget |
Widget? |
"No results" text |
Custom empty state widget |
maxHeight |
double? |
360 |
Max results list height (px) |
width |
double? |
0.45 |
Card width as fraction of screen (clamped 320–720 px) |
animationDuration |
Duration? |
150ms |
Entry/exit animation duration |
SpotlightSearch(
controller: controller,
delegate: delegate,
itemBuilder: (context, item, isSelected, query) {
return ListTile(
selected: isSelected,
title: Text(item.title),
subtitle: item.subtitle != null ? Text(item.subtitle!) : null,
leading: item.leading,
onTap: item.onSelected,
);
},
child: child,
)cd example
flutter pub get
flutter run -d chromegroupBysupport — divide results into labelled sections (e.g. "Pages", "Actions")- Fuzzy search — built-in fuzzy matching with score-based ranking
SpotlightSearchBartrigger button — a pre-built button widget that opens the overlay and displays the keyboard shortcut hint
MIT © 2026 Sarwer Hussain