Command
A command menu component that can be used to search, filter, and select items.
<script lang="ts">
import { Command } from "bits-ui";
import Sticker from "phosphor-svelte/lib/Sticker";
import CodeBlock from "phosphor-svelte/lib/CodeBlock";
import Palette from "phosphor-svelte/lib/Palette";
import CalendarBlank from "phosphor-svelte/lib/CalendarBlank";
import RadioButton from "phosphor-svelte/lib/RadioButton";
import Textbox from "phosphor-svelte/lib/Textbox";
</script>
<Command.Root
class="flex h-full w-full flex-col divide-y divide-border self-start overflow-hidden rounded-xl border border-muted bg-background"
>
<Command.Input
class="focus-override inline-flex h-input w-[296px] truncate rounded-xl bg-background px-4 text-sm transition-colors placeholder:text-foreground-alt/50 focus:outline-none focus:ring-0"
placeholder="Search for something..."
/>
<Command.List
class="max-h-[280px] overflow-y-auto overflow-x-hidden px-2 pb-2"
>
<Command.Viewport>
<Command.Empty
class="flex w-full items-center justify-center pb-6 pt-8 text-sm text-muted-foreground"
>
No results found.
</Command.Empty>
<Command.Group>
<Command.GroupHeading
class="px-3 pb-2 pt-4 text-xs text-muted-foreground"
>
Suggestions
</Command.GroupHeading>
<Command.GroupItems>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["getting started", "tutorial"]}
>
<Sticker class="size-4" />
Introduction
</Command.Item>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["child", "custom element", "snippets"]}
>
<CodeBlock class="size-4 " />
Delegation
</Command.Item>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["css", "theme", "colors", "fonts", "tailwind"]}
>
<Palette class="size-4" />
Styling
</Command.Item>
</Command.GroupItems>
</Command.Group>
<Command.Separator />
<Command.Group>
<Command.GroupHeading
class="px-3 pb-2 pt-4 text-xs text-muted-foreground"
>
Components
</Command.GroupHeading>
<Command.GroupItems>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["dates", "times"]}
>
<CalendarBlank class="size-4" />
Calendar
</Command.Item>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["buttons", "forms"]}
>
<RadioButton class="size-4" />
Radio Group
</Command.Item>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["inputs", "text", "autocomplete"]}
>
<Textbox class="size-4" />
Combobox
</Command.Item>
</Command.GroupItems>
</Command.Group>
</Command.Viewport>
</Command.List>
</Command.Root>
import typography from "@tailwindcss/typography";
import animate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
container: {
center: true,
screens: {
"2xl": "1440px",
},
},
extend: {
colors: {
border: {
DEFAULT: "hsl(var(--border-card))",
input: "hsl(var(--border-input))",
"input-hover": "hsl(var(--border-input-hover))",
},
background: {
DEFAULT: "hsl(var(--background) / <alpha-value>)",
alt: "hsl(var(--background-alt) / <alpha-value>)",
},
foreground: {
DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
alt: "hsl(var(--foreground-alt) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground))",
},
dark: {
DEFAULT: "hsl(var(--dark) / <alpha-value>)",
4: "hsl(var(--dark-04))",
10: "hsl(var(--dark-10))",
40: "hsl(var(--dark-40))",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
},
contrast: {
DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Source Code Pro", ...fontFamily.mono],
alt: ["Courier", ...fontFamily.sans],
},
fontSize: {
xxs: "10px",
},
borderWidth: {
6: "6px",
},
borderRadius: {
card: "16px",
"card-lg": "20px",
"card-sm": "10px",
input: "9px",
button: "5px",
"5px": "5px",
"9px": "9px",
"10px": "10px",
"15px": "15px",
},
height: {
input: "3rem",
"input-sm": "2.5rem",
},
boxShadow: {
mini: "var(--shadow-mini)",
"mini-inset": "var(--shadow-mini-inset)",
popover: "var(--shadow-popover)",
kbd: "var(--shadow-kbd)",
btn: "var(--shadow-btn)",
card: "var(--shadow-card)",
"date-field-focus": "var(--shadow-date-field-focus)",
},
opacity: {
8: "0.08",
},
scale: {
80: ".80",
98: ".98",
99: ".99",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
plugins: [typography, animate],
};
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Colors */
--background: 0 0% 100%;
--background-alt: 0 0% 100%;
--foreground: 0 0% 9%;
--foreground-alt: 0 0% 32%;
--muted: 240 5% 96%;
--muted-foreground: 0 0% 9% / 0.4;
--border: 240 6% 10%;
--border-input: 240 6% 10% / 0.17;
--border-input-hover: 240 6% 10% / 0.4;
--border-card: 240 6% 10% / 0.1;
--dark: 240 6% 10%;
--dark-10: 240 6% 10% / 0.1;
--dark-40: 240 6% 10% / 0.4;
--dark-04: 240 6% 10% / 0.04;
--accent: 204 94% 94%;
--accent-foreground: 204 80% 16%;
--destructive: 347 77% 50%;
/* black */
--constrast: 0 0% 0%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: 0 0% 5%;
--background-alt: 0 0% 8%;
--foreground: 0 0% 95%;
--foreground-alt: 0 0% 70%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 100% / 0.4;
--border: 0 0% 96%;
--border-input: 0 0% 96% / 0.17;
--border-input-hover: 0 0% 96% / 0.4;
--border-card: 0 0% 96% / 0.1;
--dark: 0 0% 96%;
--dark-40: 0 0% 96% / 0.4;
--dark-10: 0 0% 96% / 0.1;
--dark-04: 0 0% 96% / 0.04;
--accent: 204 90 90%;
--accent-foreground: 204 94% 94%;
--destructive: 350 89% 60%;
/* white */
--constrast: 0 0% 100%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
/* Mobile tap highlight */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
html {
-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
}
::selection {
background: #fdffa4;
color: black;
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-card-lg !bg-dark-10;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: var(--bg-muted);
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
.link {
@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
}
Structure
<script lang="ts">
import { Combobox } from "bits-ui";
</script>
<Command.Root>
<Combobox.Input />
<Command.List>
<Command.Viewport>
<Command.Empty />
<Command.Loading />
<Command.Group>
<Command.GroupHeading />
<Command.GroupItems>
<Command.Item />
<Command.LinkItem />
</Command.GroupItems>
</Command.Group>
<Command.Separator />
<Command.Item />
<Command.LinkItem />
</Command.Viewport>
</Command.List>
</Command.Root>
Value State
The value
prop is used to determine which command item is currently selected. Bits UI provides flexible options for controlling and synchronizing the Command's value
state.
Two-Way Binding
Use the bind:value
directive for effortless two-way synchronization between your local state and the Command's internal state.
<script lang="ts">
import { Command } from "bits-ui";
let myValue = $state<string[]>("");
</script>
<button onclick={() => (myValue = "abc")}> Set value to "abc" </button>
<Command.Root bind:value={myValue}>
<!-- ... -->
</Command.Root>
This setup enables setting the Command's value via the custom button and ensures the local myValue
state updates when the Command updates the value through any internal means (e.g., searching for a command item).
Change Handler
You can also use the onValueChange
prop to update local state when the Command's value
state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the state changes.
<script lang="ts">
import { Command } from "bits-ui";
let myValue = $state<string[]>("");
</script>
<Command.Root
value={myValue}
onValueChange={(value) => {
myValue = value;
// additional logic here.
}}
>
<!-- ... -->
</Command.Root>
Controlled
Sometimes, you may want complete control over the Command's value state, meaning you will be "kept in the loop" and be required to apply the value state change yourself. While you will rarely need this, it's possible to do so by setting the controlledValue
prop to true
.
You will then be responsible for updating a local value state variable that is passed as the value
prop to the Command.Root
component.
<script lang="ts">
import { Command } from "bits-ui";
let myValue = $state("");
</script>
<Command.Root controlledValue value={myValue} onValueChange={(v) => (myValue = v)}>
<!-- ... -->
</Command.Root>
See the Controlled State documentation for more information about controlled values.
In a Modal
You can combine the Command
component with the Dialog
component to display the command menu within a modal.
<script lang="ts">
import { Command, Dialog } from "bits-ui";
import Sticker from "phosphor-svelte/lib/Sticker";
import CodeBlock from "phosphor-svelte/lib/CodeBlock";
import Palette from "phosphor-svelte/lib/Palette";
import CalendarBlank from "phosphor-svelte/lib/CalendarBlank";
import RadioButton from "phosphor-svelte/lib/RadioButton";
import Textbox from "phosphor-svelte/lib/Textbox";
let dialogOpen = $state(false);
function handleKeydown(e: KeyboardEvent) {
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
dialogOpen = true;
}
}
</script>
<svelte:document onkeydown={handleKeydown} />
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Trigger
class="inline-flex h-12 select-none
items-center justify-center whitespace-nowrap rounded-input bg-dark px-[21px]
text-[15px] font-semibold text-background shadow-mini transition-colors hover:bg-dark/95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-98"
>
Open Command Menu ⌘J
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<Dialog.Content
class="fixed left-[50%] top-[50%] z-50 w-full max-w-[94%] translate-x-[-50%] translate-y-[-50%] rounded-card-lg bg-background shadow-popover outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:max-w-[490px] md:w-full"
>
<Dialog.Title class="sr-only">Command Menu</Dialog.Title>
<Dialog.Description class="sr-only">
This is the command menu. Use the arrow keys to navigate and press ⌘K to
open the search bar.
</Dialog.Description>
<Command.Root
class="flex h-full w-full flex-col divide-y divide-border self-start overflow-hidden rounded-xl border border-muted bg-background"
>
<Command.Input
class="focus-override inline-flex h-input w-[296px] truncate rounded-xl bg-background px-4 text-sm transition-colors placeholder:text-foreground-alt/50 focus:outline-none focus:ring-0"
placeholder="Search for something..."
/>
<Command.List
class="max-h-[280px] overflow-y-auto overflow-x-hidden px-2 pb-2"
>
<Command.Viewport>
<Command.Empty
class="flex w-full items-center justify-center pb-6 pt-8 text-sm text-muted-foreground"
>
No results found.
</Command.Empty>
<Command.Group>
<Command.GroupHeading
class="px-3 pb-2 pt-4 text-xs text-muted-foreground"
>
Suggestions
</Command.GroupHeading>
<Command.GroupItems>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["getting started", "tutorial"]}
>
<Sticker class="size-4" />
Introduction
</Command.Item>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["child", "custom element", "snippets"]}
>
<CodeBlock class="size-4 " />
Delegation
</Command.Item>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["css", "theme", "colors", "fonts", "tailwind"]}
>
<Palette class="size-4" />
Styling
</Command.Item>
</Command.GroupItems>
</Command.Group>
<Command.Separator />
<Command.Group>
<Command.GroupHeading
class="px-3 pb-2 pt-4 text-xs text-muted-foreground"
>
Components
</Command.GroupHeading>
<Command.GroupItems>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["dates", "times"]}
>
<CalendarBlank class="size-4" />
Calendar
</Command.Item>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["buttons", "forms"]}
>
<RadioButton class="size-4" />
Radio Group
</Command.Item>
<Command.Item
class="flex h-10 cursor-pointer select-none items-center gap-2 rounded-button px-3 py-2.5 text-sm capitalize outline-none data-[selected]:bg-muted"
keywords={["inputs", "text", "autocomplete"]}
>
<Textbox class="size-4" />
Combobox
</Command.Item>
</Command.GroupItems>
</Command.Group>
</Command.Viewport>
</Command.List>
</Command.Root>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
import typography from "@tailwindcss/typography";
import animate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
container: {
center: true,
screens: {
"2xl": "1440px",
},
},
extend: {
colors: {
border: {
DEFAULT: "hsl(var(--border-card))",
input: "hsl(var(--border-input))",
"input-hover": "hsl(var(--border-input-hover))",
},
background: {
DEFAULT: "hsl(var(--background) / <alpha-value>)",
alt: "hsl(var(--background-alt) / <alpha-value>)",
},
foreground: {
DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
alt: "hsl(var(--foreground-alt) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground))",
},
dark: {
DEFAULT: "hsl(var(--dark) / <alpha-value>)",
4: "hsl(var(--dark-04))",
10: "hsl(var(--dark-10))",
40: "hsl(var(--dark-40))",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
},
contrast: {
DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Source Code Pro", ...fontFamily.mono],
alt: ["Courier", ...fontFamily.sans],
},
fontSize: {
xxs: "10px",
},
borderWidth: {
6: "6px",
},
borderRadius: {
card: "16px",
"card-lg": "20px",
"card-sm": "10px",
input: "9px",
button: "5px",
"5px": "5px",
"9px": "9px",
"10px": "10px",
"15px": "15px",
},
height: {
input: "3rem",
"input-sm": "2.5rem",
},
boxShadow: {
mini: "var(--shadow-mini)",
"mini-inset": "var(--shadow-mini-inset)",
popover: "var(--shadow-popover)",
kbd: "var(--shadow-kbd)",
btn: "var(--shadow-btn)",
card: "var(--shadow-card)",
"date-field-focus": "var(--shadow-date-field-focus)",
},
opacity: {
8: "0.08",
},
scale: {
80: ".80",
98: ".98",
99: ".99",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
plugins: [typography, animate],
};
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Colors */
--background: 0 0% 100%;
--background-alt: 0 0% 100%;
--foreground: 0 0% 9%;
--foreground-alt: 0 0% 32%;
--muted: 240 5% 96%;
--muted-foreground: 0 0% 9% / 0.4;
--border: 240 6% 10%;
--border-input: 240 6% 10% / 0.17;
--border-input-hover: 240 6% 10% / 0.4;
--border-card: 240 6% 10% / 0.1;
--dark: 240 6% 10%;
--dark-10: 240 6% 10% / 0.1;
--dark-40: 240 6% 10% / 0.4;
--dark-04: 240 6% 10% / 0.04;
--accent: 204 94% 94%;
--accent-foreground: 204 80% 16%;
--destructive: 347 77% 50%;
/* black */
--constrast: 0 0% 0%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: 0 0% 5%;
--background-alt: 0 0% 8%;
--foreground: 0 0% 95%;
--foreground-alt: 0 0% 70%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 100% / 0.4;
--border: 0 0% 96%;
--border-input: 0 0% 96% / 0.17;
--border-input-hover: 0 0% 96% / 0.4;
--border-card: 0 0% 96% / 0.1;
--dark: 0 0% 96%;
--dark-40: 0 0% 96% / 0.4;
--dark-10: 0 0% 96% / 0.1;
--dark-04: 0 0% 96% / 0.04;
--accent: 204 90 90%;
--accent-foreground: 204 94% 94%;
--destructive: 350 89% 60%;
/* white */
--constrast: 0 0% 100%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
/* Mobile tap highlight */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
html {
-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
}
::selection {
background: #fdffa4;
color: black;
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-card-lg !bg-dark-10;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: var(--bg-muted);
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
.link {
@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
}
Filtering
Custom Filter
By default, the Command
component uses a scoring algorithm to determine how the items should be sorted/filtered. You can provide a custom filter function to override this behavior.
The function should return a number between 0
and 1
, with 1
being a perfect match, and 0
being no match, resulting in the item being hidden entirely.
The following example shows how you might implement a strict substring match filter:
<script lang="ts">
import { Command } from "bits-ui";
function customFilter(value: string, search: string, keywords?: string[]): number {
return value.includes(search) ? 1 : 0;
}
</script>
<Command.Root filter={customFilter}>
<!-- ... -->
</Command.Root>
Disable Filtering
You can disable filtering by setting the shouldFilter
prop to false
.
<Command.Root shouldFilter={false}>
<!-- ... -->
</Command.Root>
This is useful when you have a lot of custom logic, need to fetch items asynchronously, or just want to handle filtering yourself. You'll be responsible for iterating over the items and determining which ones should be shown.
Item Selection
You can use the onSelect
prop to handle the selection of items.
<Command.Item onSelect={() => console.log("selected something!")} />
Links
If you want one of the items to get all the benefits of a link (prefetching, etc.), you should use the Command.LinkItem
component instead of the Command.Item
component. The only difference is that the Command.LinkItem
component will render an a
element instead of a div
element.
<Command.LinkItem href="/some/path">
<!-- ... -->
</Command.LinkItem>
API Reference
The root command component which manages & scopes the state of the command.
Property | Type | Description |
---|---|---|
value $bindable | string | The value of the command. Default: undefined |
onValueChange | function | A callback that is fired when the command value changes. Default: undefined |
controlledValue | boolean | Whether or not the value is controlled or not. If Default: false |
label | string | An accessible label for the command menu. This is not visible and is only used for screen readers. Default: undefined |
filter | function | A custom filter function used to filter items. This function should return a number between Default: undefined |
shouldFilter | boolean | Whether or not the command menu should filter items. This is useful when you want to apply custom filtering logic outside of the Command component. Default: true |
loop | boolean | Whether or not the command menu should loop through items when navigating with the keyboard. Default: false |
disablePointerSelection | boolean | Set this to true to prevent items from being selected when the users pointer moves over them. Default: false |
vimBindings | boolean | Whether VIM bindings should be enabled or not, which allow the user to navigate using ctrl+n/j/p/k Default: true |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-command-root | '' | Present on the root element. |
A representation of the combobox input element, which is typically displayed in the content.
Property | Type | Description |
---|---|---|
value $bindable | string | The value of the search query. This is used to filter items and to search for items. Default: undefined |
ref $bindable | HTMLInputElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-command-input | '' | Present on the input element. |
The container for the viewport and its items of the command menu.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-command-list | '' | Present on the list element. |
CSS Variable | Description |
---|---|
--bits-command-list-height | The height of the command list element, which is computed by the |
The viewport component which contains the items of the command menu. This component tracks the height of the viewport and updates the --bits-command-list-height
CSS variable on the Command.List
component.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-command-viewport | '' | Present on the viewport element. |
A group of items within the command menu.
Property | Type | Description |
---|---|---|
value | string | If a Default: undefined |
forceMount | boolean | Whether or not the group should always be mounted to the DOM, regardless of the internal filtering logic Default: false |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-command-group | '' | Present on the group element. |
A heading for a group of items within the command menu.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-command-group-heading | '' | Present on the group heading element. |
The container for the items within a group.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-command-group-items | '' | Present on the group items element. |
Represents a single item within the command menu. If you wish to render an anchor element to link to a page, use the Command.LinkItem
component.
Property | Type | Description |
---|---|---|
value required | string | The value of the item. Default: undefined |
keywords | string[] | An array of additional keywords or aliases that will be used to filter the item. Default: undefined |
forceMount | boolean | Whether or not the item should always be mounted to the DOM, regardless of the internal filtering logic Default: false |
onSelect | function | A callback that is fired when the item is selected. Default: undefined |
disabled | boolean | Whether or not the combobox item is disabled. This will prevent interaction/selection. Default: false |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present when the item is disabled. |
data-selected | '' | Present when the item is selected. |
data-command-item | '' | Present on the item element. |
Similar to the Command.Item
component, but renders an anchor element to take advantage of preloading before navigation.
Property | Type | Description |
---|---|---|
value required | string | The value of the item. Default: undefined |
keywords | string[] | An array of additional keywords or aliases that will be used to filter the item. Default: undefined |
forceMount | boolean | Whether or not the item should always be mounted to the DOM, regardless of the internal filtering logic Default: false |
onSelect | function | A callback that is fired when the item is selected. Default: undefined |
disabled | boolean | Whether or not the combobox item is disabled. This will prevent interaction/selection. Default: false |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present when the item is disabled. |
data-selected | '' | Present when the item is selected. |
data-command-item | '' | Present on the item element. |
The empty state of the command menu. Shown when there are no items to display.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-command-empty | '' | Present on the empty element. |
The loading state of the command menu. Shown when the menu is loading items.
Property | Type | Description |
---|---|---|
progress | number | The progress of the loading state. Default: 0 |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-command-loading | '' | Present on the loading element. |
A visual separator for use between items and groups. Visible when the search query is empty or the forceMount
prop is true
.
Property | Type | Description |
---|---|---|
forceMount | boolean | Whether or not the separator should always be mounted to the DOM, regardless of the internal filtering logic Default: false |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-command-separator | '' | Present on the separator element. |