• Stars
    star
    471
  • Rank 93,216 (Top 2 %)
  • Language Svelte
  • License
    MIT License
  • Created about 1 year ago
  • Updated about 2 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

cmdk, but for Svelte โœจ

โŒ˜K-sv cmdk package version

A port of cmdk, to Svelte.

โŒ˜K-sv is a command menu Svelte component that can also be used as an accessible combobox. You render items, it filters and sorts them automatically.

Demo and examples: cmdk-sv.com

Install

npm install cmdk-sv

Use

<script lang="ts">
	import { Command } from 'cmdk-sv';
</script>

<Command.Root label="Command Menu">
	<Command.Input />
	<Command.List>
		<Command.Empty>No results found.</Command.Empty>

		<Command.Group heading="Letters">
			<Command.Item>a</Command.Item>
			<Command.Item>b</Command.Item>
			<Command.Separator />
			<Command.Item>c</Command.Item>
		</Command.Group>

		<Command.Item>Apple</Command.Item>
	</Command.List>
</Command.Root>

Or in a dialog:

<script lang="ts">
	import { Command } from 'cmdk-sv';
</script>

<Command.Dialog label="Command Menu">
	<Command.Input />
	<Command.List>
		<Command.Empty>No results found.</Command.Empty>

		<Command.Group heading="Letters">
			<Command.Item>a</Command.Item>
			<Command.Item>b</Command.Item>
			<Command.Separator />
			<Command.Item>c</Command.Item>
		</Command.Group>

		<Command.Item>Apple</Command.Item>
	</Command.List>
</Command.Dialog>

Styling

Each part has a specific data-attribute (starting with data-cmdk-) that can be used for styling.

Command [cmdk-root]

Render this to show the command menu inline, or use Dialog to render in a elevated context. Can be controlled by binding to the value prop.

<script lang="ts">
	import { Command } from 'cmdk-sv';

	let value = 'apple';
</script>

<Command.Root bind:value>
	<Command.Input />
	<Command.List>
		<Command.Item>Orange</Command.Item>
		<Command.Item>Apple</Command.Item>
	</Command.List>
</Command.Root>

You can provide a custom filter function that is called to rank each item. Both strings are normalized as lowercase and trimmed.

<Command.Root
	filter={(value, search) => {
		if (value.includes(search)) return 1;
		return 0;
	}}
/>

Or disable filtering and sorting entirely:

