Slash Command Suggestions
The command picker that opens above the composer, with an app rail, grouped options and a matched-command preview.
The command picker that opens above the composer, with an app rail, grouped options and a matched-command preview. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.
import { SlashCommand } from "@/registry/bases/newt/ui/slash-command"
import {
SlashCommandAppIcon,
SlashCommandSuggestion,
SlashCommandSuggestionGroup,
SlashCommandSuggestionMatched,
SlashCommandSuggestions,
SlashCommandSuggestionsHeader,
SlashCommandSuggestionsList,
SlashCommandSuggestionsRail,
SlashCommandSuggestionsRailItem,
} from "@/registry/bases/newt/ui/slash-command-suggestions"
const ShieldIcon = () => (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2 4 5v7c0 5 3.4 8.4 8 10 4.6-1.6 8-5 8-10V5l-8-3Z" />
</svg>
)
export default function SlashCommandSuggestionsDemo() {
return (
<SlashCommandSuggestions>
<SlashCommandSuggestionsRail aria-label="Filter commands by app">
<SlashCommandSuggestionsRailItem active aria-label="Frequently used">
<svg viewBox="0 0 24 24" className="h-5 w-5" fill="currentColor">
<path d="M12 3a9 9 0 1 0 9 9h-2a7 7 0 1 1-7-7v4l5-4-5-4v2Z" />
<path d="M11 7h2v5l4 2-1 1.7L11 13V7Z" />
</svg>
</SlashCommandSuggestionsRailItem>
<SlashCommandSuggestionsRailItem aria-label="Moderation commands">
<SlashCommandAppIcon size="rail">
<ShieldIcon />
</SlashCommandAppIcon>
</SlashCommandSuggestionsRailItem>
</SlashCommandSuggestionsRail>
<SlashCommandSuggestionsList aria-label="Slash commands">
<SlashCommandSuggestionsHeader>
Frequently Used
</SlashCommandSuggestionsHeader>
<SlashCommandSuggestionGroup aria-label="Moderation">
<SlashCommandSuggestion
active
name="ban"
description="Ban a member from the server."
appLabel="Moderation"
icon={
<SlashCommandAppIcon>
<ShieldIcon />
</SlashCommandAppIcon>
}
/>
<SlashCommandSuggestion
name="timeout"
description="Mute a member for a set amount of time."
appLabel="Moderation"
icon={
<SlashCommandAppIcon>
<ShieldIcon />
</SlashCommandAppIcon>
}
/>
<SlashCommandSuggestion
disabled
name="verify"
description="Only available to server administrators."
appLabel="Moderation"
icon={
<SlashCommandAppIcon>
<ShieldIcon />
</SlashCommandAppIcon>
}
/>
</SlashCommandSuggestionGroup>
<SlashCommandSuggestionMatched>
<SlashCommand
name="ban"
options={[{ name: "user", value: "@username" }]}
/>
</SlashCommandSuggestionMatched>
</SlashCommandSuggestionsList>
</SlashCommandSuggestions>
)
}<script setup lang="ts">
import { SlashCommand } from "~~/registry/bases/newt/ui/slash-command"
import {
SlashCommandAppIcon,
SlashCommandSuggestion,
SlashCommandSuggestionGroup,
SlashCommandSuggestionMatched,
SlashCommandSuggestions,
SlashCommandSuggestionsHeader,
SlashCommandSuggestionsList,
SlashCommandSuggestionsRail,
SlashCommandSuggestionsRailItem,
} from "~~/registry/bases/newt/ui/slash-command-suggestions"
const shieldPath = "M12 2 4 5v7c0 5 3.4 8.4 8 10 4.6-1.6 8-5 8-10V5l-8-3Z"
</script>
<template>
<SlashCommandSuggestions>
<SlashCommandSuggestionsRail aria-label="Filter commands by app">
<SlashCommandSuggestionsRailItem active aria-label="Frequently used">
<svg viewBox="0 0 24 24" class="h-5 w-5" fill="currentColor">
<path d="M12 3a9 9 0 1 0 9 9h-2a7 7 0 1 1-7-7v4l5-4-5-4v2Z" />
<path d="M11 7h2v5l4 2-1 1.7L11 13V7Z" />
</svg>
</SlashCommandSuggestionsRailItem>
<SlashCommandSuggestionsRailItem aria-label="Moderation commands">
<SlashCommandAppIcon size="rail">
<svg viewBox="0 0 24 24" fill="currentColor">
<path :d="shieldPath" />
</svg>
</SlashCommandAppIcon>
</SlashCommandSuggestionsRailItem>
</SlashCommandSuggestionsRail>
<SlashCommandSuggestionsList aria-label="Slash commands">
<SlashCommandSuggestionsHeader>
Frequently Used
</SlashCommandSuggestionsHeader>
<SlashCommandSuggestionGroup aria-label="Moderation">
<SlashCommandSuggestion
active
name="ban"
description="Ban a member from the server."
app-label="Moderation"
>
<template #icon>
<SlashCommandAppIcon>
<svg viewBox="0 0 24 24" fill="currentColor">
<path :d="shieldPath" />
</svg>
</SlashCommandAppIcon>
</template>
</SlashCommandSuggestion>
<SlashCommandSuggestion
name="timeout"
description="Mute a member for a set amount of time."
app-label="Moderation"
>
<template #icon>
<SlashCommandAppIcon>
<svg viewBox="0 0 24 24" fill="currentColor">
<path :d="shieldPath" />
</svg>
</SlashCommandAppIcon>
</template>
</SlashCommandSuggestion>
<SlashCommandSuggestion
disabled
name="verify"
description="Only available to server administrators."
app-label="Moderation"
>
<template #icon>
<SlashCommandAppIcon>
<svg viewBox="0 0 24 24" fill="currentColor">
<path :d="shieldPath" />
</svg>
</SlashCommandAppIcon>
</template>
</SlashCommandSuggestion>
</SlashCommandSuggestionGroup>
<SlashCommandSuggestionMatched>
<SlashCommand
name="ban"
:options="[{ name: 'user', value: '@username' }]"
/>
</SlashCommandSuggestionMatched>
</SlashCommandSuggestionsList>
</SlashCommandSuggestions>
</template>Installation
pnpm dlx newtui@latest add slash-command-suggestionsnpx newtui@latest add slash-command-suggestionsyarn dlx newtui@latest add slash-command-suggestionsbunx newtui@latest add slash-command-suggestionsInstall the dependencies
pnpm dlx --yes class-variance-authoritynpx --yes class-variance-authorityyarn dlx --yes class-variance-authoritybunx --yes class-variance-authorityAdd the components it builds on
Copy the source into your project
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"
/*
* The picker that opens above the composer once a message starts with `/`.
* Focus stays in the composer: wire `aria-activedescendant` on the input to
* the id of the active option rather than moving focus into the list.
*/
const SlashCommandSuggestions = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"flex max-h-80 w-full max-w-[640px] overflow-hidden rounded-lg bg-newt-bg-floating text-newt-text-primary shadow-elevation-high",
className
)}
{...props}
/>
))
SlashCommandSuggestions.displayName = "SlashCommandSuggestions"
/* Vertical strip of app icons that filters the list by app. */
const SlashCommandSuggestionsRail = React.forwardRef<
HTMLElement,
React.ComponentProps<"nav">
>(({ className, ...props }, ref) => (
<nav
ref={ref}
className={cn(
"flex shrink-0 flex-col items-center gap-2 overflow-y-auto border-e border-newt-border bg-newt-bg-base p-2",
className
)}
{...props}
/>
))
SlashCommandSuggestionsRail.displayName = "SlashCommandSuggestionsRail"
export interface SlashCommandSuggestionsRailItemProps extends React.ComponentProps<"button"> {
active?: boolean
}
const SlashCommandSuggestionsRailItem = React.forwardRef<
HTMLButtonElement,
SlashCommandSuggestionsRailItemProps
>(({ className, active = false, type = "button", ...props }, ref) => (
<button
ref={ref}
type={type}
aria-pressed={active}
data-state={active ? "active" : "inactive"}
className={cn(
"inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-newt-text-muted transition-colors duration-fast ease-newt",
"hover:bg-newt-bg-hover hover:text-newt-text-primary",
"data-[state=active]:bg-newt-bg-active data-[state=active]:text-newt-text-primary",
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link",
className
)}
{...props}
/>
))
SlashCommandSuggestionsRailItem.displayName = "SlashCommandSuggestionsRailItem"
const SlashCommandSuggestionsList = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, role = "listbox", ...props }, ref) => (
<div
ref={ref}
role={role}
className={cn("min-w-0 flex-1 overflow-y-auto p-2", className)}
{...props}
/>
))
SlashCommandSuggestionsList.displayName = "SlashCommandSuggestionsList"
/* Sticky section header, as used by the "Frequently Used" block. */
const SlashCommandSuggestionsHeader = React.forwardRef<
HTMLParagraphElement,
React.ComponentProps<"p">
>(({ className, ...props }, ref) => (
<p
ref={ref}
role="presentation"
className={cn(
"sticky top-0 z-[1] m-0 flex items-center gap-1.5 bg-newt-bg-floating px-2 py-1.5 text-sm font-semibold text-newt-text-primary",
className
)}
{...props}
/>
))
SlashCommandSuggestionsHeader.displayName = "SlashCommandSuggestionsHeader"
const SlashCommandSuggestionGroup = React.forwardRef<
HTMLElement,
React.ComponentProps<"section">
>(({ className, role = "group", ...props }, ref) => (
<section
ref={ref}
role={role}
className={cn("min-w-0", className)}
{...props}
/>
))
SlashCommandSuggestionGroup.displayName = "SlashCommandSuggestionGroup"
const appIconVariants = cva(
"inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-newt-bg-elevated text-newt-text-muted [&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>svg]:h-1/2 [&>svg]:w-1/2",
{
variants: {
size: {
header: "h-4 w-4",
row: "h-6 w-6",
rail: "h-8 w-8",
},
},
defaultVariants: { size: "row" },
}
)
export interface SlashCommandAppIconProps
extends React.ComponentProps<"span">, VariantProps<typeof appIconVariants> {}
const SlashCommandAppIcon = React.forwardRef<
HTMLSpanElement,
SlashCommandAppIconProps
>(({ className, size, ...props }, ref) => (
<span
ref={ref}
aria-hidden="true"
className={cn(appIconVariants({ size }), className)}
{...props}
/>
))
SlashCommandAppIcon.displayName = "SlashCommandAppIcon"
export interface SlashCommandSuggestionProps extends Omit<
React.ComponentProps<"div">,
"children"
> {
/** Command path without the leading slash, e.g. `moderation ban`. */
name: string
description?: string
/** Name of the app the command belongs to, shown on the right. */
appLabel?: string
/** Icon for the owning app; pair with `<SlashCommandAppIcon>`. */
icon?: React.ReactNode
active?: boolean
disabled?: boolean
}
const SlashCommandSuggestion = React.forwardRef<
HTMLDivElement,
SlashCommandSuggestionProps
>(
(
{
className,
name,
description,
appLabel,
icon,
active = false,
disabled = false,
...props
},
ref
) => (
<div
ref={ref}
role="option"
aria-selected={active}
aria-disabled={disabled || undefined}
data-state={active ? "active" : "inactive"}
className={cn(
"grid min-h-12 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 rounded-md px-2 py-1.5",
disabled
? "cursor-default opacity-60"
: "cursor-pointer hover:bg-newt-bg-hover",
"data-[state=active]:bg-newt-bg-active",
className
)}
{...props}
>
{icon}
<div className="flex min-w-0 flex-col gap-0.5">
{/* The client spaces the slash away from the command path. */}
<span className="text-[15px] font-semibold leading-tight text-newt-text-primary">
/ {name}
</span>
{description ? (
<span className="truncate text-[13px] leading-snug text-newt-text-secondary">
{description}
</span>
) : null}
</div>
{appLabel ? (
<span className="shrink-0 text-[13px] text-newt-text-secondary">
{appLabel}
</span>
) : null}
</div>
)
)
SlashCommandSuggestion.displayName = "SlashCommandSuggestion"
export interface SlashCommandSuggestionMatchedProps extends React.ComponentProps<"div"> {
active?: boolean
}
/* Row that previews the command already typed, rendered with `<SlashCommand>`. */
const SlashCommandSuggestionMatched = React.forwardRef<
HTMLDivElement,
SlashCommandSuggestionMatchedProps
>(({ className, active = false, ...props }, ref) => (
<div
ref={ref}
role="option"
aria-selected={active}
data-state={active ? "active" : "inactive"}
className={cn(
"min-h-11 cursor-pointer rounded-md px-3 py-2 hover:bg-newt-bg-hover",
"data-[state=active]:bg-newt-bg-active",
className
)}
{...props}
/>
))
SlashCommandSuggestionMatched.displayName = "SlashCommandSuggestionMatched"
export {
SlashCommandSuggestions,
SlashCommandSuggestionsRail,
SlashCommandSuggestionsRailItem,
SlashCommandSuggestionsList,
SlashCommandSuggestionsHeader,
SlashCommandSuggestionGroup,
SlashCommandAppIcon,
SlashCommandSuggestion,
SlashCommandSuggestionMatched,
}<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<!--
The picker that opens above the composer once a message starts with `/`.
Focus stays in the composer: wire `aria-activedescendant` on the input to
the id of the active option rather than moving focus into the list.
-->
<div
:class="
cn(
'flex max-h-80 w-full max-w-[640px] overflow-hidden rounded-lg bg-newt-bg-floating text-newt-text-primary shadow-elevation-high',
props.class
)
"
>
<slot />
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<!-- Vertical strip of app icons that filters the list by app. -->
<nav
:class="
cn(
'flex shrink-0 flex-col items-center gap-2 overflow-y-auto border-e border-newt-border bg-newt-bg-base p-2',
props.class
)
"
>
<slot />
</nav>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = withDefaults(
defineProps<{ class?: HTMLAttributes["class"]; active?: boolean }>(),
{ active: false }
)
</script>
<template>
<button
type="button"
:aria-pressed="props.active"
:data-state="props.active ? 'active' : 'inactive'"
:class="
cn(
'inline-flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-newt-text-muted transition-colors duration-fast ease-newt',
'hover:bg-newt-bg-hover hover:text-newt-text-primary',
'data-[state=active]:bg-newt-bg-active data-[state=active]:text-newt-text-primary',
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link',
props.class
)
"
>
<slot />
</button>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
role="listbox"
:class="cn('min-w-0 flex-1 overflow-y-auto p-2', props.class)"
>
<slot />
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<!-- Sticky section header, as used by the "Frequently Used" block. -->
<p
role="presentation"
:class="
cn(
'sticky top-0 z-[1] m-0 flex items-center gap-1.5 bg-newt-bg-floating px-2 py-1.5 text-sm font-semibold text-newt-text-primary',
props.class
)
"
>
<slot />
</p>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<section role="group" :class="cn('min-w-0', props.class)">
<slot />
</section>
</template><script setup lang="ts">
import { cva } from "class-variance-authority"
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const appIconVariants = cva(
"inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-newt-bg-elevated text-newt-text-muted [&>img]:h-full [&>img]:w-full [&>img]:object-cover [&>svg]:h-1/2 [&>svg]:w-1/2",
{
variants: {
size: {
header: "h-4 w-4",
row: "h-6 w-6",
rail: "h-8 w-8",
},
},
defaultVariants: { size: "row" },
}
)
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
size?: "header" | "row" | "rail"
}>(),
{ size: "row" }
)
</script>
<template>
<span
aria-hidden="true"
:class="cn(appIconVariants({ size: props.size }), props.class)"
>
<slot />
</span>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
/** Command path without the leading slash, e.g. `moderation ban`. */
name: string
description?: string
/** Name of the app the command belongs to, shown on the right. */
appLabel?: string
active?: boolean
disabled?: boolean
}>(),
{ active: false, disabled: false }
)
</script>
<template>
<div
role="option"
:aria-selected="props.active"
:aria-disabled="props.disabled || undefined"
:data-state="props.active ? 'active' : 'inactive'"
:class="
cn(
'grid min-h-12 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 rounded-md px-2 py-1.5',
props.disabled
? 'cursor-default opacity-60'
: 'cursor-pointer hover:bg-newt-bg-hover',
'data-[state=active]:bg-newt-bg-active',
props.class
)
"
>
<slot name="icon" />
<div class="flex min-w-0 flex-col gap-0.5">
<!-- The client spaces the slash away from the command path. -->
<span
class="text-[15px] font-semibold leading-tight text-newt-text-primary"
>
/ {{ props.name }}
</span>
<span
v-if="props.description"
class="truncate text-[13px] leading-snug text-newt-text-secondary"
>
{{ props.description }}
</span>
</div>
<span
v-if="props.appLabel"
class="shrink-0 text-[13px] text-newt-text-secondary"
>
{{ props.appLabel }}
</span>
</div>
</template><script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = withDefaults(
defineProps<{ class?: HTMLAttributes["class"]; active?: boolean }>(),
{ active: false }
)
</script>
<template>
<!-- Row that previews the command already typed, rendered with `<SlashCommand>`. -->
<div
role="option"
:aria-selected="props.active"
:data-state="props.active ? 'active' : 'inactive'"
:class="
cn(
'min-h-11 cursor-pointer rounded-md px-3 py-2 hover:bg-newt-bg-hover',
'data-[state=active]:bg-newt-bg-active',
props.class
)
"
>
<slot />
</div>
</template>export { default as SlashCommandSuggestions } from "./SlashCommandSuggestions.vue"
export { default as SlashCommandSuggestionsRail } from "./SlashCommandSuggestionsRail.vue"
export { default as SlashCommandSuggestionsRailItem } from "./SlashCommandSuggestionsRailItem.vue"
export { default as SlashCommandSuggestionsList } from "./SlashCommandSuggestionsList.vue"
export { default as SlashCommandSuggestionsHeader } from "./SlashCommandSuggestionsHeader.vue"
export { default as SlashCommandSuggestionGroup } from "./SlashCommandSuggestionGroup.vue"
export { default as SlashCommandAppIcon } from "./SlashCommandAppIcon.vue"
export { default as SlashCommandSuggestion } from "./SlashCommandSuggestion.vue"
export { default as SlashCommandSuggestionMatched } from "./SlashCommandSuggestionMatched.vue"Update the import paths
The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.
Usage
import { SlashCommandSuggestions } from "@/components/ui/slash-command-suggestions"
export function Example() {
return <SlashCommandSuggestions />
}<script setup lang="ts">
import { SlashCommandSuggestions, SlashCommandSuggestionsRail, SlashCommandSuggestionsRailItem, SlashCommandSuggestionsList, SlashCommandSuggestionsHeader, SlashCommandSuggestionGroup, SlashCommandAppIcon, SlashCommandSuggestion, SlashCommandSuggestionMatched } from "@/components/ui/slash-command-suggestions"
</script>
<template>
<SlashCommandSuggestions />
</template>API reference
SlashCommandSuggestionsRailItem
active?boolean | undefined
Optional active value.
boolean | undefinedSlashCommandSuggestion
name?string
Command path without the leading slash, e.g. `moderation ban`.
stringdescription?string | undefined
Optional description value.
string | undefinedappLabel?string | undefined
Name of the app the command belongs to, shown on the right.
string | undefinedicon?React.ReactNode | undefined
Icon for the owning app; pair with `<SlashCommandAppIcon>`.
React.ReactNode | undefinedactive?boolean | undefined
Optional active value.
boolean | undefineddisabled?boolean | undefined
Optional disabled value.
boolean | undefinedSlashCommandSuggestionMatched
active?boolean | undefined
Optional active value.
boolean | undefinedThe registry exports SlashCommandSuggestions, SlashCommandSuggestionsRail, SlashCommandSuggestionsRailItem, SlashCommandSuggestionsList, SlashCommandSuggestionsHeader, SlashCommandSuggestionGroup, SlashCommandAppIcon, SlashCommandSuggestion, SlashCommandSuggestionMatched. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.
Accessibility
Keep focus in the composer and point aria-activedescendant at the active option. Announce disabled options and label both the app rail and command list.
Tokens
Slash Command Suggestions reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer.