Playground

Command

A simple and searchable command menu element.

+ Tailwind and Alpine

Copied!
<div x-data="{
        commandItems: {
            suggestions : [
                {
                    title: 'Calendar',
                    value: 'calendar',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'18\' height=\'18\' x=\'3\' y=\'4\' rx=\'2\' ry=\'2\'></rect><line x1=\'16\' x2=\'16\' y1=\'2\' y2=\'6\'></line><line x1=\'8\' x2=\'8\' y1=\'2\' y2=\'6\'></line><line x1=\'3\' x2=\'21\' y1=\'10\' y2=\'10\'></line></svg>',
                    default: true,
                },
                {
                    title: 'Search Emoji',
                    value: 'emoji',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><circle cx=\'12\' cy=\'12\' r=\'10\'></circle><path d=\'M8 14s1.5 2 4 2 4-2 4-2\'></path><line x1=\'9\' x2=\'9.01\' y1=\'9\' y2=\'9\'></line><line x1=\'15\' x2=\'15.01\' y1=\'9\' y2=\'9\'></line></svg>',
                    default: true,
                },
                {
                    title: 'Calculator',
                    value: 'calculator',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'16\' height=\'20\' x=\'4\' y=\'2\' rx=\'2\'></rect><line x1=\'8\' x2=\'16\' y1=\'6\' y2=\'6\'></line><line x1=\'16\' x2=\'16\' y1=\'14\' y2=\'18\'></line><path d=\'M16 10h.01\'></path><path d=\'M12 10h.01\'></path><path d=\'M8 10h.01\'></path><path d=\'M12 14h.01\'></path><path d=\'M8 14h.01\'></path><path d=\'M12 18h.01\'></path><path d=\'M8 18h.01\'></path></svg>',
                    default: true,
                }
            ],
            settings: [
                {
                    title: 'Profile',
                    value: 'profile',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><path d=\'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\'></path><circle cx=\'12\' cy=\'7\' r=\'4\'></circle></svg>',
                    right: '⌘P',
                    default: true,
                },
                {
                    title: 'Billing',
                    value: 'billing',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'20\' height=\'14\' x=\'2\' y=\'5\' rx=\'2\'></rect><line x1=\'2\' x2=\'22\' y1=\'10\' y2=\'10\'></line></svg>',
                    right: '⌘B',
                    default: true,
                },
                {
                    title: 'Settings',
                    value: 'settings',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><path d=\'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\'></path><circle cx=\'12\' cy=\'12\' r=\'3\'></circle></svg>',
                    right: '⌘S',
                    default: true,
                },
                {
                    title: 'Keyboard Settings',
                    value: 'keyboard-settngs',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'20\' height=\'16\' x=\'2\' y=\'4\' rx=\'2\' ry=\'2\'></rect><path d=\'M6 8h.001\'></path><path d=\'M10 8h.001\'></path><path d=\'M14 8h.001\'></path><path d=\'M18 8h.001\'></path><path d=\'M8 12h.001\'></path><path d=\'M12 12h.001\'></path><path d=\'M16 12h.001\'></path><path d=\'M7 16h10\'></path></svg>',
                    right: '⌘K',
                    default: false,
                },
            ]
        },
        commandItemsFiltered: [],
        commandItemActive: null,
        commandItemSelected: null,
        commandId: $id('command'),
        commandSearch: '',
        commandSearchIsEmpty() {
            return this.commandSearch.length == 0;
        },
        commandItemIsActive(item) {
            return this.commandItemActive && this.commandItemActive.value==item.value;
        },
        commandItemActiveNext(){
            let index = this.commandItemsFiltered.indexOf(this.commandItemActive);
            if(index < this.commandItemsFiltered.length-1){
                this.commandItemActive = this.commandItemsFiltered[index+1];
                this.commandScrollToActiveItem();
            }
        },
        commandItemActivePrevious(){
            let index = this.commandItemsFiltered.indexOf(this.commandItemActive);
            if(index > 0){
                this.commandItemActive = this.commandItemsFiltered[index-1];
                this.commandScrollToActiveItem();
            }
        },
        commandScrollToActiveItem(){
            if(this.commandItemActive){
                activeElement = document.getElementById(this.commandItemActive.value + '-' + this.commandId)
                if(!activeElement) return;
                
                newScrollPos = (activeElement.offsetTop + activeElement.offsetHeight) - this.$refs.commandItemsList.offsetHeight;
                if(newScrollPos > 0){
                    this.$refs.commandItemsList.scrollTop=newScrollPos;
                } else {
                    this.$refs.commandItemsList.scrollTop=0;
                }
            }
        },
        commandSearchItems() {
            if(!this.commandSearchIsEmpty()){
                searchTerm = this.commandSearch.replace(/\*/g, '').toLowerCase();
                this.commandItemsFiltered = this.commandItems.filter(item => item.title.toLowerCase().startsWith(searchTerm));
                
                this.commandScrollToActiveItem();
            } else {
                this.commandItemsFiltered = this.commandItems.filter(item => item.default);
            }
            this.commandItemActive = this.commandItemsFiltered[0];
        },
        commandShowCategory(item, index){
            if(index == 0) return true;
            if(typeof this.commandItems[index-1] === 'undefined') return false;
            return item.category != this.commandItems[index-1].category;
        },
        commandCategoryCapitalize(string){
            return string.charAt(0).toUpperCase() + string.slice(1);
        },
        commandItemsReorganize(){
            commandItemsOriginal = this.commandItems;
            keys = Object.keys(this.commandItems);
            this.commandItems = [];
            keys.forEach((key, index) => {
                for(i=0; i<commandItemsOriginal[key].length; i++){
                    commandItemsOriginal[key][i].category = key;
                    this.commandItems.push( commandItemsOriginal[key][i] );
                }
            });
        }
    }" x-init="
        commandItemsReorganize();
        commandSearchItems();
        $watch('commandSearch', value => commandSearchItems());
        $watch('commandItemSelected', function(item){
            if(item){
                console.log('item:', item);
            }
        });
    " 
    @keydown.down="event.preventDefault(); commandItemActiveNext();" 
    @keydown.up="event.preventDefault(); commandItemActivePrevious()" 
    @keydown.enter="commandItemSelected=commandItemActive;" 
    class="flex min-h-[370px] justify-center w-full max-w-xl items-start" x-cloak>
    <div class="flex flex-col w-full h-full overflow-hidden bg-white border rounded-lg shadow-md">
        <div class="flex items-center px-3 border-b">
            <svg class="w-4 h-4 mr-0 text-neutral-400 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" x2="16.65" y1="21" y2="16.65"></line></svg>
            <input type="text" x-ref="commandInput" x-model="commandSearch" class="flex w-full px-2 py-3 text-sm bg-transparent border-0 rounded-md outline-none focus:outline-none focus:ring-0 focus:border-0 placeholder:text-neutral-400 h-11 disabled:cursor-not-allowed disabled:opacity-50" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false">
        </div>
        <div x-ref="commandItemsList" class="max-h-[320px] overflow-y-auto overflow-x-hidden">
            <template x-for="(item, index) in commandItemsFiltered" :key="'item-' + index">

                <div class="pb-1 space-y-1">
                    <template x-if="commandShowCategory(item, index)">
                        <div class="px-1 overflow-hidden text-gray-700">
                            <div class="px-2 py-1 my-1 text-xs font-medium text-neutral-500" x-text="commandCategoryCapitalize(item.category)"></div>
                        </div>
                    </template>
                    
                    <template x-if="(item.default && commandSearchIsEmpty()) || !commandSearchIsEmpty()">
                        <div class="px-1">
                            <div :id="item.value + '-' + commandId" @click="commandItemSelected=item" @mousemove="commandItemActive=item" :class="{ 'bg-neutral-100 text-gray-900' : commandItemIsActive(item) }" class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50">
                                <span x-html="item.icon"></span>
                                <span x-text="item.title"></span>
                                <template x-if="item.right">
                                    <span class="ml-auto text-xs tracking-widest text-muted-foreground" x-text="item.right"></span>
                                </template>
                            </div>
                        </div>
                    </template>
                </div>

            </template>
        </div>
    </div>