<Command.Root shouldFilter={false}>
	<Command.List>
		{#each filteredItems as item}
			<Command.Item value={item}>
				{item}
			</Command.Item>
		{/each}
	</Command.List>
</Command.Root>

You can make the arrow keys wrap around the list (when you reach the end, it goes back to the first item) by setting the loop prop:

<Command.Root loop />

This component also exposes two additional slot props for state (the current reactive value of the command state) and stateStore (the underlying writable state store). These can be used to implement more advanced use cases, such as debouncing the search updates with the stateStore.updateState method:

<Command.Root {state} let:stateStore>
	{@const handleUpdateState = debounce(stateStore.updateState, 200)}
	<CustomCommandInput {handleUpdateState} />
</Command.Root>

Dialog [cmdk-dialog] [cmdk-overlay]

Props are forwarded to Command. Composes Bits UI's Dialog component. The overlay is always rendered. See the Bits Documentation for more information. Can be controlled by binding to the open prop.

<script lang="ts">
	let open = false;
	let value: string;
</script>

<Command.Dialog bind:value bind:open>
	<!-- ... -->
</Command.Dialog>

You can provide a portal prop that accepts an HTML element that is forwarded to Bits UI's Dialog Portal component to specify which element the Dialog should portal into (defaults to body). To disable portalling, pass null as the portal prop.

<Command.Dialog portal={null} />

Input [cmdk-input]

All props are forwarded to the underlying input element. Can be controlled as a normal input by binding to its value prop.

<script lang="ts">
	import { Command } from 'cmdk-sv';

	let search = '';
</script>

<Command.Input bind:value={search} />

List [cmdk-list]

Contains items and groups. Animate height using the --cmdk-list-height CSS variable.

[data-cmdk-list] {
	min-height: 300px;
	height: var(--cmdk-list-height);
	max-height: 500px;
	transition: height 100ms ease;
}

To scroll item into view earlier near the edges of the viewport, use scroll-padding:

[data-cmdk-list] {
	scroll-padding-block-start: 8px;
	scroll-padding-block-end: 8px;
}

Item [cmdk-item] [data-disabled?] [data-selected?]

Item that becomes active on pointer enter. You should provide a unique value for each item, but it will be automatically inferred from the .textContent if you don't. Text content is normalized as lowercase and trimmed.

<Command.Item
	onSelect={(value) => {
		console.log('Selected', value);
		// Value is implicity "apple" because of the provided text content
	}}
>
	Apple
</Command.Item>

You can force an item to always render, regardless of filtering, by passing the alwaysRender prop.

Group [cmdk-group] [hidden?]

Groups items together with the given heading ([cmdk-group-heading]).

<Command.Group heading="Fruit">
	<Command.Item>Apple</Command.Item>
</Command.Group>

Groups will not be removed from the DOM, rather the hidden attribute is applied to hide it from view. This may be relevant in your styling.

You can force a group to always be visible, regardless of filtering, by passing the alwaysRender prop.

Separator [cmdk-separator]

Visible when the search query is empty or alwaysRender is true, hidden otherwise.

Empty [cmdk-empty]

Automatically renders when there are no results for the search query.

Loading [cmdk-loading]

You should conditionally render this with progress while loading asynchronous items.

<script lang="ts">
	import { Command } from 'cmdk-sv';

	let loading = false;
</script>

<Command.List>
	{#if loading}
		<Command.Loading progress={0.5}>Loadingโ€ฆ</Command.Loading>
	{/if}
</Command.List>;

createState(initialState?: State)

Create a state store which can be passed and used by the component. This is provided for more advanced use cases and should not be commonly used.

A good use case would be to render a more detailed empty state, like so:

<script lang="ts">
	import { Command, createState } from 'cmdk-sv';

	const state = createState();
</script>

<Command.Root {state}>
	<Command.Empty>
		{#if $state.search}
			No results found for "{state.search}".
		{:else}
			No results found.
		{/if}
	</Command.Empty>
</Command.Root>

Examples

Code snippets for common use cases.

Nested items

Often selecting one item should navigate deeper, with a more refined set of items. For example selecting "Change themeโ€ฆ" should show new items "Dark theme" and "Light theme". We call these sets of items "pages", and they can be implemented with simple state:

<script lang="ts">
	let open = false;
	let search = '';
	let pages: string[] = [];
	let page: string | undefined = undefined;

	$: page = pages[pages.length - 1];

	function changePage(newPage: string) {
		pages = [...pages, newPage];
	}
</script>

<Command
	onKeyDown={(e) => {
		// Escape goes to previous page
		// Backspace goes to previous page when search is empty
		if (e.key === 'Escape' || (e.key === 'Backspace' && !search)) {
			e.preventDefault();
			const newPages = pages.slice(0, -1);
			pages = newPages;
		}
	}}
>
	<Command.Input bind:value={search} />
	<Command.List>
		{#if !page}
			<Command.Item onSelect={() => changePage('projects')}>Search projectsโ€ฆ</Command.Item>
			<Command.Item onSelect={() => changePage('teams')}>Join a teamโ€ฆ</Command.Item>
		{:else if page === 'projects'}
			<Command.Item>Project A</Command.Item>
			<Command.Item>Project B</Command.Item>
		{:else if page === 'teams'}
			<Command.Item>Team 1</Command.Item>
			<Command.Item>Team 2</Command.Item>
		{/if}
	</Command.List>
</Command>

Show sub-items when searching

If your items have nested sub-items that you only want to reveal when searching, render based on the search state:

<!-- SubItem.svelte -->
<script lang="ts">
	import { Command } from 'cmdk-sv';

	type $$Props = Command.ItemProps & {
		search?: string;
	};
</script>

{#if search}
	<Command.Item {...$$restProps}>
		<slot />
	</Command.Item>
{/if}

Using the state store:

<!-- CommandMenu.svelte -->
<script lang="ts">
	import { Command, createState } from 'cmdk-sv';
	import SubItem from './SubItem.svelte';
	const state = createState();
</script>

<Command.Root {state}>
	<Command.Input />
	<Command.List>
		<Command.Item>Change themeโ€ฆ</Command.Item>
		<SubItem search={$state.search}>Change theme to dark</SubItem>
		<SubItem search={$state.search}>Change theme to light</SubItem>
	</Command.List>
</Command.Root>

or

Using the input value:

<!-- CommandMenu.svelte -->
<script lang="ts">
	import { Command } from 'cmdk-sv';
	import SubItem from './SubItem.svelte';
	let search: string;
</script>

<Command.Root>
	<Command.Input bind:value={search} />
	<Command.List>
		<Command.Item>Change themeโ€ฆ</Command.Item>
		<SubItem {search}>Change theme to dark</SubItem>
		<SubItem {search}>Change theme to light</SubItem>
	</Command.List>
</Command.Root>

Asynchronous results

Render the items as they become available. Filtering and sorting will happen automatically.

<script lang="ts">
	import { Command } from 'cmdk-sv';

	let loading = false;
	let items: string[] = [];

	onMount(async () => {
		loading = true;
		const res = await api.get('/dictionary');
		items = res;
		loading = false;
	});
</script>

<Command.Root>
	<Command.Input />
	<Command.List>
		{#if loading}
			<Command.Loading>Fetching wordsโ€ฆ</Command.Loading>
		{:else}
			{#each items as item}
				<Command.Item value={item}>
					{item}
				</Command.Item>
			{/each}
		{/if}
	</Command.List>
</Command.Root>

Use inside Popover

We recommend using the Bits UI popover component. โŒ˜K-sv relies on the Bits UI Dialog component, so this will reduce the number of dependencies you'll need.

npm install bits-ui

Render Command inside of the popover content:

<script lang="ts">
	import { Command } from 'cmdk-sv';
	import { Popover } from 'bits-ui';
</script>

<Popover.Root>
	<Popover.Trigger>Toggle popover</Popover.Trigger>

	<Popover.Content>
		<Command.Root>
			<Command.Input />
			<Command.List>
				<Command.Item>Apple</Command.Item>
			</Command.List>
		</Command.Root>
	</Popover.Content>
</Popover.Root>

Drop in stylesheets

You can find global stylesheets to drop in as a starting point for styling. See src/styles/cmdk for examples.

Render Delegation

Each of the components (except the dialog) accept an asChild prop that can be used to render a custom element in place of the default. When using this prop, you'll need to check the components slot props to see what attributes & actions you'll need to pass to your custom element.

Components that contain only a single element will just have attrs & action slot props, or just attrs. Components that contain multiple elements will have an attrs and possibly an actions object whose properties are the attributes and actions for each element.

FAQ

Accessible? Yes. Labeling, aria attributes, and DOM ordering tested with Voice Over and Chrome DevTools. Dialog composes an accessible Dialog implementation.

Filter/sort items manually? Yes. Pass shouldFilter={false} to Command. Better memory usage and performance. Bring your own virtualization this way.

Unstyled? Yes, use the listed CSS selectors.

Weird/wrong behavior? Make sure your Command.Item has a unique value.

Listen for โŒ˜K automatically? No, do it yourself to have full control over keybind context.

History

Written in 2019 by Paco (@pacocoursey) to see if a composable combobox API was possible. Used for the Vercel command menu and autocomplete by Rauno (@raunofreiberg) in 2020. Re-written independently in 2022 with a simpler and more performant approach. Ideas and help from Shu (@shuding_).

Ported to Svelte in 2023 by Huntabyte (@huntabyte)

More Repositories

1

shadcn-svelte

shadcn/ui, but for Svelte. โœจ
Svelte
5,058
star
2

bits-ui

The headless components for Svelte.
TypeScript
1,205
star
3

vaul-svelte

An unstyled drawer component for Svelte.
TypeScript
438
star
4

chatty

A chatbot application build with OpenAI's ChatGPT API.
TypeScript
182
star
5

tig-stack

The Telegraf, InfluxDB, & Grafana stack (TIG) powered by Docker & Docker Compose.
Shell
117
star
6

showcase

Full stack application built with SvelteKit & PocketBase.
Svelte
91
star
7

modern-saas

Source code for Modern SaaS Apps with SvelteKit, Stripe, & Supabase
TypeScript
61
star
8

sveltekit-lucia-prisma

TypeScript
57
star
9

sk-supabase-auth

Learn how to add Supabase Authentication to your SvelteKit application.
CSS
53
star
10

sveltekit-pocketbase-auth

Source code for the 'Authentication with SvelteKit & PocketBase' video on my YouTube channel
Svelte
37
star
11

sveltekit-protected-routes

CSS
34
star
12

projecthunt

A dev community built with SvelteKit & PocketBase live on my YouTube channel.
Svelte
34
star
13

noted-skeletonui

Svelte
31
star
14

hire-nerds

JavaScript
23
star
15

sveltekit-loading-data

Final source code for the 'Loading Data in SvelteKit' video on my YouTube channel.
JavaScript
23
star
16

sk-prisma

TypeScript
22
star
17

sveltekit-actions

Final source code for the 'Form Actions in SvelteKit' video on my YouTube channel.
Svelte
22
star
18

svelte-search-filter

An example of Searching/Filtering Data in Svelte.
CSS
19
star
19

elif-ai

TypeScript
19
star
20

huntabyte.com-admin

(WIP) Full-Stack personal custom CMS/Admin Panel.
Svelte
18
star
21

sveltekit-redis-caching

TypeScript
14
star
22

sk-supabase

Svelte
13
star
23

sveltekit-theme-switcher

Svelte
13
star
24

sveltekit-endpoints

JavaScript
13
star
25

advent-of-svelte-2023

Svelte
13
star
26

superforms-demo

JavaScript
13
star
27

sk-layout-auth-concerns

A repository for a video demonstrating the concerns of using +layout.server to authenticate/authorize and protect routes.
TypeScript
12
star
28

rate-limiting

Svelte
12
star
29

sk-supabase-oauth

TypeScript
10
star
30

better-redirects

TypeScript
9
star
31

sk-pg

TypeScript
7
star
32

sk-use-enhance

Svelte
6
star
33

sk2-shallow-routing

Svelte
6
star
34

purple

Svelte
6
star
35

sveltekit-form-validation

Final source code for the 'Loading Data in SvelteKit' video on my YouTube channel.
JavaScript
5
star
36

sk-error-handling

Svelte
5
star
37

netnotics

Full-Stack Infrastructure Management Web Application
Python
5
star
38

huntabyte.com

(WIP) Personal Site
TypeScript
5
star
39

comp-parser

๐Ÿ“ƒExtract props, actions, slots and css variables from Svelte components.
TypeScript
5
star
40

sveltekit-params

JavaScript
4
star
41

sveltekit-layouts

Svelte
4
star
42

sk-snapshots

Svelte
4
star
43

sk-defer

JavaScript
4
star
44

flask-article-api

Python
4
star
45

catalyst-8000-wizard

A simple quiz wizard to determine which Catalyst 8000 Series router you will need based on your requirements.
JavaScript
3
star
46

sk-sb-realtime

TypeScript
3
star
47

nestjs-starter-redis

NextJS dev quick start with user authentication, MySQL, & Redis for session storage.
TypeScript
3
star
48

asynchronet

Asynchronous multi-vendor library for interacting with network devices, inspired by Netmiko. (fork of netdev).
HTML
3
star
49

appwrite-crud

JavaScript
2
star
50

adv-flask-rest-api

Python
2
star
51

shadcn-repro-template

Reproduction template for shadcn-svelte issues.
Svelte
2
star
52

go-rock-paper-scissors

A simple rock, paper, scissors web app built with Go, Javascript, & HTML. Built this to learn the fundamentals of web apps with Go.
Go
2
star
53

flask-restx-boilerplate

Python
2
star
54

seat-selector

WIP airplane seat selector widget built on stream
CSS
2
star
55

flash-messages

Source code for my video on the SvelteKit Flash Message library.
Svelte
2
star
56

summarizer-api

Python
2
star
57

vscode-settings

2
star
58

movie-info-app

JavaScript
2
star
59

recipe-app-api

Recipe app API source code. Built to gain experience with Django and the Django REST framework.
Python
2
star
60

newsgrid

Example news website
HTML
2
star
61

nestjs-basic-sessions

Integration of basic session authentication with NestJS
TypeScript
2
star
62

huntabyte.com-old

Personal blog built with Hugo and running on GitHub Pages.
HTML
2
star
63

huntabyte

Profile ReadMe
2
star
64

movie-matchup

JavaScript
2
star
65

GNS3

2
star
66

supabase-invalidate-loop-repro

TypeScript
2
star
67

elk-stack

Shell
2
star
68

blog

2
star
69

hb

A quick start project with all of my usual configs/packages.
CSS
2
star
70

chat-app

A real-time chat application built using React, Firebase (Authentication & Firestore), and styled components. Built to learn the integration of the included technologies.
JavaScript
2
star
71

template-svelte-lib

TypeScript
1
star
72

eslint-repro

JavaScript
1
star
73

static

TypeScript
1
star
74

melt-ui-md

TypeScript
1
star
75

hj.run

My personal website
TypeScript
1
star
76

component-props

JavaScript
1
star
77

menubar-repro

Svelte
1
star