Oblivion
Oblivion
Docs
Go back
Component by
OV
Oblivion

Command 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>
  );
}
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-palette
Dependenciesnpm install framer-motion lucide-react

Usage Example

tsx
import GlassCommandPalette from "@/components/oblivion/glass-command-palette"

export default function Demo() {
  return (
    <div className="p-10 flex justify-center">
      <GlassCommandPalette />
    </div>
  )
}