How to create a multistep command bar with Tailwind CSS and Alpinejs

Build a three-step automation command bar with Alpine.js, including filtering, keyboard control, and dynamic forms

Published on September 29, 2025 by Michael Andreuzza

LEt’s build a commandbar

Multistep command bars let people run complex actions without leaving the current page. The example below combines Tailwind CSS for structure and Alpine.js for the behavior: search, keyboard navigation, dynamic forms, and a confirmation screen. We will rebuild it piece by piece and explain exactly what each line of Alpine code does.

Heads up: the snippets assume you already have Tailwind CSS and Alpine.js available in your project. Drop the component anywhere inside your page and Alpine will take over on load.

1. Base styles and skeleton markup

Start by adding a tiny helper style so anything marked with x-cloak stays hidden until Alpine finishes booting.

<style>
  :global([x-cloak]) {
    display: none !important;
  }
</style>

Now paste the outer wrapper. This creates the card container, wires Alpine with x-data, and tells Alpine to listen for global keyboard shortcuts with @keydown.window.

<div
  class="w-full max-w-3xl overflow-hidden bg-white shadow-lg rounded-xl outline outline-zinc-200"
  x-data="commandBar()"
  x-init="init()"
  x-cloak
  x-show="open"
  @keydown.window="onKey($event)"
>
  <!-- Steps render here -->
</div>

We will define the commandBar() function next. Using a named function instead of an inline object keeps the template tidy.

2. Alpine state, line by line

Drop the script right before the closing </body> tag (or inline with your component). Every property has a small job, so let’s look at each one.

