UIDex

Command Palette

لوحة الأوامر

role="dialog" + role="combobox"

also calledcommand bar، command menu، quick open، Cmd+K menu، quick switcher، action launcher، كوماند باليت، شريط الأوامر، صندوق الأوامر السريع، قائمة الإجراءات السريعة، نافذة Cmd+K، التنقّل السريع

A command palette is a blocking dialog opened by a keyboard shortcut, holding one input whose typing narrows a list of commands that run on the spot. What comes out of it is an action, never a value: Enter makes something happen (a file opens, the theme changes, a task is created) and the field keeps nothing. A search field is different, because it returns content for the user to read and judge while the palette acts for you. A menu is different too, since it shows its items in full and you walk them with arrow keys without typing, while a palette starts from the assumption that the command list is far too long to display. A combobox is the closest of the three and still not the same: it deposits a chosen value into a field and hands the user back to the form, and the palette closes itself because the command already ran. One question settles it: what is left after Enter? A page of results means search, a value sitting in a field means combobox, and a changed application state means command palette.

If you called it…

"the box that opens when you hit command K""the search that runs things instead of finding pages""the popup in the middle where you type any action""a spotlight for the app""that shortcut window every dev tool has now"

Live specimen

Interact with the demo. Every part is real and numbered.

لوحة المشروع
RTLالتلميح عند النهاية المنطقية، وترتيب مفاتيحه يبقى لاتينياً

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a command palette opened by a keyboard shortcut. Register one keydown listener on document and match on event.code === "KeyK" together with metaKey or ctrlKey rather than event.key alone, since that key produces ن on an Arabic layout, then call preventDefault because the browser binds Ctrl+K to searching from the address bar. The wrapper is a role="dialog" element with aria-modal="true" and an accessible name, plus a focus trap you write yourself because aria-modal traps nothing, and an Escape that closes and returns focus to whatever opened it. Inside it goes a single <input> carrying dir="auto", role="combobox", aria-expanded, aria-autocomplete="list", an aria-controls pointing at the list id, and an aria-activedescendant naming the highlighted row; never move DOM focus onto the rows. The list is role="listbox" with role="option" rows whose ids come from the command id, groups are role="group" elements tied to their headings with aria-labelledby, and a group disappears entirely once the filter empties it. Rank recent and frequent commands above literal match strength, and fold the text before comparing: strip tashkeel and tatweel and map the hamza forms onto ا. Put an aria-live="polite" region in the tree from the moment the palette opens to report the result count on a debounced filter, make the empty state offer something runnable instead of an apology, and do not build it as a role="option". Render each command shortcut at the logical end of its row inside <kbd> elements in a dir="ltr" container so the command glyph does not detach from the letter after it, and wrap every command label in <bdi>. Make sure every command also has a visible entry point outside the palette. Use logical properties throughout: margin-inline-start: auto for the shortcut hint, border-inline-start for the active row bar, padding-inline for spacing, text-align: start for headings, and never left, right, or box-shadow: inset for the indicator.

How it behaves right-to-left

This section is ours alone.

Isolating the command label

never mirrors

The rows run RTL while command names arrive mixed: "فتح Pull Request جديد", or a label ending in PDF. Neutral characters touching the Latin run (colons, brackets, an ellipsis, a slash) join the Arabic text around them and move to the far end of the line. Wrap every label in <bdi>, which carries dir="auto" and unicode-bidi: isolate together and turns that label into a direction unit of its own that nothing outside can reorder. Do not settle for dir="auto" on the list container: the algorithm takes the direction from the first strong character inside the element, so every row inherits the direction of the first one.

The shortcut hint inside a row

mirrors

The hint sits at the logical end of the row, which puts it on the left in Arabic: use margin-inline-start: auto inside the flex row, never margin-left: auto and never a float. The order of the keys inside the hint mirrors nothing, and the glyphs command, shift and option are all bidi class ON (other neutral) in Unicode, so in an Arabic context they take the paragraph direction, detach from the Latin letter after them, and render with the symbol to the right of the letter instead of the left. Put dir="ltr" on the key container, or wrap it in <bdi dir="ltr">. If each key is its own <kbd> in a flex row, the inline axis itself flips and shows K before the command glyph: the fix is direction: ltr on the container, not flex-direction: row-reverse.

The direction of the command field

mirrors

Do not pin dir="rtl" on the command field. People in an Arabic palette type settings and PDF in Latin, and in a field locked to rtl the trailing dot, slash or bracket joins the Arabic paragraph and jumps to the opposite end of the field. Put dir="auto" on the input itself: HTML resolves an input direction from the first strong character of its value, and falls back to the parent element while the value is empty, so the Arabic placeholder stays on the right and the field turns ltr on the first Latin letter. A dir="auto" on the wrapper around the input does nothing at all, because that computation walks text content and never sees the value of an input.

The highlighted row indicator

mirrors

The thin bar marking the active row stands at the logical start of that row: either a ::before pinned with inset-inline-start: 0, or border-inline-start-width: 2px on the row itself with a transparent colour on the others so the text does not shift as the highlight moves. Do not draw it with box-shadow: inset 2px 0, because shadow offsets are purely physical and CSS gives them no logical form, so the bar stays on the wrong side after the flip. The command icon follows the same logical start through flex order and never needs a margin-right.

The shortcut on an Arabic layout

never mirrors

Matching a press with event.key === "k" breaks for an Arabic user: event.key carries the character the layout produces, and the key printed K on a standard Arabic layout produces ن. event.code describes the physical position and stays "KeyK" whatever the layout, so bind to that, or accept both values. The other half is display: what you print in the hint has to match the letter printed on the key the user is pressing, so read the character from navigator.keyboard.getLayoutMap() where it exists and fall back to the Latin letter, instead of hard-coding it from the code. None of this is a direction problem: the same rule holds for Persian, Hebrew, and every non-QWERTY layout.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIArole="dialog" aria-modal="true" + role="combobox"Two patterns stacked in one component; shipping one of them leaves the other broken.
ARIAaria-activedescendantPoints from the field at the highlighted row id, so focus stays put and typing continues; aria-controls is what makes a reference outside the input legal.
HTMLdialog.showModal()Hands you the top layer, an inert background, and Escape for free, but it focuses the first focusable element, so put autofocus on the input.
Radix UI<Dialog.Root> + cmdk <Command>The usual split: Radix owns the blocking layer and the focus trap, cmdk owns filtering and list movement.
shadcn/ui<CommandDialog> + <Command shouldFilter={false}>A ready wrapper over cmdk, but shouldFilter belongs to the inner Command and not to CommandDialog, so set it there and filter yourself once you need Arabic folding and recency ranking.
HTML<bdi>Isolates a Latin command name inside an Arabic row so its dots and brackets stop migrating to the far end.
CSSborder-inline-startThe logical way to draw the active row bar; box-shadow offsets have no logical form at all.

See also

Updated · 2026-08-19