Slash Command
An inline /command with its subcommand path and typed options, including the focused one.
An inline /command with its subcommand path and typed options, including the focused one. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.
newt-commandimport { SlashCommand } from "@/registry/bases/newt/ui/slash-command"
export default function SlashCommandDemo() {
return (
<div className="flex flex-col items-start gap-3">
<SlashCommand name="command" />
<SlashCommand
name="ban"
subcommand="member"
options={[
{ name: "user", value: "@username" },
{
name: "reason",
description: "why they were banned",
focused: true,
},
]}
/>
</div>
)
}<script setup lang="ts">
import { SlashCommand } from "~~/registry/bases/newt/ui/slash-command"
const banOptions = [
{ name: "user", value: "@username" },
{ name: "reason", description: "why they were banned", focused: true },
]
</script>
<template>
<div class="flex flex-col items-start gap-3">
<SlashCommand name="command" />
<SlashCommand name="ban" subcommand="member" :options="banOptions" />
</div>
</template>Installation
pnpm dlx newtui@latest add slash-commandnpx newtui@latest add slash-commandyarn dlx newtui@latest add slash-commandbunx newtui@latest add slash-commandCopy the source into your project
import * as React from "react"
import { cn } from "@/lib/utils"
export interface SlashCommandOption {
name: string
/** The value the user has entered, if any. */
value?: string
/** Stands in for a missing value, the way the client previews the option. */
description?: string
/** The option the caret currently sits in. */
focused?: boolean
}
export interface SlashCommandProps extends Omit<
React.HTMLAttributes<HTMLSpanElement>,
"children"
> {
/** Command name rendered after the leading slash. */
name: string
/** Subcommand segment, e.g. `/ban member`. */
subcommand?: string
/** Subcommand group segment, e.g. `/settings roles add`. */
subcommandGroup?: string
options?: readonly SlashCommandOption[]
}
/** `/name group sub` — the segments a caller can actually reach. */
function commandPath(
props: Pick<SlashCommandProps, "name" | "subcommand" | "subcommandGroup">
) {
return [props.name, props.subcommandGroup, props.subcommand]
.filter(Boolean)
.join(" ")
}
function describe(
path: string,
options: readonly SlashCommandOption[]
): string {
const spoken = options
.map(
(option) =>
`${option.name}: ${option.value ?? option.description ?? option.name}`
)
.join(" ")
return `Slash command /${path}${spoken ? ` ${spoken}` : ""}`
}
const SlashCommand = React.forwardRef<HTMLSpanElement, SlashCommandProps>(
(
{ className, name, subcommand, subcommandGroup, options = [], ...props },
ref
) => {
const path = commandPath({ name, subcommand, subcommandGroup })
return (
<span
ref={ref}
role="group"
aria-label={describe(path, options)}
className={cn(
"inline-flex flex-wrap items-center gap-1 rounded-sm bg-[color-mix(in_srgb,var(--newt-brand)_15%,transparent)] py-[2px] ps-[6px] pe-2 font-mono text-[13px] text-newt-mention-text",
className
)}
{...props}
>
<span className="opacity-80" aria-hidden="true">
/
</span>
<span className="font-semibold">{name}</span>
{subcommandGroup ? (
<span className="font-semibold">{subcommandGroup}</span>
) : null}
{subcommand ? (
<span className="font-semibold">{subcommand}</span>
) : null}
{options.map((option) => (
<span
key={option.name}
className={cn(
"inline-flex items-center gap-1",
/* The option the caret currently sits in. */
option.focused &&
"rounded-sm bg-[color-mix(in_srgb,var(--newt-brand)_35%,transparent)] px-0.5"
)}
>
<span className="text-newt-text-muted after:content-[':']">
{option.name}
</span>
{option.value ? (
<span className="font-medium text-newt-text-primary">
{option.value}
</span>
) : (
/* No value yet — the option's description stands in. */
<span className="italic text-newt-text-muted">
{option.description ?? option.name}
</span>
)}
</span>
))}
</span>
)
}
)
SlashCommand.displayName = "SlashCommand"
export { SlashCommand }<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { computed } from "vue"
import { cn } from "@/lib/utils"
import type { SlashCommandOption } from "."
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
/** Command name rendered after the leading slash. */
name: string
/** Subcommand segment, e.g. `/ban member`. */
subcommand?: string
/** Subcommand group segment, e.g. `/settings roles add`. */
subcommandGroup?: string
options?: readonly SlashCommandOption[]
}>(),
{ options: () => [] }
)
/** `/name group sub` — the segments a caller can actually reach. */
const path = computed(() =>
[props.name, props.subcommandGroup, props.subcommand]
.filter(Boolean)
.join(" ")
)
const label = computed(() => {
const spoken = props.options
.map(
(option) =>
`${option.name}: ${option.value ?? option.description ?? option.name}`
)
.join(" ")
return `Slash command /${path.value}${spoken ? ` ${spoken}` : ""}`
})
</script>
<template>
<span
role="group"
:aria-label="label"
:class="
cn(
'inline-flex flex-wrap items-center gap-1 rounded-sm bg-[color-mix(in_srgb,var(--newt-brand)_15%,transparent)] py-[2px] ps-[6px] pe-2 font-mono text-[13px] text-newt-mention-text',
props.class
)
"
>
<span class="opacity-80" aria-hidden="true">/</span>
<span class="font-semibold">{{ props.name }}</span>
<span v-if="props.subcommandGroup" class="font-semibold">
{{ props.subcommandGroup }}
</span>
<span v-if="props.subcommand" class="font-semibold">
{{ props.subcommand }}
</span>
<span
v-for="option of props.options"
:key="option.name"
:class="
cn(
'inline-flex items-center gap-1',
// The option the caret currently sits in.
option.focused &&
'rounded-sm bg-[color-mix(in_srgb,var(--newt-brand)_35%,transparent)] px-0.5'
)
"
>
<span class="text-newt-text-muted after:content-[':']">
{{ option.name }}
</span>
<span v-if="option.value" class="font-medium text-newt-text-primary">
{{ option.value }}
</span>
<!-- No value yet — the option's description stands in. -->
<span v-else class="italic text-newt-text-muted">
{{ option.description ?? option.name }}
</span>
</span>
</span>
</template>export { default as SlashCommand } from "./SlashCommand.vue"
export interface SlashCommandOption {
name: string
/** The value the user has entered, if any. */
value?: string
/** Stands in for a missing value, the way the client previews the option. */
description?: string
/** The option the caret currently sits in. */
focused?: boolean
}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 { SlashCommand } from "@/components/ui/slash-command"
export function Example() {
return <SlashCommand />
}<script setup lang="ts">
import { SlashCommand } from "@/components/ui/slash-command"
</script>
<template>
<SlashCommand />
</template>API reference
SlashCommand
name?string
Command name rendered after the leading slash.
stringsubcommand?string | undefined
Subcommand segment, e.g. `/ban member`.
string | undefinedsubcommandGroup?string | undefined
Subcommand group segment, e.g. `/settings roles add`.
string | undefinedoptions?readonly SlashCommandOption[] | undefined
Optional options value.
readonly SlashCommandOption[] | undefinedThe registry exports SlashCommandOption, SlashCommand. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.
Accessibility
Preserve the native semantics of the rendered elements and pass through ARIA attributes when the surrounding context needs a label or description. Any interactive child needs a visible focus indicator, and meaning must not rely on colour alone.
Tokens
Slash Command 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. In the plain HTML and CSS distribution the same component is newt-command.