<script>
  function commandBar() {
    return {
      // Basic UI state ---------------------------------------------------
      open: true,
      step: 1,            // 1: list, 2: configure, 3: done
      query: '',
      filter: 'all',
      selected: 0,
      toast: '',

      // Data -------------------------------------------------------------
      favorites: new Set([2]),
      current: null,
      form: {},
      automations: [
        {
          id: 1,
          name: 'Onboard new customer',
          desc: 'Create a CRM record and send the resources pack.',
          icon: 'zap',
          tags: ['Lifecycle', 'CRM'],
          params: [
            { key: 'customerName', label: 'Customer name', type: 'text', placeholder: 'Jane Doe' },
            { key: 'sendWelcomeEmail', label: 'Send welcome email', type: 'toggle', default: true },
            { key: 'assignTo', label: 'Assign to', type: 'select', options: ['Michela', 'Andre', 'Lexi'] },
            { key: 'scheduleAt', label: 'Schedule run', type: 'datetime' },
          ],
        },
        {
          id: 2,
          name: 'Invoice follow-up',
          desc: 'Email overdue accounts and log the interaction.',
          icon: 'mail',
          tags: ['Billing', 'Email'],
          params: [
            { key: 'recipient', label: 'Recipient email', type: 'text', placeholder: 'finance@company.com' },
            { key: 'template', label: 'Message template', type: 'select', options: ['Polite reminder', 'Firm notice', 'Final notice'] },
            { key: 'scheduleAt', label: 'Schedule run', type: 'datetime' },
          ],
        },
        {
          id: 3,
          name: 'Post-webinar nurture',
          desc: 'Send recap email and schedule follow-up tasks.',
          icon: 'calendar',
          tags: ['Marketing', 'Lifecycle'],
          params: [
            { key: 'webinarDate', label: 'Webinar date', type: 'datetime' },
            { key: 'includeRecording', label: 'Attach recording', type: 'toggle', default: true },
            { key: 'owner', label: 'Account owner', type: 'select', options: ['Cory', 'Nyla', 'Mike'] },
          ],
        },
        {
          id: 4,
          name: 'Social media blast',
          desc: 'Prep assets and schedule posts across channels.',
          icon: 'camera',
          tags: ['Marketing', 'Social'],
          params: [
            { key: 'campaignName', label: 'Campaign name', type: 'text', placeholder: 'Summer Product Drop' },
            { key: 'channels', label: 'Channels', type: 'select', options: ['All', 'Instagram', 'LinkedIn', 'TikTok'] },
            { key: 'scheduleAt', label: 'Schedule run', type: 'datetime' },
          ],
        },
      ],
      scheduled: [
        { id: 2, name: 'Invoice follow-up', at: 'Today · 4:00 PM' },
        { id: 3, name: 'Post-webinar nurture', at: 'Tomorrow · 9:00 AM' },
      ],

      // Lifecycle --------------------------------------------------------
      init() {
        this.$nextTick(() => this.$refs.input?.focus());
        this.$watch('query', () => (this.selected = 0));
        this.$watch('filter', () => (this.selected = 0));
      },

      // Derived data -----------------------------------------------------
      list() {
        let items = [...this.automations];
        if (this.filter === 'favorites') {
          items = items.filter((item) => this.favorites.has(item.id));
        } else if (this.filter === 'scheduled') {
          const scheduledIds = new Set(this.scheduled.map((item) => item.id));
          items = items.filter((item) => scheduledIds.has(item.id));
        }
        if (this.query.trim()) {
          const q = this.query.toLowerCase();
          items = items.filter((item) => (
            item.name.toLowerCase().includes(q) ||
            item.desc.toLowerCase().includes(q) ||
            item.tags.some((tag) => tag.toLowerCase().includes(q))
          ));
        }
        if (items.length === 0) {
          this.selected = 0;
        } else if (this.selected >= items.length) {
          this.selected = items.length - 1;
        }
        return items;
      },

      // Mutations --------------------------------------------------------
      toggleFav(id) {
        const favs = new Set(this.favorites);
        if (favs.has(id)) {
          favs.delete(id);
        } else {
          favs.add(id);
        }
        this.favorites = favs;
      },
      defaultParams(item) {
        const values = {};
        (item.params || []).forEach((param) => {
          if (Object.prototype.hasOwnProperty.call(param, 'default')) {
            values[param.key] = param.default;
            return;
          }
          if (param.type === 'toggle') {
            values[param.key] = false;
            return;
          }
          if (param.type === 'select') {
            values[param.key] = param.options?.[0] || '';
            return;
          }
          if (param.type === 'datetime') {
            const now = new Date();
            now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
            values[param.key] = now.toISOString().slice(0, 16);
            return;
          }
          if (param.type === 'text') {
            values[param.key] = '';
            return;
          }
          values[param.key] = null;
        });
        return values;
      },
      openWizard(item) {
        this.current = item;
        this.form = { ...this.defaultParams(item) };
        this.step = 2;
        this.toast = '';
        this.open = true;
      },
      back() {
        this.step = 1;
        this.current = null;
        this.toast = '';
        this.$nextTick(() => this.$refs.input?.focus());
      },
      quickRun(item) {
        this.current = item;
        this.toast = `${item.name} is running now.`;
        this.step = 3;
        this.open = true;
      },
      runNow() {
        if (!this.current) return;
        this.toast = `${this.current.name} launched successfully.`;
        this.step = 3;
        this.open = true;
      },
      scheduleAt() {
        if (!this.current) return;
        let runAtLabel = 'Soon';
        if (this.form.scheduleAt) {
          const parsed = new Date(this.form.scheduleAt);
          if (!Number.isNaN(parsed.getTime())) {
            runAtLabel = parsed.toLocaleString('en-US', {
              month: 'short',
              day: 'numeric',
              hour: '2-digit',
              minute: '2-digit',
            });
          }
        }
        this.scheduled = [
          ...this.scheduled,
          { id: this.current.id, name: this.current.name, at: runAtLabel },
        ];
        this.toast = `${this.current.name} scheduled for ${runAtLabel}.`;
        this.step = 3;
        this.open = true;
      },

      // Keyboard support -------------------------------------------------
      onKey(event) {
        if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
          event.preventDefault();
          this.open = true;
          this.step = 1;
          this.$nextTick(() => this.$refs.input?.focus());
          return;
        }
        if (!this.open) return;
        const tag = event.target.tagName.toLowerCase();
        if (event.key === '/' && tag !== 'input' && tag !== 'textarea') {
          event.preventDefault();
          this.$nextTick(() => this.$refs.input?.focus());
          return;
        }
        if (this.step === 1) {
          const items = this.list();
          if (event.key === 'ArrowDown') {
            event.preventDefault();
            if (!items.length) return;
            this.selected = (this.selected + 1) % items.length;
            return;
          }
          if (event.key === 'ArrowUp') {
            event.preventDefault();
            if (!items.length) return;
            this.selected = (this.selected - 1 + items.length) % items.length;
            return;
          }
          if (event.key === 'Enter') {
            const item = items[this.selected];
            if (!item) return;
            event.preventDefault();
            if (event.shiftKey) {
              this.quickRun(item);
            } else {
              this.openWizard(item);
            }
            return;
          }
          if (event.key === 'Escape') {
            event.preventDefault();
            this.open = false;
            return;
          }
        }
        if (this.step === 2) {
          if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
            event.preventDefault();
            this.scheduleAt();
            return;
          }
          if (!event.shiftKey && !(event.metaKey || event.ctrlKey) && event.key === 'Enter') {
            event.preventDefault();
            this.runNow();
            return;
          }
          if (event.shiftKey && event.key === 'Enter') {
            event.preventDefault();
            this.runNow();
            return;
          }
          if (event.key === 'Escape') {
            event.preventDefault();
            this.back();
            return;
          }
        }
        if (this.step === 3 && event.key === 'Escape') {
          event.preventDefault();
          this.open = false;
        }
      },
    };
  }
