HextaUI

Command

インラインまたは⌘Kパレットとして使える、検索可能な操作リストです。ページ、ショートカット、一致箇所のハイライトに対応します。

import {
  IconCalculator,
  IconCalendar,
  IconCreditCard,
  IconMoodSmile,
  IconSettings,
  IconUser,
} from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"

export function CommandDemo() {
  return (
    <Command highlight className="w-full max-w-sm">
      <CommandInput placeholder="Type a command or search…" />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>
            <IconCalendar />
            Calendar
          </CommandItem>
          <CommandItem>
            <IconMoodSmile />
            Search Emoji
          </CommandItem>
          <CommandItem disabled>
            <IconCalculator />
            Calculator
          </CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem shortcut="mod+p">
            <IconUser />
            Profile
          </CommandItem>
          <CommandItem shortcut="mod+b">
            <IconCreditCard />
            Billing
          </CommandItem>
          <CommandItem shortcut="mod+,">
            <IconSettings />
            Settings
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/command.json

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"
<Command>
  <CommandInput placeholder="Type a command or search…" />
  <CommandList>
    <CommandEmpty>No results found.</CommandEmpty>
    <CommandGroup heading="Suggestions">
      <CommandItem onSelect={() => openCalendar()}>Calendar</CommandItem>
      <CommandItem shortcut="mod+p">Profile</CommandItem>
    </CommandGroup>
  </CommandList>
</Command>

ホットキーでは、Apple デバイスでは mod が ⌘、それ以外では Ctrl を表します。ラベルはプラットフォームごとに自動で整形されます。

Command
├── CommandInput
├── CommandList
│   ├── CommandEmpty
│   ├── CommandLoading
│   ├── CommandGroup
│   │   └── CommandItem
│   │       └── CommandShortcut
│   ├── CommandSeparator
│   └── CommandPage
│       └── CommandGroup
└── CommandFooter

CommandDialog
└── Command

基本

入力に応じて、項目の絞り込みと順位付けが行われます。一致のないグループは消え、リストの高さは残りに合わせてアニメーションします。

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command"

export function CommandBasic() {
  return (
    <Command className="w-full max-w-sm">
      <CommandInput placeholder="Type a command or search…" />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="Suggestions">
          <CommandItem>Calendar</CommandItem>
          <CommandItem>Search Emoji</CommandItem>
          <CommandItem>Calculator</CommandItem>
        </CommandGroup>
        <CommandSeparator />
        <CommandGroup heading="Settings">
          <CommandItem>Profile</CommandItem>
          <CommandItem>Billing</CommandItem>
          <CommandItem>Settings</CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

Dialog

<CommandDialog /> の内側に <Command /> を置き、useCommandHotkey で切り替えます。⌘K または Ctrl K を押してください。項目のショートカットは開いている間に動作し、一致はハイライトされ、preserveSearch は次回開いたときのためにクエリと選択を保持します。

"use client"

import * as React from "react"
import {
  IconCalculator,
  IconCalendar,
  IconCheck,
  IconCreditCard,
  IconExternalLink,
  IconMoodSmile,
  IconPalette,
  IconPoint,
  IconSettings,
  IconUser,
} from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Command,
  CommandDialog,
  CommandEmpty,
  CommandFooter,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandPage,
  CommandSeparator,
  CommandShortcut,
  useCommandHotkey,
} from "@/components/ui/command"

export function CommandDialogDemo() {
  const [open, setOpen] = React.useState(false)
  const [last, setLast] = React.useState<string>()
  const [theme, setTheme] = React.useState("System")

  useCommandHotkey("mod+k", () => setOpen((value) => !value))

  const run = (action: string) => {
    setLast(action)
    setOpen(false)
  }

  return (
    <div className="flex flex-col items-center gap-3">
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open palette
        <CommandShortcut hotkey="mod+k" />
      </Button>
      <p className="text-sm text-muted-foreground">
        {last ? `Ran “${last}”.` : "Nothing run yet."} Theme: {theme}.
      </p>
      <CommandDialog open={open} onOpenChange={setOpen} preserveSearch>
        <Command highlight>
          <CommandInput placeholder="Type a command or search…" />
          <CommandList>
            <CommandEmpty>
              {(search) => `No results for “${search}”.`}
            </CommandEmpty>
            <CommandGroup heading="Suggestions">
              <CommandItem
                shortcut="mod+shift+c"
                onSelect={() => run("Calendar")}
              >
                <IconCalendar />
                Calendar
              </CommandItem>
              <CommandItem onSelect={() => run("Search Emoji")}>
                <IconMoodSmile />
                Search Emoji
              </CommandItem>
              <CommandItem disabled>
                <IconCalculator />
                Calculator
              </CommandItem>
              <CommandItem page="theme" pageTitle="Theme">
                <IconPalette />
                Change theme…
              </CommandItem>
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup heading="Settings">
              <CommandItem shortcut="mod+p" onSelect={() => run("Profile")}>
                <IconUser />
                Profile
              </CommandItem>
              <CommandItem shortcut="mod+b" onSelect={() => run("Billing")}>
                <IconCreditCard />
                Billing
              </CommandItem>
              <CommandItem
                shortcut="mod+,"
                keywords={["preferences", "options"]}
                onSelect={() => run("Settings")}
              >
                <IconSettings />
                Settings
              </CommandItem>
            </CommandGroup>
            <CommandSeparator />
            <CommandGroup heading="Links">
              <CommandItem href="/docs" onSelect={() => setOpen(false)}>
                <IconExternalLink />
                All components
              </CommandItem>
            </CommandGroup>
            <CommandPage id="theme">
              <CommandGroup heading="Theme">
                {["Light", "Dark", "System"].map((option) => (
                  <CommandItem
                    key={option}
                    onSelect={() => {
                      setTheme(option)
                      run(`Theme: ${option}`)
                    }}
                  >
                    {option === theme ? <IconCheck /> : <IconPoint />}
                    {option}
                  </CommandItem>
                ))}
              </CommandGroup>
            </CommandPage>
          </CommandList>
          <CommandFooter />
        </Command>
      </CommandDialog>
    </div>
  )
}

ページ

page を持つ項目は、対応する <CommandPage /> を開きます。ページのタイトルが入力欄内にチップとして表示され、リストは横からスライドインし、検索が空のときの Backspace または Escape で戻ります。

"use client"

import * as React from "react"
import {
  IconBrandGithub,
  IconFolder,
  IconFolderPlus,
  IconUsers,
} from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandPage,
} from "@/components/ui/command"

const projects = ["hextaui", "marketing-site", "design-tokens"]
const members = ["Ada Lovelace", "Grace Hopper", "Alan Turing"]

export function CommandPages() {
  const [last, setLast] = React.useState<string>()

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <Command>
        <CommandInput placeholder="Search…" />
        <CommandList>
          <CommandEmpty>No results found.</CommandEmpty>
          <CommandGroup heading="Workspace">
            <CommandItem page="projects" pageTitle="Projects">
              <IconFolder />
              Projects
            </CommandItem>
            <CommandItem page="members" pageTitle="Members">
              <IconUsers />
              Members
            </CommandItem>
            <CommandItem onSelect={() => setLast("New project")}>
              <IconFolderPlus />
              New project
            </CommandItem>
          </CommandGroup>
          <CommandPage id="projects">
            <CommandGroup heading="Projects">
              {projects.map((project) => (
                <CommandItem key={project} onSelect={setLast}>
                  <IconBrandGithub />
                  {project}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandPage>
          <CommandPage id="members">
            <CommandGroup heading="Members">
              {members.map((member) => (
                <CommandItem key={member} onSelect={setLast}>
                  <IconUsers />
                  {member}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandPage>
        </CommandList>
      </Command>
      <p className="text-sm text-muted-foreground">
        {last ? `Selected “${last}”.` : "Nothing selected yet."}
      </p>
    </div>
  )
}

スクロール可能

長いリストは、上限のある高さの中でスクロールします。キーボードで移動している間、選択された項目は常に表示範囲内に保たれます。

import { IconFileText } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"

const documents = Array.from({ length: 60 }, (_, index) => ({
  id: index + 1,
  title: `Document ${String(index + 1).padStart(2, "0")}`,
}))

export function CommandScrollable() {
  return (
    <Command className="w-full max-w-sm">
      <CommandInput placeholder="Search 60 documents…" />
      <CommandList>
        <CommandEmpty>No documents match.</CommandEmpty>
        <CommandGroup heading="Documents">
          {documents.map((doc) => (
            <CommandItem key={doc.id}>
              <IconFileText />
              {doc.title}
            </CommandItem>
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

非同期の結果

shouldFilter={false} を設定し、取得した結果をレンダリングします。<CommandLoading /> は 150 ms 待ってから表示され、その後は最低 300 ms とどまるため、高速な応答でスピナーが一瞬見えることはありません。両方のレイテンシーを試してみてください。

"use client"

import * as React from "react"
import { IconUser } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandLoading,
  useCommandLoading,
} from "@/components/ui/command"

const people = [
  "Ada Lovelace",
  "Alan Turing",
  "Grace Hopper",
  "Katherine Johnson",
  "Linus Torvalds",
  "Margaret Hamilton",
  "Tim Berners-Lee",
]

export function CommandAsync() {
  const [query, setQuery] = React.useState("")
  const [results, setResults] = React.useState(people)
  const [loading, setLoading] = React.useState(false)
  const [latency, setLatency] = React.useState(700)
  const pending = useCommandLoading(loading)
  const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  React.useEffect(() => () => clearTimeout(timerRef.current), [])

  const search = (nextQuery: string, nextLatency: number) => {
    clearTimeout(timerRef.current)
    setLoading(true)
    timerRef.current = setTimeout(() => {
      setResults(
        people.filter((person) =>
          person.toLowerCase().includes(nextQuery.toLowerCase())
        )
      )
      setLoading(false)
    }, nextLatency)
  }

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <div className="flex gap-2">
        {[80, 700].map((ms) => (
          <Button
            key={ms}
            size="sm"
            variant={latency === ms ? "secondary" : "outline"}
            onClick={() => {
              setLatency(ms)
              search(query, ms)
            }}
          >
            {ms} ms
          </Button>
        ))}
      </div>
      <Command shouldFilter={false} highlight>
        <CommandInput
          placeholder="Search people…"
          value={query}
          onValueChange={(next) => {
            setQuery(next)
            search(next, latency)
          }}
        />
        <CommandList>
          <CommandLoading loading={loading}>Searching…</CommandLoading>
          <CommandEmpty>
            {(value) => `No people match “${value}”.`}
          </CommandEmpty>
          {pending ? null : (
            <CommandGroup heading="People">
              {results.map((person) => (
                <CommandItem key={person}>
                  <IconUser />
                  {person}
                </CommandItem>
              ))}
            </CommandGroup>
          )}
        </CommandList>
      </Command>
    </div>
  )
}

長いコンテンツ

見出しは折り返され、長い名前は選択に応じて省略または折り返され、ショートカットが押し出されることはありません。

import { IconFileText } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandShortcut,
} from "@/components/ui/command"

export function CommandLongContent() {
  return (
    <Command className="w-full max-w-72">
      <CommandInput placeholder="Search…" />
      <CommandList>
        <CommandEmpty>No results found.</CommandEmpty>
        <CommandGroup heading="A group heading that is long enough to wrap onto two lines">
          <CommandItem>
            <IconFileText />
            <span className="min-w-0 truncate">
              quarterly-planning-final-final-v2-reviewed-by-legal.pdf
            </span>
            <CommandShortcut>⌘⇧O</CommandShortcut>
          </CommandItem>
          <CommandItem>
            <IconFileText />
            <span className="min-w-0 wrap-anywhere">
              averyveryverylongunbrokenfilenamethatshouldwrapinsteadofescaping.txt
            </span>
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  )
}

右から左

アイコン、ショートカット、ページチップ、ページのスライドはすべて、読む方向に従います。

import { IconCalendar, IconSettings } from "@tabler/icons-react"

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandShortcut,
} from "@/components/ui/command"

export function CommandRtl() {
  return (
    <div dir="rtl" className="w-full max-w-sm">
      <Command dir="rtl">
        <CommandInput placeholder="ابحث عن أمر…" />
        <CommandList>
          <CommandEmpty>لا توجد نتائج.</CommandEmpty>
          <CommandGroup heading="اقتراحات">
            <CommandItem>
              <IconCalendar />
              التقويم
              <CommandShortcut>⌘T</CommandShortcut>
            </CommandItem>
            <CommandItem>
              <IconSettings />
              الإعدادات
              <CommandShortcut>⌘S</CommandShortcut>
            </CommandItem>
          </CommandGroup>
        </CommandList>
      </Command>
    </div>
  )
}
キーアクション
↓次の項目を選択します。
↑前の項目を選択します。
Alt↓次のグループの最初の項目に移動します。
Alt↑前のグループの最初の項目に移動します。
Home最初の項目を選択します。
End最後の項目を選択します。
CtrlN次の項目を選択します。Ctrl J でも動作します。vimBindings でオフにできます。
CtrlP前の項目を選択します。Ctrl K でも動作します。vimBindings でオフにできます。
Enter選択された項目を実行します。リンクの項目では、⌘ Enter または Ctrl Enter で新しいタブで開きます。
Escまず検索をクリアし、次にページを 1 つ戻り、最後にダイアログを閉じます。
Backspace検索が空のとき、ページを 1 つ戻ります。
⌘P項目のショートカットは、コマンドメニュー内にフォーカスがある間、その項目を実行します。
  • 入力欄は、選択された項目を指す combobox であり、移動するたびにスクリーンリーダーが各項目を読み上げます。
  • polite なライブリージョンが、入力を止めた少し後に結果の件数を通知し、ページを開いたときや離れたときにページタイトルを通知します。文言は formatResults と rootTitle で変更できます。
  • <CommandDialog /> は非表示のタイトルと説明を持ち、開いている間はフォーカスを閉じ込め、閉じるとトリガーにフォーカスを戻します。
  • 項目のショートカットは aria-keyshortcuts で公開されます。
  • モーションの低減が有効な場合、項目は確定の点滅なしで実行され、ページはスライドではなくフェードします。

cmdk をベースにしており、<CommandDialog /> は Base UI のダイアログ上に構築されています。各パーツは、ラップしている cmdk のパーツの props を受け付けます。

プロパティ型デフォルト
labelメニューのアクセシブルな名前。
string"Command menu"
highlight各項目の一致した文字をハイライトし、残りを暗くします。
booleanfalse
shouldFilterfalse に設定すると、項目のフィルタリングと並べ替えを自分で行います。たとえば、結果がサーバーから返ってくる場合です。
booleantrue
filter0(非表示)から 1(最も一致)のスコアを返します。
(value: string, search: string, keywords?: string[]) => number–
value選択された項目の値。
string–
defaultValue
string–
onValueChange
(value: string) => void–
loopリストの端で反対側に回り込みます。
booleanfalse
vimBindingsCtrl の N、J、P、K による移動。
booleantrue
disablePointerSelection
booleanfalse
formatResults入力後にスクリーンリーダーへ通知されるテキスト。
(count: number) => string"3 results"
rootTitle最後のページを離れてルートに戻ったときに通知されます。
string"All commands"
属性説明
data-slot="command"CSSでルートを指定します。
data-highlightingハイライトがオンで、検索が空でない間、付与されます。
--command-radius外側の角丸の半径。項目はこれから同心の角丸を算出します。
--command-insetリストの端と項目の間のパディング。
プロパティ型デフォルト
open
boolean–
defaultOpen
booleanfalse
onOpenChange
(open: boolean, details) => void–
preserveSearchダイアログをマウントしたままにするため、閉じてもクエリ、ページ、選択が維持されます。再び開くと、クエリが選択された状態になります。
booleanfalse
title視覚的に非表示のダイアログのタイトル。
string"Command menu"
description視覚的に非表示のダイアログの説明。
string"Search for a command to run."
showCloseButton
booleanfalse
classNameダイアログのポップアップに適用されます。
string–
属性説明
data-slot="command-dialog"ダイアログのポップアップ。
data-slot="command-dialog-overlay"バックドロップ。
data-openポップアップが開いている間、ポップアップに付与されます。
プロパティ型デフォルト
value制御された検索テキスト。
string–
onValueChange
(search: string) => void–
placeholder
string–
clearLabelクリアボタンのアクセシブルな名前。
string"Clear search"
backLabelページチップのアクセシブルな名前。
(title: string) => string(title) => `Back from ${title}`
属性説明
data-slot="command-input"入力欄。
data-slot="command-input-wrapper"アイコン、入力欄、クリアボタンを保持する行。
data-slot="command-clear"入力すると表示される、クリアボタン。
data-slot="command-page-chip"ページに表示される、戻るチップ。
プロパティ型デフォルト
labelリストのアクセシブルな名前。
string–
属性説明
data-slot="command-list"リスト。
data-settledリストが自分のサイズを計測し終えると付与されます。高さのトランジションは、これが設定されている間だけ実行されます。
--cmdk-list-height表示されている項目の高さ。リストのアニメーションに使われます。
プロパティ型デフォルト
childrenクエリをそのまま表示するには、関数形式を使います。
ReactNode | (search: string) => ReactNode–
属性説明
data-slot="command-empty"リスト内に CommandLoading がある間は非表示になります。
プロパティ型デフォルト
loading
booleantrue
delayスピナーを表示するまでの待機時間(ミリ秒)。
number150
minDuration一度表示されたスピナーが最低限とどまる時間(ミリ秒)。
number300
labelアクセシブルなラベル。デフォルトは文字列の children です。
string–
progress
number–
属性説明
data-slot="command-loading"読み込み中の行。
data-pending遅延の間、行が通知されているがまだ表示されていないときに付与されます。
プロパティ型デフォルト
heading
ReactNode–
value見出しがない場合は必須です。
string–
forceMountフィルタリング中もグループを表示したままにします。
booleanfalse
属性説明
data-slot="command-group"グループ。
[cmdk-group-heading]見出し要素。
プロパティ型デフォルト
onSelectクリック、Enter、または項目のショートカットで、確定の点滅の後に実行されます。
(value: string) => void–
valueフィルタリングに使われます。デフォルトは、ショートカットを除いた項目のテキストです。
string–
keywordsこの項目に一致させる追加の単語。
string[]–
disabled
booleanfalse
shortcut"mod+shift+c" のようなホットキー。項目に表示され、フォーカスがメニュー内にある間、その項目を実行します。
string–
page実行する代わりに、この id を持つ CommandPage を開きます。
string–
pageTitleページチップに表示されるタイトル。デフォルトは value です。
string–
href項目をリンクとしてレンダリングします。Enter でリンクに移動し、⌘ または Ctrl と Enter で新しいタブで開きます。
string–
render代わりにレンダリングするリンク要素。Next.js の <Link /> など。
ReactElement–
confirm選択が伝わるよう、実行する前に項目を短く点滅させます。
booleantrue
forceMountフィルタリング中も項目を表示したままにします。
booleanfalse
属性説明
data-slot="command-item"項目。
data-selected="true"選択された項目に付与されます。
data-disabled="true"無効な項目に付与されます。
data-valueフィルタリングに使われる値。
data-confirming確定の点滅中に付与されます。
data-pageページを開く項目に付与されます。
プロパティ型デフォルト
idそれを開く項目の page prop と一致させます。そのグループと項目は、現在のページである間だけレンダリングされます。
string–
プロパティ型デフォルト
hotkey"mod+k" のようなホットキーを、現在のプラットフォーム向けに整形します。children で上書きできます。
string–
属性説明
data-slot="command-shortcut"ショートカットのラベル。
プロパティ型デフォルト
alwaysRender検索中も表示したままにします。
booleanfalse
属性説明
data-slot="command-separator"セパレーター。
プロパティ型デフォルト
childrenデフォルトは、ページに応じて更新されるキーのヒントです。タッチスクリーンでは非表示になります。
ReactNode–
属性説明
data-slot="command-footer"フッター。
const [open, setOpen] = React.useState(false)

useCommandHotkey("mod+k", () => setOpen((value) => !value))
プロパティ型デフォルト
hotkeyドキュメント全体でリッスンされます。修飾キーのないホットキーは、フィールドに入力している間は無視されます。
string–
callback
(event: KeyboardEvent) => void–
options.enabled
booleantrue

<CommandLoading /> と同じ遅延と最小表示時間で、読み込みインジケーターを表示すべきかどうかを返します。リクエスト実行中に古い結果を隠すのに使います。

プロパティ型デフォルト
loading
boolean–
options.delay
number150
options.minDuration
number300
  • useCommandPages() は、ページを自分のコードから制御するための { pages, page, push, pop, reset } を返します。
  • useCommandState(selector) は、検索内容や絞り込み後の件数など、cmdk の状態を読み取ります。
  • useHotkeyLabel(hotkey) は、ホットキーを現在のプラットフォーム向けに、⌘K や Ctrl+K のように整形します。

使用しているブロック

Command の上に構築されるブロック。