The Omnibar component provides a macOS Spotlight-style search overlay that appears over the entire application.
React component providing a full-screen search overlay similar to macOS Spotlight.
/**
* Omnibar component for full-screen search overlay
* @template T - Type of items in the search results
*/
class Omnibar<T> extends React.PureComponent<OmnibarProps<T>> {
/** Generic factory method for type inference */
static ofType<U>(): new (props: OmnibarProps<U>) => Omnibar<U>;
}
interface OmnibarProps<T> extends ListItemsProps<T> {
/** Whether the omnibar overlay is open */
isOpen: boolean;
/** Callback when omnibar should be closed */
onClose?: (event?: React.SyntheticEvent<HTMLElement>) => void;
/** Props for the search input */
inputProps?: InputGroupProps;
/** Props for the overlay container */
overlayProps?: Partial<OverlayProps>;
}Usage Examples:
import React, { useState } from "react";
import { Omnibar, ItemRenderer } from "@blueprintjs/select";
import { MenuItem, Button } from "@blueprintjs/core";
interface SearchItem {
title: string;
description: string;
category: string;
action: () => void;
}
const searchItems: SearchItem[] = [
{
title: "Create New Project",
description: "Start a new project from scratch",
category: "Actions",
action: () => console.log("Creating new project..."),
},
{
title: "Open Settings",
description: "Configure application preferences",
category: "Navigation",
action: () => console.log("Opening settings..."),
},
{
title: "View Documentation",
description: "Browse help articles and guides",
category: "Help",
action: () => console.log("Opening documentation..."),
},
{
title: "Export Data",
description: "Download your data in various formats",
category: "Actions",
action: () => console.log("Exporting data..."),
},
];
const renderSearchItem: ItemRenderer<SearchItem> = (item, { handleClick, modifiers, query }) => (
<MenuItem
key={item.title}
text={item.title}
label={item.category}
onClick={handleClick}
active={modifiers.active}
disabled={modifiers.disabled}
shouldDismissPopover={false}
>
<div style={{ fontSize: "12px", color: "#666", marginTop: "4px" }}>
{item.description}
</div>
</MenuItem>
);
const AppOmnibar = () => {
const [isOpen, setIsOpen] = useState(false);
const handleItemSelect = (item: SearchItem) => {
item.action();
setIsOpen(false);
};
const searchPredicate = (query: string, item: SearchItem) => {
const searchText = `${item.title} ${item.description} ${item.category}`.toLowerCase();
return searchText.includes(query.toLowerCase());
};
return (
<>
<Button
text="Open Search (Cmd+K)"
onClick={() => setIsOpen(true)}
icon="search"
/>
<Omnibar<SearchItem>
isOpen={isOpen}
items={searchItems}
itemRenderer={renderSearchItem}
itemPredicate={searchPredicate}
onItemSelect={handleItemSelect}
onClose={() => setIsOpen(false)}
noResults={<MenuItem disabled text="No results found." />}
inputProps={{
placeholder: "Search actions, navigate, or get help...",
leftIcon: "search",
}}
/>
</>
);
};
// Global keyboard shortcut integration
const GlobalOmnibar = () => {
const [isOpen, setIsOpen] = useState(false);
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setIsOpen(true);
}
if (e.key === "Escape" && isOpen) {
setIsOpen(false);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen]);
return (
<Omnibar<SearchItem>
isOpen={isOpen}
items={searchItems}
itemRenderer={renderSearchItem}
onItemSelect={(item) => {
item.action();
setIsOpen(false);
}}
onClose={() => setIsOpen(false)}
inputProps={{
placeholder: "Search everything... (Esc to close)",
autoFocus: true,
}}
/>
);
};Omnibar supports extensive overlay customization through Blueprint's Overlay component props.
// Subset of OverlayProps available to Omnibar
interface OverlayProps {
/** Additional CSS classes for the overlay */
className?: string;
/** Whether overlay should focus trap */
enforceFocus?: boolean;
/** Whether to restore focus on close */
shouldReturnFocusOnClose?: boolean;
/** Whether clicking outside should close */
canOutsideClickClose?: boolean;
/** Whether pressing Escape should close */
canEscapeKeyClose?: boolean;
/** Callback when overlay attempts to close */
onClose?: (event?: React.SyntheticEvent<HTMLElement>) => void;
/** Custom backdrop props */
backdropProps?: React.HTMLProps<HTMLDivElement>;
/** Whether to use portal for rendering */
usePortal?: boolean;
/** Portal container element */
portalContainer?: HTMLElement;
}Usage Examples:
import { Omnibar } from "@blueprintjs/select";
const CustomOverlayOmnibar = () => (
<Omnibar<SearchItem>
isOpen={true}
items={searchItems}
itemRenderer={renderSearchItem}
onItemSelect={(item) => item.action()}
onClose={() => console.log("Closing omnibar")}
overlayProps={{
className: "custom-omnibar-overlay",
enforceFocus: true,
shouldReturnFocusOnClose: true,
canOutsideClickClose: true,
canEscapeKeyClose: true,
backdropProps: {
style: { backgroundColor: "rgba(0, 0, 0, 0.8)" },
},
usePortal: true,
}}
inputProps={{
placeholder: "Custom overlay omnibar...",
large: true,
}}
/>
);Omnibar can implement sophisticated search behaviors like fuzzy matching, categorization, and recent items.
Usage Examples:
interface CommandItem {
id: string;
title: string;
description?: string;
category: string;
keywords: string[];
icon?: string;
action: () => void;
lastUsed?: Date;
}
const commands: CommandItem[] = [
{
id: "new-file",
title: "New File",
description: "Create a new file in the current directory",
category: "File",
keywords: ["create", "file", "new", "document"],
icon: "document",
action: () => console.log("Creating new file"),
},
// ... more commands
];
const FuzzySearchOmnibar = () => {
const [isOpen, setIsOpen] = useState(false);
const [recentCommands, setRecentCommands] = useState<string[]>([]);
// Fuzzy search with scoring
const fuzzySearch = (query: string, commands: CommandItem[]): CommandItem[] => {
if (!query) {
// Show recent items first when no query
const recentItems = commands.filter(cmd => recentCommands.includes(cmd.id));
const otherItems = commands.filter(cmd => !recentCommands.includes(cmd.id));
return [...recentItems, ...otherItems];
}
const scored = commands.map(command => {
let score = 0;
const q = query.toLowerCase();
// Title matching
if (command.title.toLowerCase().includes(q)) {
score += command.title.toLowerCase().startsWith(q) ? 100 : 50;
}
// Keywords matching
const keywordMatch = command.keywords.some(keyword =>
keyword.toLowerCase().includes(q)
);
if (keywordMatch) score += 30;
// Description matching
if (command.description?.toLowerCase().includes(q)) score += 20;
// Recent usage bonus
if (recentCommands.includes(command.id)) score += 10;
return { command, score };
})
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score)
.map(({ command }) => command);
return scored;
};
const handleItemSelect = (command: CommandItem) => {
command.action();
// Update recent commands
const updated = [command.id, ...recentCommands.filter(id => id !== command.id)].slice(0, 5);
setRecentCommands(updated);
setIsOpen(false);
};
const renderCommand: ItemRenderer<CommandItem> = (command, { handleClick, modifiers, query }) => {
const isRecent = recentCommands.includes(command.id);
return (
<MenuItem
key={command.id}
icon={command.icon}
text={
<div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span>{command.title}</span>
<span style={{ fontSize: "11px", color: "#666" }}>
{command.category} {isRecent && "• Recent"}
</span>
</div>
{command.description && (
<div style={{ fontSize: "12px", color: "#666", marginTop: "2px" }}>
{command.description}
</div>
)}
</div>
}
onClick={handleClick}
active={modifiers.active}
shouldDismissPopover={false}
/>
);
};
return (
<Omnibar<CommandItem>
isOpen={isOpen}
items={commands}
itemRenderer={renderCommand}
itemListPredicate={fuzzySearch}
onItemSelect={handleItemSelect}
onClose={() => setIsOpen(false)}
resetOnClose
inputProps={{
placeholder: "Type a command or search...",
leftIcon: "command",
autoFocus: true,
}}
/>
);
};Common patterns for integrating Omnibar into applications.
Usage Examples:
// With React Router navigation
import { useNavigate } from "react-router-dom";
const NaviagtionOmnibar = () => {
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);
const navigationItems = [
{
title: "Dashboard",
path: "/dashboard",
description: "View your main dashboard",
},
{
title: "Profile",
path: "/profile",
description: "Manage your account settings",
},
{
title: "Projects",
path: "/projects",
description: "Browse all your projects",
},
];
return (
<Omnibar
isOpen={isOpen}
items={navigationItems}
itemRenderer={(item, { handleClick, modifiers }) => (
<MenuItem
text={item.title}
onClick={handleClick}
active={modifiers.active}
>
<div style={{ fontSize: "12px", color: "#666" }}>
{item.description}
</div>
</MenuItem>
)}
onItemSelect={(item) => {
navigate(item.path);
setIsOpen(false);
}}
onClose={() => setIsOpen(false)}
/>
);
};
// With async data loading
const AsyncDataOmnibar = () => {
const [isOpen, setIsOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState<SearchItem[]>([]);
const performSearch = async (query: string) => {
if (!query) {
setSearchResults([]);
return;
}
setLoading(true);
try {
// Simulate API call
const results = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await results.json();
setSearchResults(data);
} catch (error) {
console.error("Search failed:", error);
setSearchResults([]);
} finally {
setLoading(false);
}
};
// Debounced search
React.useEffect(() => {
const timeoutId = setTimeout(() => performSearch(query), 300);
return () => clearTimeout(timeoutId);
}, [query]);
return (
<Omnibar<SearchItem>
isOpen={isOpen}
items={searchResults}
itemRenderer={renderSearchItem}
onItemSelect={(item) => {
item.action();
setIsOpen(false);
}}
onClose={() => setIsOpen(false)}
onQueryChange={(query) => setQuery(query)}
noResults={
loading ? (
<MenuItem disabled text="Searching..." icon="search" />
) : (
<MenuItem disabled text="No results found." />
)
}
/>
);
};