</script>

Cheat sheet: what each block controls

SectionPurpose
open, step, query, filter, selected, toastTop-level flags controlling which step is visible and what feedback the user sees.
favorites, current, form, automations, scheduledData backing the list, pre-filled form values, and the confirmation screen.
initFocuses the search field and resets selected whenever the query or filter changes.
listApplies filtering/searching, keeps the highlighted row in range, and feeds the <template x-for>.
toggleFav, defaultParams, openWizard, back, quickRun, runNow, scheduleAtMutations that move the workflow forward, inject defaults, and record scheduled runs.
onKeyCaptures global keyboard shortcuts (⌘/Ctrl+K, /, arrow keys, Enter, Escape) and routes them to the right action.

3. Populate the automations array

Each automation entry drives both the search list and the dynamic form in step 2. You can keep the sample data or replace it with your own tasks.

automations: [
  {
    id: 1,
    name: 'Onboard new customer',
    desc: 'Create a CRM record and send the resources pack.',
    icon: 'zap',
    tags: ['Lifecycle', 'CRM'],
    params: [
      { key: 'customerName', label: 'Customer name', type: 'text', placeholder: 'Jane Doe' },
      { key: 'sendWelcomeEmail', label: 'Send welcome email', type: 'toggle', default: true },
      { key: 'assignTo', label: 'Assign to', type: 'select', options: ['Michela', 'Andre', 'Lexi'] },
      { key: 'scheduleAt', label: 'Schedule run', type: 'datetime' },
    ],
  },
  // ...add more automations here
],

Add or remove fields in the params array and Alpine instantly rebuilds the configuration form. Supported field types in this setup are text, toggle, select, and datetime.

4. Rendering the three steps

The template uses <template x-if> blocks to swap between steps.

Step 1 – search, filters, and results

  • Search input uses x-model="query" so every keystroke updates query and re-runs list().
  • Buttons update the filter property and use Tailwind classes to show the active state.
  • The results <template x-for> iterates over list(), highlighting the currently selected row with selected===idx.
  • Action buttons call toggleFav, quickRun, or openWizard.

Step 2 – dynamic configuration form

  • The header shows current?.name.
  • The grid loops over current.params and picks an input control based on p.type.
  • Each control binds to form[p.key], so Alpine builds an object like { scope: 'All', notify: 'ops@company.com' }.
  • The footer buttons call scheduleAt() or runNow() and provide a Back button to return to step 1.

Step 3 – confirmation

  • Shows a success icon and the latest toast message.
  • “Run another” resets step to 1 and focuses the search input.
  • “Close” simply sets open to false so the panel can be hidden by your layout logic.

5. Keyboard shortcuts and accessibility

  • Cmd/Ctrl + K or / opens the palette and focuses the search input.
  • Arrow keys move between list rows.
  • Enter opens the wizard, while Shift + Enter executes the quick run shortcut.
  • In the form step, Enter (or Shift + Enter) runs the automation and Cmd/Ctrl + Enter schedules it.
  • Escape backs out of the current step or closes the bar entirely.