</div>

Data

Below you will find the data properties available in the x-data attribute of this element.


Property and Description
commandItems
The items inside the command, each object will contain a category of items. Each item includes a title, value, icon, and default value. All items will be searchable, but only default items will be visible with an empty search.
commandItemsFiltered
A filtered array of all the items. Updated on initialize and as the user types in the search field.
commandItemActive
A command item being hovered by the mouse or highlighted by the keyboard.
commandItemSelected
A command item that has been selected by the user via mouse click or enter on keyboard.
commandId
Each item will have a unique ID, which is used to scroll to element in the event of an overflow of items in the command menu.
commandSearch
The search term entered by the user.
commandSearchIsEmpty()
A boolean value indicating if the search field is empty or not.
commandItemIsActive(item)
A boolean value indicating if the command item is active or not.
commandItemActiveNext()
Based on the current active item, this will be the next item in the list.
commandItemActivePrevious()
Based on the current active item, this will be the previous item in the list.
commandScrollToActiveItem()
If there are more commands than can fit in the menu, this will scroll the menu to the active item.
commandSearchItems()
This will filter the command items based on the search term.
commandShowCategory(item, index)
When the user searches, this function will determine if the filtered items contain a category that should be shown.
commandCategoryCapitalize(string)
Simple helper function to capitalize the category name.
commandItemsReorganize()
This will re-organize the commandItems on init. It will flatten them into a single dimensional array. This makes it easier to filter and iterate over the items.

Inside the init method is a watcher for the commandItemSelected which console.logs the selected item. Replace the console.log with your own functionality you want to use when a user selects that item.

More Examples

Below you will find more Command examples you may wish to use in your projects.


Copied!
<div x-data="{
        commandItems: {
            suggestions : [
                {
                    title: 'Calendar',
                    value: 'calendar',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'18\' height=\'18\' x=\'3\' y=\'4\' rx=\'2\' ry=\'2\'></rect><line x1=\'16\' x2=\'16\' y1=\'2\' y2=\'6\'></line><line x1=\'8\' x2=\'8\' y1=\'2\' y2=\'6\'></line><line x1=\'3\' x2=\'21\' y1=\'10\' y2=\'10\'></line></svg>',
                    default: true,
                },
                {
                    title: 'Search Emoji',
                    value: 'emoji',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><circle cx=\'12\' cy=\'12\' r=\'10\'></circle><path d=\'M8 14s1.5 2 4 2 4-2 4-2\'></path><line x1=\'9\' x2=\'9.01\' y1=\'9\' y2=\'9\'></line><line x1=\'15\' x2=\'15.01\' y1=\'9\' y2=\'9\'></line></svg>',
                    default: true,
                },
                {
                    title: 'Calculator',
                    value: 'calculator',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'16\' height=\'20\' x=\'4\' y=\'2\' rx=\'2\'></rect><line x1=\'8\' x2=\'16\' y1=\'6\' y2=\'6\'></line><line x1=\'16\' x2=\'16\' y1=\'14\' y2=\'18\'></line><path d=\'M16 10h.01\'></path><path d=\'M12 10h.01\'></path><path d=\'M8 10h.01\'></path><path d=\'M12 14h.01\'></path><path d=\'M8 14h.01\'></path><path d=\'M12 18h.01\'></path><path d=\'M8 18h.01\'></path></svg>',
                    default: true,
                }
            ],
            settings: [
                {
                    title: 'Profile',
                    value: 'profile',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><path d=\'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\'></path><circle cx=\'12\' cy=\'7\' r=\'4\'></circle></svg>',
                    right: '⌘P',
                    default: true,
                },
                {
                    title: 'Billing',
                    value: 'billing',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'20\' height=\'14\' x=\'2\' y=\'5\' rx=\'2\'></rect><line x1=\'2\' x2=\'22\' y1=\'10\' y2=\'10\'></line></svg>',
                    right: '⌘B',
                    default: true,
                },
                {
                    title: 'Settings',
                    value: 'settings',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><path d=\'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\'></path><circle cx=\'12\' cy=\'12\' r=\'3\'></circle></svg>',
                    right: '⌘S',
                    default: true,
                },
                {
                    title: 'Keyboard Settings',
                    value: 'keyboard-settngs',
                    icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'20\' height=\'16\' x=\'2\' y=\'4\' rx=\'2\' ry=\'2\'></rect><path d=\'M6 8h.001\'></path><path d=\'M10 8h.001\'></path><path d=\'M14 8h.001\'></path><path d=\'M18 8h.001\'></path><path d=\'M8 12h.001\'></path><path d=\'M12 12h.001\'></path><path d=\'M16 12h.001\'></path><path d=\'M7 16h10\'></path></svg>',
                    right: '⌘K',
                    default: false,
                },
            ]
        },
        commandItemsFiltered: [],
        commandItemActive: null,
        commandItemSelected: null,
        commandId: $id('command'),
        commandSearch: '',
        commandSearchIsEmpty() {
            return this.commandSearch.length == 0;
        },
        commandItemIsActive(item) {
            return this.commandItemActive && this.commandItemActive.value==item.value;
        },
        commandItemActiveNext(){
            let index = this.commandItemsFiltered.indexOf(this.commandItemActive);
            if(index < this.commandItemsFiltered.length-1){
                this.commandItemActive = this.commandItemsFiltered[index+1];
                this.commandScrollToActiveItem();
            }
        },
        commandItemActivePrevious(){
            let index = this.commandItemsFiltered.indexOf(this.commandItemActive);
            if(index > 0){
                this.commandItemActive = this.commandItemsFiltered[index-1];
                this.commandScrollToActiveItem();
            }
        },
        commandScrollToActiveItem(){
            if(this.commandItemActive){
                activeElement = document.getElementById(this.commandItemActive.value + '-' + this.commandId)
                if(!activeElement) return;
                
                newScrollPos = (activeElement.offsetTop + activeElement.offsetHeight) - this.$refs.commandItemsList.offsetHeight;
                if(newScrollPos > 0){
                    this.$refs.commandItemsList.scrollTop=newScrollPos;
                } else {
                    this.$refs.commandItemsList.scrollTop=0;
                }
            }
        },
        commandSearchItems() {
            if(!this.commandSearchIsEmpty()){
                searchTerm = this.commandSearch.replace(/\*/g, '').toLowerCase();
                this.commandItemsFiltered = this.commandItems.filter(item => item.title.toLowerCase().startsWith(searchTerm));
                
                this.commandScrollToActiveItem();
            } else {
                this.commandItemsFiltered = this.commandItems.filter(item => item.default);
            }
            this.commandItemActive = this.commandItemsFiltered[0];
        },
        commandShowCategory(item, index){
            if(index == 0) return true;
            if(typeof this.commandItems[index-1] === 'undefined') return false;
            return item.category != this.commandItems[index-1].category;
        },
        commandCategoryCapitalize(string){
            return string.charAt(0).toUpperCase() + string.slice(1);
        },
        commandItemsReorganize(){
            commandItemsOriginal = this.commandItems;
            keys = Object.keys(this.commandItems);
            this.commandItems = [];
            keys.forEach((key, index) => {
                for(i=0; i<commandItemsOriginal[key].length; i++){
                    commandItemsOriginal[key][i].category = key;
                    this.commandItems.push( commandItemsOriginal[key][i] );
                }
            });
        }
    }" x-init="
        commandItemsReorganize();
        commandItemsFiltered = commandItems;
        commandItemActive=commandItems[0];
        $watch('commandSearch', value => commandSearchItems());
        $watch('commandItemSelected', function(item){
            if(item){
                console.log('item:', item);
            }
        });
    " 
    @keydown.down="event.preventDefault(); commandItemActiveNext();" 
    @keydown.up="event.preventDefault(); commandItemActivePrevious()" 
    @keydown.enter="commandItemSelected=commandItemActive;" 
    class="flex min-h-[370px] justify-center w-full max-w-xl items-start" x-cloak>
    <div class="flex flex-col w-full h-full overflow-hidden bg-white border rounded-lg shadow-md">
        <div class="flex items-center px-3 border-b">
            <svg class="w-4 h-4 mr-0 text-neutral-400 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" x2="16.65" y1="21" y2="16.65"></line></svg>
            <input type="text" x-ref="commandInput" x-model="commandSearch" class="flex w-full px-2 py-3 text-sm bg-transparent border-0 rounded-md outline-none focus:outline-none focus:ring-0 focus:border-0 placeholder:text-neutral-400 h-11 disabled:cursor-not-allowed disabled:opacity-50" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false">
        </div>
        <div x-ref="commandItemsList" class="max-h-[320px] overflow-y-auto overflow-x-hidden">
            <template x-for="(item, index) in commandItemsFiltered" :key="'item-' + index">

                <div class="pb-1 space-y-1">
                    <template x-if="commandShowCategory(item, index)">
                        <div class="px-1 overflow-hidden text-gray-700">
                            <div class="px-2 py-1 my-1 text-xs font-medium text-neutral-500" x-text="commandCategoryCapitalize(item.category)"></div>
                        </div>
                    </template>
                    
                    <template x-if="(item.default && commandSearchIsEmpty()) || !commandSearchIsEmpty()">
                        <div class="px-1">
                            <div :id="item.value + '-' + commandId" @click="commandItemSelected=item" @mousemove="commandItemActive=item" :class="{ 'bg-blue-600 text-white' : commandItemIsActive(item) }" class="relative flex cursor-default select-none items-center rounded px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50">
                                <span x-html="item.icon"></span>
                                <span x-text="item.title"></span>
                                <template x-if="item.right">
                                    <span class="ml-auto text-xs tracking-widest text-muted-foreground" x-text="item.right"></span>
                                </template>
                            </div>
                        </div>
                    </template>
                </div>

            </template>
        </div>
    </div>
