Docs
Go back
Component by
OV
OblivionCommand palette with full keyboard navigation
TSX
glass-command-palette
"use client"
import React, { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Search, Command, ChevronRight, Zap, FileText, Code2, Shield, Settings, Star, ArrowRight } from "lucide-react";
// Replaced with Tailwind classes for seamless dark/light mode support.
interface GlassCommandPaletteProps {
open: boolean;
onClose: () => void;
commands?: Array<{
icon: React.ReactNode;
label: string;
category: string;
accent: string;
onClick?: () => void;
}>;
}
export function GlassCommandPalette({ open, onClose, commands: customCommands }: GlassCommandPaletteProps) {
const [query, setQuery] = useState("");
const [selected, setSelected] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const defaultCommands: Array<{
icon: React.ReactNode;
label: string;
category: string;
accent: string;
onClick?: () => void;
}> = [
{ icon: <Zap size={14} />, label: "New component", category: "Create", accent: "#c8ff3e" },
{ icon: <FileText size={14} />, label: "Open docs", category: "Navigate", accent: "#60a5fa" },
{ icon: <Code2 size={14} />, label: "Generate snippet", category: "Development", accent: "#a78bfa" },
{ icon: <Shield size={14} />, label: "Auth setup", category: "Create", accent: "#34d399" },
{ icon: <Settings size={14} />, label: "Preferences", category: "Settings", accent: "#71717a" },
{ icon: <Star size={14} />, label: "Star on GitHub", category: "Social", accent: "#facc15" },
{ icon: <ArrowRight size={14} />, label: "View changelog", category: "Navigate", accent: "#60a5fa" },
];
const commands = customCommands || defaultCommands;
const filtered = commands.filter((c) =>
c.label.toLowerCase().includes(query.toLowerCase()) ||
c.category.toLowerCase().includes(query.toLowerCase())
);
useEffect(() => {
if (open) setTimeout(() => inputRef.current?.focus(), 60);
}, [open]);
useEffect(() => {
setSelected(0);
}, [query]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (!open) return;
if (e.key === "ArrowDown") {
e.preventDefault();
setSelected((p) => Math.min(p + 1, filtered.length - 1));
}
if (e.key === "ArrowUp") {
e.preventDefault();
setSelected((p) => Math.max(p - 1, 0));
}
if (e.key === "Enter") {
e.preventDefault();
filtered[selected]?.onClick?.();
onClose?.();
}
if (e.key === "Escape") onClose?.();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [filtered, open, selected, onClose]);
return (
<AnimatePresence>
{open && (
<>
<motion.div
key="cmd-bg"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 z-50 bg-black/10 dark:bg-black/60 backdrop-blur-[2px]"
/>
<motion.div
key="cmd-palette"
initial={{ opacity: 0, scale: 0.94, y: -20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.94, y: -12 }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
className="fixed top-[20%] left-1/2 z-[61] w-[calc(100%-32px)] max-w-[520px] -translate-x-1/2 overflow-hidden rounded-[20px] bg-white/70 dark:bg-zinc-900/70 backdrop-blur-2xl border border-black/5 dark:border-white/10 shadow-[0_40px_100px_rgba(0,0,0,0.1)] dark:shadow-[0_40px_100px_rgba(0,0,0,0.6)]"
>
{/* Search bar */}
<div className="flex items-center gap-3 px-4 py-3.5 border-b border-black/5 dark:border-white/10">
<Search size={16} className="text-neutral-500 dark:text-zinc-500" />
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search commands..."
className="flex-1 bg-transparent border-none outline-none text-sm text-neutral-900 dark:text-zinc-100 placeholder:text-neutral-400 dark:placeholder:text-zinc-500"
/>
<div className="flex items-center gap-1 px-2 py-1 bg-black/5 dark:bg-white/10 border border-black/5 dark:border-white/10 rounded-md text-[11px] text-neutral-500 dark:text-zinc-400">
<Command size={10} /> K
</div>
</div>
{/* Results */}
<div className="p-2 max-h-[320px] overflow-y-auto">
{filtered.length === 0 ? (
<div className="p-6 text-center text-[13px] text-neutral-500 dark:text-zinc-500">
No results for "{query}"
</div>
) : filtered.map((cmd, i) => (
<motion.div
key={cmd.label}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.03 }}
className={`flex items-center gap-3 px-3 py-2.5 rounded-[11px] cursor-pointer transition-colors ${
selected === i
? "bg-black/5 dark:bg-white/10 border border-black/5 dark:border-white/10"
: "bg-transparent border border-transparent"
}`}
onMouseEnter={() => setSelected(i)}
onClick={() => {
cmd.onClick?.();
onClose?.();
}}
>
<div
className="flex items-center justify-center w-8 h-8 rounded-lg"
style={{ background: `${cmd.accent}18`, color: cmd.accent }}
>
{cmd.icon}
</div>
<div className="flex-1">
<div className="text-[13px] text-neutral-900 dark:text-zinc-100 font-semibold">{cmd.label}</div>
<div className="text-[11px] text-neutral-500 dark:text-zinc-400">{cmd.category}</div>
</div>
<ChevronRight size={13} className="text-neutral-400 dark:text-zinc-500" />
</motion.div>
))}
</div>
{/* Footer */}
<div className="flex gap-4 px-4 py-2.5 border-t border-black/5 dark:border-white/10 text-[11px] text-neutral-500 dark:text-zinc-500 font-medium">
<span>↑↓ navigate</span>
<span>↵ select</span>
<span>esc close</span>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}
export const componentMeta = {
name: "Glass Command Palette",
description: "A breathtaking glassmorphism command palette with category filtering and keyboard navigation.",
props: {
open: { type: "boolean", description: "Whether the palette is visible", default: "false" },
onClose: { type: "function", description: "Callback when the palette should close" },
commands: { type: "array", description: "Array of command objects", default: "defaultCommands" }
}
};
export default function GlassCommandPalettePreview() {
const [open, setOpen] = React.useState(false);
return (
<div className="flex flex-col items-center justify-center min-h-[500px] w-full relative p-8">
<div className="text-center relative z-10">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setOpen(true)}
className="flex items-center gap-4 px-8 py-4 bg-white/70 dark:bg-white/10 border border-black/10 dark:border-white/20 rounded-2xl text-neutral-900 dark:text-zinc-100 font-bold backdrop-blur-2xl hover:bg-white dark:hover:bg-white/20 transition-all shadow-xl"
>
<Command size={18} />
<span>Press <kbd className="bg-black/5 dark:bg-black/20 px-1.5 py-0.5 rounded text-xs font-mono">⌘K</kbd> to search</span>
</motion.button>
<p className="mt-4 text-xs text-neutral-500 dark:text-zinc-500 font-medium tracking-wide">Command palette with full keyboard navigation</p>
</div>
<GlassCommandPalette open={open} onClose={() => setOpen(false)} />
</div>
);
}"use client"
import React, { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Search, Command, ChevronRight, Zap, FileText, Code2, Shield, Settings, Star, ArrowRight } from "lucide-react";
// Replaced with Tailwind classes for seamless dark/light mode support.
interface GlassCommandPaletteProps {
open: boolean;
onClose: () => void;
commands?: Array<{
icon: React.ReactNode;
label: string;
category: string;
accent: string;
onClick?: () => void;
}>;
}
export function GlassCommandPalette({ open, onClose, commands: customCommands }: GlassCommandPaletteProps) {
const [query, setQuery] = useState("");
const [selected, setSelected] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const defaultCommands: Array<{
icon: React.ReactNode;
label: string;
category: string;
accent: string;
onClick?: () => void;
}> = [
{ icon: <Zap size={14} />, label: "New component", category: "Create", accent: "#c8ff3e" },
{ icon: <FileText size={14} />, label: "Open docs", category: "Navigate", accent: "#60a5fa" },
{ icon: <Code2 size={14} />, label: "Generate snippet", category: "Development", accent: "#a78bfa" },
{ icon: <Shield size={14} />, label: "Auth setup", category: "Create", accent: "#34d399" },
{ icon: <Settings size={14} />, label: "Preferences", category: "Settings", accent: "#71717a" },
{ icon: <Star size={14} />, label: "Star on GitHub", category: "Social", accent: "#facc15" },
{ icon: <ArrowRight size={14} />, label: "View changelog", category: "Navigate", accent: "#60a5fa" },
];
const commands = customCommands || defaultCommands;
const filtered = commands.filter((c) =>
c.label.toLowerCase().includes(query.toLowerCase()) ||
c.category.toLowerCase().includes(query.toLowerCase())
);
useEffect(() => {
if (open) setTimeout(() => inputRef.current?.focus(), 60);
}, [open]);
useEffect(() => {
setSelected(0);
}, [query]);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (!open) return;
if (e.key === "ArrowDown") {
e.preventDefault();
setSelected((p) => Math.min(p + 1, filtered.length - 1));
}
if (e.key === "ArrowUp") {
e.preventDefault();
setSelected((p) => Math.max(p - 1, 0));
}
if (e.key === "Enter") {
e.preventDefault();
filtered[selected]?.onClick?.();
onClose?.();
}
if (e.key === "Escape") onClose?.();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [filtered, open, selected, onClose]);
return (
<AnimatePresence>
{open && (
<>
<motion.div
key="cmd-bg"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 z-50 bg-black/10 dark:bg-black/60 backdrop-blur-[2px]"
/>
<motion.div
key="cmd-palette"
initial={{ opacity: 0, scale: 0.94, y: -20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.94, y: -12 }}
transition={{ type: "spring", stiffness: 400, damping: 30 }}
className="fixed top-[20%] left-1/2 z-[61] w-[calc(100%-32px)] max-w-[520px] -translate-x-1/2 overflow-hidden rounded-[20px] bg-white/70 dark:bg-zinc-900/70 backdrop-blur-2xl border border-black/5 dark:border-white/10 shadow-[0_40px_100px_rgba(0,0,0,0.1)] dark:shadow-[0_40px_100px_rgba(0,0,0,0.6)]"
>
{/* Search bar */}
<div className="flex items-center gap-3 px-4 py-3.5 border-b border-black/5 dark:border-white/10">
<Search size={16} className="text-neutral-500 dark:text-zinc-500" />
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search commands..."
className="flex-1 bg-transparent border-none outline-none text-sm text-neutral-900 dark:text-zinc-100 placeholder:text-neutral-400 dark:placeholder:text-zinc-500"
/>
<div className="flex items-center gap-1 px-2 py-1 bg-black/5 dark:bg-white/10 border border-black/5 dark:border-white/10 rounded-md text-[11px] text-neutral-500 dark:text-zinc-400">
<Command size={10} /> K
</div>
</div>
{/* Results */}
<div className="p-2 max-h-[320px] overflow-y-auto">
{filtered.length === 0 ? (
<div className="p-6 text-center text-[13px] text-neutral-500 dark:text-zinc-500">
No results for "{query}"
</div>
) : filtered.map((cmd, i) => (
<motion.div
key={cmd.label}
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.03 }}
className={`flex items-center gap-3 px-3 py-2.5 rounded-[11px] cursor-pointer transition-colors ${
selected === i
? "bg-black/5 dark:bg-white/10 border border-black/5 dark:border-white/10"
: "bg-transparent border border-transparent"
}`}
onMouseEnter={() => setSelected(i)}
onClick={() => {
cmd.onClick?.();
onClose?.();
}}
>
<div
className="flex items-center justify-center w-8 h-8 rounded-lg"
style={{ background: `${cmd.accent}18`, color: cmd.accent }}
>
{cmd.icon}
</div>
<div className="flex-1">
<div className="text-[13px] text-neutral-900 dark:text-zinc-100 font-semibold">{cmd.label}</div>
<div className="text-[11px] text-neutral-500 dark:text-zinc-400">{cmd.category}</div>
</div>
<ChevronRight size={13} className="text-neutral-400 dark:text-zinc-500" />
</motion.div>
))}
</div>
{/* Footer */}
<div className="flex gap-4 px-4 py-2.5 border-t border-black/5 dark:border-white/10 text-[11px] text-neutral-500 dark:text-zinc-500 font-medium">
<span>↑↓ navigate</span>
<span>↵ select</span>
<span>esc close</span>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}
export const componentMeta = {
name: "Glass Command Palette",
description: "A breathtaking glassmorphism command palette with category filtering and keyboard navigation.",
props: {
open: { type: "boolean", description: "Whether the palette is visible", default: "false" },
onClose: { type: "function", description: "Callback when the palette should close" },
commands: { type: "array", description: "Array of command objects", default: "defaultCommands" }
}
};
export default function GlassCommandPalettePreview() {
const [open, setOpen] = React.useState(false);
return (
<div className="flex flex-col items-center justify-center min-h-[500px] w-full relative p-8">
<div className="text-center relative z-10">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setOpen(true)}
className="flex items-center gap-4 px-8 py-4 bg-white/70 dark:bg-white/10 border border-black/10 dark:border-white/20 rounded-2xl text-neutral-900 dark:text-zinc-100 font-bold backdrop-blur-2xl hover:bg-white dark:hover:bg-white/20 transition-all shadow-xl"
>
<Command size={18} />
<span>Press <kbd className="bg-black/5 dark:bg-black/20 px-1.5 py-0.5 rounded text-xs font-mono">⌘K</kbd> to search</span>
</motion.button>
<p className="mt-4 text-xs text-neutral-500 dark:text-zinc-500 font-medium tracking-wide">Command palette with full keyboard navigation</p>
</div>
<GlassCommandPalette open={open} onClose={() => setOpen(false)} />
</div>
);
}Save to favoritesCopy to Figma
React
Glass Command Palette
A breathtaking glassmorphism command palette with category filtering and keyboard navigation.
Installation
$ npx @cosmoo/oblivion add glass-command-paletteDependencies
npm install framer-motion lucide-reactUsage Example
tsx
import GlassCommandPalette from "@/components/oblivion/glass-command-palette"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<GlassCommandPalette />
</div>
)
}import GlassCommandPalette from "@/components/oblivion/glass-command-palette"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<GlassCommandPalette />
</div>
)
}