To keep the component accessible:

  • List items respond to both mouse hover and keyboard selection.
  • Buttons include aria-label or hidden text (for example, the favorite toggle).
  • Consider wrapping the confirmation toast in aria-live="polite" if you want screen readers to announce status changes automatically.

6. Variations you can try

  1. Programmatic trigger: expose the component globally and call commandBarInstance.openWizard(automation) from anywhere in your app.
  2. Server data: replace the automations array with a fetch call in init() and update form defaults once the data arrives.
  3. Custom fields: add new type branches in the param renderer (for example, a textarea or radio group).
  4. Persist schedules: push scheduled entries to your backend instead of keeping them in memory.

Full component snippet

Drop everything below into a new component to see it in action. The markup matches the live example shown earlier and uses the commandBar() helper.

<style>
  :global([x-cloak]) {
    display: none !important;
  }
</style>

<div
  class="w-full max-w-3xl overflow-hidden bg-white shadow-lg rounded-xl outline outline-zinc-200"
  x-data="commandBar()"
  x-init="init()"
  x-cloak
  x-show="open"
  @keydown.window="onKey($event)"
>
  <!-- Header: search + filters -->
  <div class="p-2">
    <div class="relative">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        class="icon icon-tabler-arrow-search size-5 pointer-events-none absolute top-3.5 left-4 text-zinc-400"
        slot="icon"
      >
        <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
        <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"></path>
        <path d="M21 21l-6 -6"></path>
      </svg>
      <input
        x-ref="input"
        x-model="query"
        @focus="open=true"
        class="w-full h-12 bg-transparent border-0 rounded-lg pr-28 text-zinc-800 placeholder-zinc-400 pl-11 sm:text-sm ring-1 ring-zinc-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
        placeholder="Run or schedule an automation…"
        autocomplete="off"
      />
      <div
        class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 hidden sm:flex gap-1 text-[0.70rem] text-zinc-500"
      >
        <kbd class="px-1 border rounded bg-zinc-100 border-zinc-200">⌘</kbd>
        <kbd class="px-1 border rounded bg-zinc-100 border-zinc-200">K</kbd>
      </div>
      <button
        type="button"
        x-show="query.length>0"
        @click="query=''; $nextTick(()=> $refs.input?.focus())"
        class="absolute right-[3.75rem] top-1/2 -translate-y-1/2 size-7 inline-flex items-center justify-center rounded-md text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100"
        aria-label="Clear search"
      >
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
          class="icon icon-tabler-wave-x size-4"
        >
          <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
          <path d="M18 6l-12 12"></path>
          <path d="M6 6l12 12"></path>
        </svg>
      </button>
    </div>
    <!-- Filters -->
    <div class="mt-2 px-2 flex items-center gap-2 text-[12px]">
      <button
        @click="filter='all'"
        :class="filter==='all' ? 'bg-zinc-100  text-zinc-900 ' : 'text-zinc-600 '"
        class="px-2 py-1 rounded-md"
      >
        All
      </button>
      <button
        @click="filter='favorites'"
        :class="filter==='favorites' ? 'bg-zinc-100  text-zinc-900 ' : 'text-zinc-600 '"
        class="px-2 py-1 rounded-md"
      >
        Favorites
      </button>
      <button
        @click="filter='scheduled'"
        :class="filter==='scheduled' ? 'bg-zinc-100  text-zinc-900 ' : 'text-zinc-600 '"
        class="px-2 py-1 rounded-md"
      >
        Scheduled
      </button>
    </div>
  </div>

  <!-- STEP 1: Automation list -->
  <template x-if="step===1">
    <div class="py-1">
      <div class="px-6 py-1 text-[0.70rem] font-medium text-zinc-500">
        Automations
      </div>
      <ul class="divide-y divide-zinc-100">
        <template x-for="(a, idx) in list()" :key="a.id">
          <li
            :class="selected===idx ? 'bg-zinc-50 ' : ''"
            class="flex items-center justify-between px-6 py-3 cursor-pointer gap-3 text-zinc-900"
            @mouseenter="selected = idx"
            @click="openWizard(a)"
          >
            <div class="flex items-center gap-3">
              <div class="bg-white rounded-lg size-8 grid place-items-center">
                <template x-if="a.icon === 'zap'">
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    class="size-5 text-zinc-500"
                  >
                    <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
                    <path d="M7 3h10l-6 7h4l-6 11v-8h-4z"></path>
                  </svg>
                </template>
                <template x-if="a.icon === 'mail'">
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    class="size-5 text-zinc-500"
                  >
                    <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
                    <path d="M3 7h18v10H3z"></path>
                    <path d="m3 7 9 6 9-6"></path>
                  </svg>
                </template>
                <template x-if="a.icon === 'camera'">
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    class="size-5 text-zinc-500"
                  >
                    <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
                    <path d="M4 7h3l2-2h6l2 2h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1"></path>
                    <path d="M12 13a3 3 0 1 0 3 3 3 3 0 0 0-3-3"></path>
                  </svg>
                </template>
                <template x-if="a.icon === 'calendar'">
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="2"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    class="size-5 text-zinc-500"
                  >
                    <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
                    <path d="M4 5h16v16H4z"></path>
                    <path d="M16 3v4"></path>
                    <path d="M8 3v4"></path>
                    <path d="M4 11h16"></path>
                    <path d="M11 15h1"></path>
                    <path d="M12 15v3"></path>
                  </svg>
                </template>
              </div>
              <div>
                <div class="text-sm font-medium" x-text="a.name"></div>
                <div class="text-xs text-zinc-500" x-text="a.desc"></div>
              </div>
            </div>
            <div class="flex items-center gap-2">
              <template x-for="t in a.tags" :key="t">
                <span
                  class="hidden md:inline text-[0.70rem] px-2 py-0.5 rounded-full bg-zinc-100 text-zinc-700"
                  x-text="t"
                ></span>
              </template>
              <button
                class="flex items-center justify-center text-center shadow-subtle font-medium duration-500 ease-in-out transition-colors focus:outline-2 focus:outline-offset-2 text-zinc-600 bg-zinc-50 outline outline-zinc-50 hover:bg-zinc-200 focus:outline-zinc-600 size-7 p-0.5 text-xs rounded-md"
                @click.stop="toggleFav(a.id)"
                :aria-pressed="favorites.has(a.id)"
                :class="favorites.has(a.id)?'text-amber-500':'text-zinc-600'"
              >
                <span class="sr-only">Toggle favorite</span>

              </button>
              <button
                class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-zinc-600 bg-zinc-50 outline outline-zinc-100 hover:bg-zinc-200 focus-visible:outline-zinc-600 h-7 px-3 text-xs"
                @click.stop="quickRun(a)"
              >
                Run
              </button>
              <button
                class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-zinc-600 bg-zinc-50 outline outline-zinc-100 hover:bg-zinc-200 focus-visible:outline-zinc-600 h-7 px-3 text-xs"
                @click.stop="openWizard(a)"
              >
                Configure
              </button>
            </div>
          </li>
        </template>
        <li
          x-show="list().length===0"
          class="px-6 py-6 text-sm italic text-center text-zinc-500"
        >
          Nothing to show
        </li>
      </ul>
      <div class="px-6 py-3" x-show="scheduled.length">
        <div class="text-[0.70rem] font-medium text-zinc-500 mb-2">
          Upcoming
        </div>
        <ul class="text-xs space-y-1 text-zinc-600">
          <template x-for="s in scheduled" :key="s.id + s.at">
            <li>
              <span class="font-medium" x-text="s.name"></span>
              ·
              <span x-text="s.at"></span>
            </li>
          </template>
        </ul>
      </div>
    </div>
  </template>

  <!-- STEP 2: Configure form -->
  <template x-if="step===2">
    <div class="px-4 py-3">
      <div class="px-2 text-[0.70rem] text-zinc-500">Configure</div>
      <div class="px-2 mt-2 text-sm font-medium" x-text="current?.name"></div>
      <div class="items-center px-2 mt-2 grid grid-cols-1 md:grid-cols-2 gap-3">
        <template x-for="p in current?.params || []" :key="p.key">
          <div>
            <label class="text-[0.70rem] text-zinc-600 block" x-text="p.label"></label>
            <template x-if="p.type==='text'">
              <input
                type="text"
                class="w-full h-9 px-2 rounded-md ring-1 ring-zinc-200 bg-transparent text-sm mt-0.5 border-zinc-200"
                :placeholder="p.placeholder || ''"
                x-model="form[p.key]"
              />
            </template>
            <template x-if="p.type==='toggle'">
              <label class="flex items-center text-sm gap-2">
                <input
                  type="checkbox"
                  class="border accent-zinc-900 rounded-md border-zinc-200"
                  x-model="form[p.key]"
                />
                <span class="text-zinc-500" x-text="form[p.key] ? 'Enabled' : 'Disabled'"></span>
              </label>
            </template>
            <template x-if="p.type==='select'">
              <select
                class="w-full h-9 px-2 rounded-md ring-1 ring-zinc-200 bg-transparent text-sm border-zinc-200 mt-0.5"
                x-model="form[p.key]"
              >
                <template x-for="opt in p.options || []" :key="opt">
                  <option x-text="opt"></option>
                </template>
              </select>
            </template>
            <template x-if="p.type==='datetime'">
              <input
                type="datetime-local"
                class="w-full px-2 text-sm bg-transparent h-9 rounded-md ring-1 ring-zinc-200 border-zinc-200"
                x-model="form[p.key]"
              />
            </template>
          </div>
        </template>
      </div>
      <div class="flex items-center justify-between px-2 mt-4">
        <button
          class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-zinc-600 bg-zinc-50 outline outline-zinc-100 hover:bg-zinc-200 focus-visible:outline-zinc-600 h-7 px-3 text-xs"
          @click="back()"
        >
          Back
        </button>
        <div class="flex items-center gap-2">
          <button
            class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-zinc-600 bg-zinc-50 outline outline-zinc-100 hover:bg-zinc-200 focus-visible:outline-zinc-600 h-7 px-3 text-xs"
            title="Cmd/Ctrl + Enter"
            @click="scheduleAt()"
          >
            Schedule
          </button>
          <button
            class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-white bg-zinc-900 outline outline-zinc-900 hover:bg-zinc-950 focus-visible:outline-zinc-950 h-7 px-3 text-xs"
            @click="runNow()"
          >
            Run now
          </button>
        </div>
      </div>
    </div>
  </template>

  <!-- STEP 3: Done -->
  <template x-if="step===3">
    <div class="px-6 py-10 text-center">
      <div class="inline-flex items-center justify-center rounded-full size-12 bg-emerald-100 text-emerald-700">

      </div>
      <div class="mt-3 text-sm font-medium text-zinc-900" x-text="toast || 'Completed'"></div>
      <div class="mt-2 text-xs text-zinc-600">
        You can close this panel or run another automation.
      </div>
      <div class="flex items-center justify-center mt-4 gap-2">
        <button
          class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-white bg-zinc-900 outline outline-zinc-900 hover:bg-zinc-950 focus-visible:outline-zinc-950 h-7 px-3 text-xs"
          @click="step=1; open=true; query=''; toast=''; $nextTick(()=> $refs.input?.focus())"
        >
          Run another
        </button>
        <button
          class="relative flex items-center justify-center text-center font-medium transition-colors duration-200 ease-in-out select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:z-10 justify-center rounded-md text-zinc-600 bg-zinc-50 outline outline-zinc-100 hover:bg-zinc-200 focus-visible:outline-zinc-600 h-7 px-3 text-xs"
          @click="open=false"
        >
          Close
        </button>
      </div>
    </div>
  </template>

  <!-- Footer help -->
  <div class="px-4 py-2 text-[0.70rem] text-zinc-500 flex items-center justify-between bg-zinc-50">
    <div>
      Press
      <kbd class="px-1 border rounded bg-zinc-100 border-zinc-200 text-zinc-600">/</kbd>
      to search •
      <kbd class="px-1 border rounded bg-zinc-100 border-zinc-200 text-zinc-600">Shift</kbd>
      +
      <kbd class="px-1 border rounded bg-zinc-100 border-zinc-200 text-zinc-600">↵</kbd>
      quick run
    </div>
    <div class="items-center hidden sm:flex gap-2">
      <span class="flex items-center gap-1">
        <kbd class="px-1 border rounded bg-zinc-100 border-zinc-200 text-zinc-600">↑</kbd>
        <kbd class="px-1 border rounded bg-zinc-100 border-zinc-200 text-zinc-600">↓</kbd>
        to navigate
      </span>
      <span class="flex items-center gap-1">
        <kbd class="px-1 border rounded bg-zinc-100 border-zinc-200 text-zinc-600">↵</kbd>
        to configure
      </span>
    </div>
  </div>