</div>
Copied!
<div x-data="{
        commandOpen: false,
    }"
    x-init="
        $watch('commandOpen', function(value){
            if(value === true){
                document.body.classList.add('overflow-hidden');
                $nextTick(() => { window.dispatchEvent(new CustomEvent('command-input-focus', {})); });
            }else{
                document.body.classList.remove('overflow-hidden');
            }
        })
    "
    @keydown.escape.window="commandOpen = false"
    class="relative z-50 w-auto h-auto">
    <button @click="commandOpen=true" class="inline-flex items-center justify-center h-10 px-4 py-2 text-sm font-medium transition-colors bg-white border rounded-md hover:bg-neutral-100 active:bg-white focus:bg-white focus:outline-none focus:ring-2 focus:ring-neutral-200/60 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none">Open</button>
    <template x-teleport="body">

        <div x-show="commandOpen" class="fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen" x-cloak>
            <div x-show="commandOpen" 
                x-transition:enter="ease-out duration-300"
                x-transition:enter-start="opacity-0"
                x-transition:enter-end="opacity-100"
                x-transition:leave="ease-in duration-300"
                x-transition:leave-start="opacity-100"
                x-transition:leave-end="opacity-0"
                @click="commandOpen=false" class="absolute inset-0 w-full h-full bg-black bg-opacity-40">
            </div>
            <div 
                x-show="commandOpen"
                x-trap.inert.noscroll="commandOpen"
                x-transition:enter="ease-out duration-300"
                x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
                x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
                x-transition:leave="ease-in duration-200"
                x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
                x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
                x-data="{
                    commandItems: {
                        suggestions : [
                            {
                                title: 'Calendar',
                                value: 'calendar',
                                icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'18\' height=\'18\' x=\'3\' y=\'4\' rx=\'2\' ry=\'2\'></rect><line x1=\'16\' x2=\'16\' y1=\'2\' y2=\'6\'></line><line x1=\'8\' x2=\'8\' y1=\'2\' y2=\'6\'></line><line x1=\'3\' x2=\'21\' y1=\'10\' y2=\'10\'></line></svg>',
                                default: true,
                            },
                            {
                                title: 'Search Emoji',
                                value: 'emoji',
                                icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><circle cx=\'12\' cy=\'12\' r=\'10\'></circle><path d=\'M8 14s1.5 2 4 2 4-2 4-2\'></path><line x1=\'9\' x2=\'9.01\' y1=\'9\' y2=\'9\'></line><line x1=\'15\' x2=\'15.01\' y1=\'9\' y2=\'9\'></line></svg>',
                                default: true,
                            },
                            {
                                title: 'Calculator',
                                value: 'calculator',
                                icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'16\' height=\'20\' x=\'4\' y=\'2\' rx=\'2\'></rect><line x1=\'8\' x2=\'16\' y1=\'6\' y2=\'6\'></line><line x1=\'16\' x2=\'16\' y1=\'14\' y2=\'18\'></line><path d=\'M16 10h.01\'></path><path d=\'M12 10h.01\'></path><path d=\'M8 10h.01\'></path><path d=\'M12 14h.01\'></path><path d=\'M8 14h.01\'></path><path d=\'M12 18h.01\'></path><path d=\'M8 18h.01\'></path></svg>',
                                default: false,
                            }
                        ],
                        settings: [
                            {
                                title: 'Profile',
                                value: 'profile',
                                icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><path d=\'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\'></path><circle cx=\'12\' cy=\'7\' r=\'4\'></circle></svg>',
                                right: '⌘P',
                                default: true,
                            },
                            {
                                title: 'Billing',
                                value: 'billing',
                                icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'20\' height=\'14\' x=\'2\' y=\'5\' rx=\'2\'></rect><line x1=\'2\' x2=\'22\' y1=\'10\' y2=\'10\'></line></svg>',
                                right: '⌘B',
                                default: true,
                            },
                            {
                                title: 'Settings',
                                value: 'settings',
                                icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><path d=\'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z\'></path><circle cx=\'12\' cy=\'12\' r=\'3\'></circle></svg>',
                                right: '⌘S',
                                default: false,
                            },
                            {
                                title: 'Keyboard Settings',
                                value: 'keyboard-settngs',
                                icon: '<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'24\' height=\'24\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'currentColor\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\' class=\'w-4 h-4 mr-2\'><rect width=\'20\' height=\'16\' x=\'2\' y=\'4\' rx=\'2\' ry=\'2\'></rect><path d=\'M6 8h.001\'></path><path d=\'M10 8h.001\'></path><path d=\'M14 8h.001\'></path><path d=\'M18 8h.001\'></path><path d=\'M8 12h.001\'></path><path d=\'M12 12h.001\'></path><path d=\'M16 12h.001\'></path><path d=\'M7 16h10\'></path></svg>',
                                right: '⌘K',
                                default: false,
                            },
                        ]
                    },
                    commandItemsFiltered: [],
                    commandItemActive: null,
                    commandItemSelected: null,
                    commandId: $id('command'),
                    commandSearch: '',
                    commandSearchIsEmpty() {
                        return this.commandSearch.length == 0;
                    },
                    commandItemIsActive(item) {
                        return this.commandItemActive && this.commandItemActive.value==item.value;
                    },
                    commandItemActiveNext(){
                        let index = this.commandItemsFiltered.indexOf(this.commandItemActive);
                        if(index < this.commandItemsFiltered.length-1){
                            this.commandItemActive = this.commandItemsFiltered[index+1];
                            this.commandScrollToActiveItem();
                        }
                    },
                    commandItemActivePrevious(){
                        let index = this.commandItemsFiltered.indexOf(this.commandItemActive);
                        if(index > 0){
                            this.commandItemActive = this.commandItemsFiltered[index-1];
                            this.commandScrollToActiveItem();
                        }
                    },
                    commandScrollToActiveItem(){
                        if(this.commandItemActive){
                            activeElement = document.getElementById(this.commandItemActive.value + '-' + this.commandId)
                            if(!activeElement) return;
                            
                            newScrollPos = (activeElement.offsetTop + activeElement.offsetHeight) - this.$refs.commandItemsList.offsetHeight;
                            if(newScrollPos > 0){
                                this.$refs.commandItemsList.scrollTop=newScrollPos;
                            } else {
                                this.$refs.commandItemsList.scrollTop=0;
                            }
                        }
                    },
                    commandSearchItems() {
                        if(!this.commandSearchIsEmpty()){
                            searchTerm = this.commandSearch.replace(/\*/g, '').toLowerCase();
                            this.commandItemsFiltered = this.commandItems.filter(item => item.title.toLowerCase().startsWith(searchTerm));
                            
                            this.commandScrollToActiveItem();
                        } else {
                            this.commandItemsFiltered = this.commandItems.filter(item => item.default);
                        }
                        this.commandItemActive = this.commandItemsFiltered[0];
                    },
                    commandShowCategory(item, index){
                        if(index == 0) return true;
                        if(typeof this.commandItems[index-1] === 'undefined') return false;
                        return item.category != this.commandItems[index-1].category;
                    },
                    commandCategoryCapitalize(string){
                        return string.charAt(0).toUpperCase() + string.slice(1);
                    },
                    commandItemsReorganize(){
                        commandItemsOriginal = this.commandItems;
                        keys = Object.keys(this.commandItems);
                        this.commandItems = [];
                        keys.forEach((key, index) => {
                            for(i=0; i<commandItemsOriginal[key].length; i++){
                                commandItemsOriginal[key][i].category = key;
                                this.commandItems.push( commandItemsOriginal[key][i] );
                            }
                        });
                    }
                }" x-init="
                    commandItemsReorganize();
                    commandItemsFiltered = commandItems;
                    commandItemActive=commandItems[0];
                    $watch('commandSearch', value => commandSearchItems());
                    $watch('commandItemSelected', function(item){
                        if(item){
                            console.log('item:', item);
                        }
                    });
                " 
                @keydown.down="event.preventDefault(); commandItemActiveNext();" 
                @keydown.up="event.preventDefault(); commandItemActivePrevious()" 
                @keydown.enter="commandItemSelected=commandItemActive;"
                @command-input-focus.window="$refs.commandInput.focus();"
                class="flex min-h-[370px] justify-center w-full max-w-xl items-start relative" x-cloak>
                <div class="box-border flex flex-col w-full h-full overflow-hidden bg-white rounded-md shadow-md bg-opacity-90 drop-shadow-md backdrop-blur-sm">
                    <div class="flex items-center px-3 border-b border-gray-300">
                        <svg class="w-4 h-4 mr-0 text-neutral-400 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" x2="16.65" y1="21" y2="16.65"></line></svg>
                        <input type="text" x-ref="commandInput" x-model="commandSearch" class="flex w-full px-2 py-3 text-sm bg-transparent border-0 rounded-md outline-none focus:outline-none focus:ring-0 focus:border-0 placeholder:text-neutral-400 h-11 disabled:cursor-not-allowed disabled:opacity-50" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false">
                    </div>
                    <div x-ref="commandItemsList" class="max-h-[320px] overflow-y-auto overflow-x-hidden">
                        <template x-for="(item, index) in commandItemsFiltered" :key="'item-' + index">
            
                            <div class="pb-1 space-y-1">
                                <template x-if="commandShowCategory(item, index)">
                                    <div class="px-1 overflow-hidden text-gray-700">
                                        <div class="px-2 py-1 my-1 text-xs font-medium text-neutral-500" x-text="commandCategoryCapitalize(item.category)"></div>
                                    </div>
                                </template>
                                
                                <template x-if="(item.default && commandSearchIsEmpty()) || !commandSearchIsEmpty()">
                                    <div class="px-1">
                                        <div :id="item.value + '-' + commandId" @click="commandItemSelected=item" @mousemove="commandItemActive=item" :class="{ 'bg-blue-600 text-white' : commandItemIsActive(item) }" class="relative flex cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50">
                                            <span x-html="item.icon"></span>
                                            <span x-text="item.title"></span>
                                            <template x-if="item.right">
                                                <span class="ml-auto text-xs tracking-widest text-muted-foreground" x-text="item.right"></span>
                                            </template>
                                        </div>
                                    </div>
                                </template>
                            </div>
            
                        </template>
                    </div>
                </div>
            </div>
        </div>
    </template>
</div>

A project by DevDojo