Blocks
Command Palette Block
App Shell UIA responsive product shell pattern for global command search, quick navigation, recent work, and keyboard-first actions.
App Shells
Command palette shell
Copy this into a SaaS app shell, internal tool, developer dashboard, productivity app, or admin console. Replace the sample command list with route, entity, and action records from your product.
1200px
<script setup>
import { computed, ref } from 'vue';
import { DomButton, DomCommandPalette, DomDropdown, DomTabs, DomToggle, DomTooltip } from '@getdom/studio/vue';
const workspaces = [
{ label: 'Atlas Studio', value: 'atlas', initials: 'A', detail: 'Product workspace', accent: 'bg-primary' },
{ label: 'Northstar Labs', value: 'northstar', initials: 'N', detail: 'Customer success', accent: 'bg-emerald-500' },
{ label: 'Arcadia Health', value: 'arcadia', initials: 'H', detail: 'Healthcare account', accent: 'bg-sky-500' },
];
const sections = [
{ label: 'Dashboard', count: '12' },
{ label: 'Customers', count: '248' },
{ label: 'Projects', count: '36' },
{ label: 'Automations', count: '18' },
{ label: 'Reports', count: '9' },
{ label: 'Settings', count: '6' },
];
const commands = [
{ value: 'new-project', label: 'Create project', description: 'Start a new workspace project' },
{ value: 'invite-teammate', label: 'Invite teammate', description: 'Send a workspace invitation' },
{ value: 'customer-northstar', label: 'Open Northstar Labs', description: 'Customer account / Health 84 / Renewal due' },
{ value: 'customer-arcadia', label: 'Open Arcadia Health', description: 'Customer account / Critical support thread' },
{ value: 'report-activation', label: 'View activation report', description: 'Funnel report / Last 30 days' },
{ value: 'automation-trial', label: 'Edit trial rescue automation', description: 'Workflow / Active / 1,240 runs' },
{ value: 'billing-settings', label: 'Open billing settings', description: 'Plan, seats, usage, and invoices' },
{ value: 'toggle-theme', label: 'Toggle theme preference', description: 'Switch visual theme for this device' },
{ value: 'export-audit', label: 'Export audit log', description: 'Download workspace activity as CSV' },
];
const recentItems = [
{ label: 'Northstar Labs', type: 'Customer', status: 'Renewal review', accent: 'bg-emerald-500' },
{ label: 'Trial rescue sequence', type: 'Automation', status: 'Active workflow', accent: 'bg-sky-500' },
{ label: 'Activation by role', type: 'Report', status: 'Updated today', accent: 'bg-violet-500' },
];
const suggestedActions = [
{ label: 'Create project', detail: 'Blank project with tasks and files', value: 'new-project' },
{ label: 'Invite teammate', detail: 'Add a collaborator to this workspace', value: 'invite-teammate' },
{ label: 'Export audit log', detail: 'Download recent account activity', value: 'export-audit' },
];
const selectedWorkspace = ref('atlas');
const paletteOpen = ref(false);
const activeTab = ref('recent');
const commandResult = ref(commands[2]);
const showExperimental = ref(true);
const workspaceLabel = computed(() => {
return workspaces.find((workspace) => workspace.value === selectedWorkspace.value)?.label || workspaces[0].label;
});
const currentWorkspace = computed(() => {
return workspaces.find((workspace) => workspace.value === selectedWorkspace.value) || workspaces[0];
});
const visibleCommands = computed(() => {
if (showExperimental.value) return commands;
return commands.filter((command) => command.value !== 'toggle-theme');
});
const resultSummary = computed(() => {
return commandResult.value
? `${commandResult.value.label} selected from the command palette.`
: 'No command selected yet.';
});
function openAction(value) {
const command = commands.find((item) => item.value === value);
if (command) commandResult.value = command;
}
function selectWorkspace(value) {
selectedWorkspace.value = value;
}
function onCommandSelect(event) {
commandResult.value = event.command;
}
</script>
<template>
<div class="min-h-[42rem] w-full overflow-hidden rounded-lg border border-border bg-background text-fg shadow-2xl shadow-black/10">
<div class="grid min-h-[42rem] lg:grid-cols-[15rem_minmax(0,1fr)]">
<aside class="hidden border-r border-border skin-raised lg:flex lg:flex-col">
<div class="border-b border-border p-4">
<div class="flex items-center gap-3">
<span
class="grid size-9 place-items-center rounded-lg text-sm font-semibold text-white"
:class="currentWorkspace.accent"
>{{ currentWorkspace.initials }}</span>
<div class="min-w-0">
<p class="truncate text-sm font-semibold">{{ workspaceLabel }}</p>
<p class="truncate text-xs text-muted-fg">Command-ready shell</p>
</div>
</div>
<DomDropdown
:items="workspaces"
width="min-w-[13rem]"
class="mt-4"
@select="selectWorkspace"
>
<template #trigger>
<span class="flex min-w-[10.75rem] items-center justify-between gap-3">
<span class="flex min-w-0 items-center gap-2">
<span
class="grid size-5 shrink-0 place-items-center rounded-md text-[10px] font-semibold text-white"
:class="currentWorkspace.accent"
>{{ currentWorkspace.initials }}</span>
<span class="truncate">{{ currentWorkspace.label }}</span>
</span>
</span>
</template>
<template #item="{ item }">
<div class="flex items-center gap-3">
<span
class="grid size-7 shrink-0 place-items-center rounded-lg text-xs font-semibold text-white"
:class="item.accent"
>{{ item.initials }}</span>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-fg">{{ item.label }}</p>
<p class="truncate text-xs text-muted-fg">{{ item.detail }}</p>
</div>
<span
v-if="item.value === selectedWorkspace"
class="grid size-5 place-items-center rounded-full bg-primary/15 text-primary"
aria-hidden="true"
>
<svg viewBox="0 0 20 20" class="size-3.5" fill="none">
<path d="m5 10 3 3 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
</div>
</template>
</DomDropdown>
</div>
<nav class="flex-1 space-y-1 p-3">
<button
v-for="section in sections"
:key="section.label"
type="button"
class="flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm font-medium text-muted-fg transition first:bg-secondary first:text-secondary-fg hover:bg-secondary hover:text-fg"
>
<span>{{ section.label }}</span>
<span class="text-xs">{{ section.count }}</span>
</button>
</nav>
<div class="border-t border-border p-3">
<DomTooltip text="Open the same palette from anywhere with Cmd+K or Ctrl+K.">
<DomButton variant="secondary" class="w-full justify-start" size="sm" @click="paletteOpen = true">
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 6h14M5 12h9M5 18h6M17 14l3 3-3 3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Command menu
</DomButton>
</DomTooltip>
</div>
</aside>
<main class="min-w-0">
<header class="border-b border-border bg-background/90 p-4 backdrop-blur sm:p-5">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p class="text-xs font-semibold uppercase text-muted-fg">Global launcher</p>
<h2 class="mt-1 text-2xl font-semibold text-fg">Command palette workspace</h2>
</div>
<div class="flex flex-wrap items-center gap-2">
<DomToggle v-model="showExperimental" label="Labs actions" />
<DomButton variant="secondary" @click="paletteOpen = true">
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M11 5a6 6 0 1 0 0 12 6 6 0 0 0 0-12Zm4.5 10.5L20 20" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
Search actions
</DomButton>
<kbd class="rounded border border-border bg-secondary px-2 py-1 text-xs font-semibold text-muted-fg">Cmd K</kbd>
</div>
</div>
</header>
<div class="grid gap-4 p-4 sm:p-5 xl:grid-cols-[minmax(0,1fr)_20rem]">
<section class="space-y-4">
<div class="rounded-lg border border-border skin-raised p-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 class="text-lg font-semibold text-fg">Quick entry point</h3>
<p class="mt-1 text-sm leading-6 text-muted-fg">Expose global navigation without making every destination permanent sidebar chrome.</p>
</div>
<DomButton @click="paletteOpen = true">Open palette</DomButton>
</div>
<div class="mt-4 rounded-lg border border-border bg-background p-3">
<div class="flex items-center gap-3">
<svg class="size-5 shrink-0 text-muted-fg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M11 5a6 6 0 1 0 0 12 6 6 0 0 0 0-12Zm4.5 10.5L20 20" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
<button
type="button"
class="min-w-0 flex-1 text-left text-sm text-muted-fg"
@click="paletteOpen = true"
>
Search customers, reports, automations, settings...
</button>
<span class="hidden rounded border border-border bg-secondary px-2 py-1 text-xs font-semibold text-muted-fg sm:inline-flex">Ctrl K</span>
</div>
</div>
</div>
<DomTabs v-model="activeTab" :tabs="[
{ key: 'recent', label: 'Recent work' },
{ key: 'actions', label: 'Suggested actions' },
{ key: 'routing', label: 'Routing result' },
]">
<template #recent>
<div class="grid gap-3 md:grid-cols-3">
<button
v-for="item in recentItems"
:key="item.label"
type="button"
class="rounded-lg border border-border bg-background p-4 text-left transition hover:-translate-y-0.5 hover:border-primary/50 hover:shadow-md"
@click="openAction(item.label === 'Northstar Labs' ? 'customer-northstar' : item.label === 'Trial rescue sequence' ? 'automation-trial' : 'report-activation')"
>
<span class="flex items-center gap-2 text-xs font-semibold text-muted-fg">
<span class="size-2 rounded-full" :class="item.accent"></span>
{{ item.type }}
</span>
<span class="mt-3 block text-sm font-semibold text-fg">{{ item.label }}</span>
<span class="mt-1 block text-xs text-muted-fg">{{ item.status }}</span>
</button>
</div>
</template>
<template #actions>
<div class="grid gap-3 md:grid-cols-3">
<button
v-for="action in suggestedActions"
:key="action.value"
type="button"
class="rounded-lg border border-border bg-background p-4 text-left transition hover:border-primary/50 hover:bg-secondary"
@click="openAction(action.value)"
>
<span class="text-sm font-semibold text-fg">{{ action.label }}</span>
<span class="mt-2 block text-sm leading-6 text-muted-fg">{{ action.detail }}</span>
</button>
</div>
</template>
<template #routing>
<div class="rounded-lg border border-border bg-background p-4">
<p class="text-xs font-semibold uppercase text-muted-fg">Selection handler</p>
<p class="mt-2 text-lg font-semibold text-fg">{{ resultSummary }}</p>
<p class="mt-2 text-sm leading-6 text-muted-fg">
In production, map this value to router navigation, dialog launchers, async jobs, or copy-to-clipboard actions.
</p>
</div>
</template>
</DomTabs>
</section>
<aside class="space-y-4">
<section class="rounded-lg border border-border skin-raised p-4">
<h3 class="text-sm font-semibold text-fg">Command coverage</h3>
<div class="mt-4 space-y-3">
<div class="flex items-center justify-between text-sm">
<span class="text-muted-fg">Navigation</span>
<span class="font-semibold text-fg">4 commands</span>
</div>
<div class="h-2 overflow-hidden rounded-full bg-secondary">
<div class="h-full w-[82%] rounded-full bg-primary"></div>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-fg">Records</span>
<span class="font-semibold text-fg">2 recent</span>
</div>
<div class="h-2 overflow-hidden rounded-full bg-secondary">
<div class="h-full w-[58%] rounded-full bg-emerald-500"></div>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-fg">Actions</span>
<span class="font-semibold text-fg">3 ready</span>
</div>
<div class="h-2 overflow-hidden rounded-full bg-secondary">
<div class="h-full w-[70%] rounded-full bg-sky-500"></div>
</div>
</div>
</section>
<section class="rounded-lg border border-border bg-background p-4">
<p class="text-xs font-semibold uppercase text-muted-fg">Last selected</p>
<h3 class="mt-2 text-lg font-semibold text-fg">{{ commandResult.label }}</h3>
<p class="mt-2 text-sm leading-6 text-muted-fg">{{ commandResult.description }}</p>
<div class="mt-4 flex flex-wrap gap-2">
<span class="rounded-full bg-secondary px-2.5 py-1 text-xs font-semibold text-muted-fg">Route-safe</span>
<span class="rounded-full bg-emerald-500/15 px-2.5 py-1 text-xs font-semibold text-emerald-700 dark:text-emerald-300">Permission checked</span>
</div>
</section>
</aside>
</div>
</main>
</div>
<DomCommandPalette
v-model="paletteOpen"
:commands="visibleCommands"
placeholder="Search pages, customers, reports, and actions..."
@select="onCommandSelect"
/>
</div>
</template>
Integration
How to use this block
Use this block when your app has more destinations and actions than fit comfortably in primary navigation. It gives users one predictable launcher for pages, records, creation flows, settings, and recent work.
- Build
commandsfrom your router, permission model, recent entities, and feature flags. - Handle
@selectby routing for navigation commands, opening drawers or dialogs for creation commands, and dispatching jobs for background actions. - Keep destructive commands out of the palette unless they open a confirmation dialog with clear audit context.
- Add analytics for query, selected command, empty searches, and shortcut usage to learn what navigation should be promoted elsewhere.
Data
Recommended command shape
{
value: 'customer:northstar',
label: 'Open Northstar Labs',
description: 'Customer account / Health score 84 / Renewal review due',
type: 'record',
section: 'Customers',
keywords: ['account', 'renewal', 'health'],
permission: 'customers:read',
action: {
kind: 'route',
to: '/customers/northstar'
},
meta: {
recent: true,
lastOpenedAt: '2026-06-10T14:28:00Z'
}
}Customization
Implementation notes
Command source
Merge static route commands with server-side records and recent entities, then filter by permissions before rendering.
Action routing
Normalize command actions into route, modal, drawer, copy, and async job handlers so selection remains predictable.
Future updates
Useful follow-ups include grouped command sections, remote search loading states, command aliases, and admin-managed shortcuts.