</div>

<script>
  function commandBar() {
    return {
      // Basic UI state ---------------------------------------------------
      open: true,
      step: 1,            // 1: list, 2: configure, 3: done
      query: '',
      filter: 'all',
      selected: 0,
      toast: '',

      // Data -------------------------------------------------------------
      favorites: new Set([2]),
      current: null,
      form: {},
      automations: [
        {
          id: 1,
          name: 'Onboard new customer',
          desc: 'Create a CRM record and send the resources pack.',
          icon: 'zap',
          tags: ['Lifecycle', 'CRM'],
          params: [
            { key: 'customerName', label: 'Customer name', type: 'text', placeholder: 'Jane Doe' },
            { key: 'sendWelcomeEmail', label: 'Send welcome email', type: 'toggle', default: true },
            { key: 'assignTo', label: 'Assign to', type: 'select', options: ['Michela', 'Andre', 'Lexi'] },
            { key: 'scheduleAt', label: 'Schedule run', type: 'datetime' },
          ],
        },
        {
          id: 2,
          name: 'Invoice follow-up',
          desc: 'Email overdue accounts and log the interaction.',
          icon: 'mail',
          tags: ['Billing', 'Email'],
          params: [
            { key: 'recipient', label: 'Recipient email', type: 'text', placeholder: 'finance@company.com' },
            { key: 'template', label: 'Message template', type: 'select', options: ['Polite reminder', 'Firm notice', 'Final notice'] },
            { key: 'scheduleAt', label: 'Schedule run', type: 'datetime' },
          ],
        },
        {
          id: 3,
          name: 'Post-webinar nurture',
          desc: 'Send recap email and schedule follow-up tasks.',
          icon: 'calendar',
          tags: ['Marketing', 'Lifecycle'],
          params: [
            { key: 'webinarDate', label: 'Webinar date', type: 'datetime' },
            { key: 'includeRecording', label: 'Attach recording', type: 'toggle', default: true },
            { key: 'owner', label: 'Account owner', type: 'select', options: ['Cory', 'Nyla', 'Mike'] },
          ],
        },
        {
          id: 4,
          name: 'Social media blast',
          desc: 'Prep assets and schedule posts across channels.',
          icon: 'camera',
          tags: ['Marketing', 'Social'],
          params: [
            { key: 'campaignName', label: 'Campaign name', type: 'text', placeholder: 'Summer Product Drop' },
            { key: 'channels', label: 'Channels', type: 'select', options: ['All', 'Instagram', 'LinkedIn', 'TikTok'] },
            { key: 'scheduleAt', label: 'Schedule run', type: 'datetime' },
          ],
        },
      ],
      scheduled: [
        { id: 2, name: 'Invoice follow-up', at: 'Today · 4:00 PM' },
        { id: 3, name: 'Post-webinar nurture', at: 'Tomorrow · 9:00 AM' },
      ],

      // Lifecycle --------------------------------------------------------
      init() {
        this.$nextTick(() => this.$refs.input?.focus());
        this.$watch('query', () => (this.selected = 0));
        this.$watch('filter', () => (this.selected = 0));
      },

      // Derived data -----------------------------------------------------
      list() {
        let items = [...this.automations];
        if (this.filter === 'favorites') {
          items = items.filter((item) => this.favorites.has(item.id));
        } else if (this.filter === 'scheduled') {
          const scheduledIds = new Set(this.scheduled.map((item) => item.id));
          items = items.filter((item) => scheduledIds.has(item.id));
        }
        if (this.query.trim()) {
          const q = this.query.toLowerCase();
          items = items.filter((item) => (
            item.name.toLowerCase().includes(q) ||
            item.desc.toLowerCase().includes(q) ||
            item.tags.some((tag) => tag.toLowerCase().includes(q))
          ));
        }
        if (items.length === 0) {
          this.selected = 0;
        } else if (this.selected >= items.length) {
          this.selected = items.length - 1;
        }
        return items;
      },

      // Mutations --------------------------------------------------------
      toggleFav(id) {
        const favs = new Set(this.favorites);
        if (favs.has(id)) {
          favs.delete(id);
        } else {
          favs.add(id);
        }
        this.favorites = favs;
      },
      defaultParams(item) {
        const values = {};
        (item.params || []).forEach((param) => {
          if (Object.prototype.hasOwnProperty.call(param, 'default')) {
            values[param.key] = param.default;
            return;
          }
          if (param.type === 'toggle') {
            values[param.key] = false;
            return;
          }
          if (param.type === 'select') {
            values[param.key] = param.options?.[0] || '';
            return;
          }
          if (param.type === 'datetime') {
            const now = new Date();
            now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
            values[param.key] = now.toISOString().slice(0, 16);
            return;
          }
          if (param.type === 'text') {
            values[param.key] = '';
            return;
          }
          values[param.key] = null;
        });
        return values;
      },
      openWizard(item) {
        this.current = item;
        this.form = { ...this.defaultParams(item) };
        this.step = 2;
        this.toast = '';
        this.open = true;
      },
      back() {
        this.step = 1;
        this.current = null;
        this.toast = '';
        this.$nextTick(() => this.$refs.input?.focus());
      },
      quickRun(item) {
        this.current = item;
        this.toast = `${item.name} is running now.`;
        this.step = 3;
        this.open = true;
      },
      runNow() {
        if (!this.current) return;
        this.toast = `${this.current.name} launched successfully.`;
        this.step = 3;
        this.open = true;
      },
      scheduleAt() {
        if (!this.current) return;
        let runAtLabel = 'Soon';
        if (this.form.scheduleAt) {
          const parsed = new Date(this.form.scheduleAt);
          if (!Number.isNaN(parsed.getTime())) {
            runAtLabel = parsed.toLocaleString('en-US', {
              month: 'short',
              day: 'numeric',
              hour: '2-digit',
              minute: '2-digit',
            });
          }
        }
        this.scheduled = [
          ...this.scheduled,
          { id: this.current.id, name: this.current.name, at: runAtLabel },
        ];
        this.toast = `${this.current.name} scheduled for ${runAtLabel}.`;
        this.step = 3;
        this.open = true;
      },

      // Keyboard support -------------------------------------------------
      onKey(event) {
        if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
          event.preventDefault();
          this.open = true;
          this.step = 1;
          this.$nextTick(() => this.$refs.input?.focus());
          return;
        }
        if (!this.open) return;
        const tag = event.target.tagName.toLowerCase();
        if (event.key === '/' && tag !== 'input' && tag !== 'textarea') {
          event.preventDefault();
          this.$nextTick(() => this.$refs.input?.focus());
          return;
        }
        if (this.step === 1) {
          const items = this.list();
          if (event.key === 'ArrowDown') {
            event.preventDefault();
            if (!items.length) return;
            this.selected = (this.selected + 1) % items.length;
            return;
          }
          if (event.key === 'ArrowUp') {
            event.preventDefault();
            if (!items.length) return;
            this.selected = (this.selected - 1 + items.length) % items.length;
            return;
          }
          if (event.key === 'Enter') {
            const item = items[this.selected];
            if (!item) return;
            event.preventDefault();
            if (event.shiftKey) {
              this.quickRun(item);
            } else {
              this.openWizard(item);
            }
            return;
          }
          if (event.key === 'Escape') {
            event.preventDefault();
            this.open = false;
            return;
          }
        }
        if (this.step === 2) {
          if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
            event.preventDefault();
            this.scheduleAt();
            return;
          }
          if (!event.shiftKey && !(event.metaKey || event.ctrlKey) && event.key === 'Enter') {
            event.preventDefault();
            this.runNow();
            return;
          }
          if (event.shiftKey && event.key === 'Enter') {
            event.preventDefault();
            this.runNow();
            return;
          }
          if (event.key === 'Escape') {
            event.preventDefault();
            this.back();
            return;
          }
        }
        if (this.step === 3 && event.key === 'Escape') {
          event.preventDefault();
          this.open = false;
        }
      },
    };
  }
</script>

/Michael Andreuzza

Did you like this post? Please share it with your friends!