Skip to content

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.

Root class newt-command
import { 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-command
npx newtui@latest add slash-command
yarn dlx newtui@latest add slash-command
bunx newtui@latest add slash-command

Copy 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

PropType
name?string

Command name rendered after the leading slash.

Typestring
subcommand?string | undefined

Subcommand segment, e.g. `/ban member`.

Typestring | undefined
subcommandGroup?string | undefined

Subcommand group segment, e.g. `/settings roles add`.

Typestring | undefined
options?readonly SlashCommandOption[] | undefined

Optional options value.

Typereadonly SlashCommandOption[] | undefined

The 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.