Aura Design System

Mention

An accessible text input that suggests and inserts items after a trigger character.

Preview

Loading...
import * as React from "react";
import {
  Mention,
  MentionContent,
  MentionInput,
  MentionItem,
  MentionLabel,
  MentionPortal,
} from "@/components/ui/Mention";
import { Textarea } from "@/components/ui/Textarea";

export function MentionDemo() {
  return {
  return (
    <Mention className="smosh">
      <MentionLabel>Mention users</MentionLabel>
      <MentionInput placeholder="Type @ to mention someone…" asChild>
        <Textarea />
      </MentionInput>
      <MentionPortal>
        <MentionContent>
          {users.map((user) => (
            <MentionItem
              key={user.id}
              value={user.name}
              className="flex-col items-start"
            >
              <span className="text-sm">{user.name}</span>
              <span className="text-xs text-gray-11">{user.email}</span>
            </MentionItem>
          ))}
        </MentionContent>
      </MentionPortal>
    </Mention>
  );
};

Installation

Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces

pnpm dlx shadcn@latest add @aura/mention

Manual

Install the following dependencies:

pnpm install @diceui/mention

Copy and paste the class names utility into your utils/class-names.ts file.

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Copy and paste the Mention component into your components/ui/Mention.tsx file.

"use client";
/**
 * @description Adds accessible, trigger-based mentions to a text input.
 */
import * as React from "react";
import * as MentionPrimitive from "@diceui/mention";

import { cn } from "@/utils/class-names";

function Mention({
  className,
  ...props
}: React.ComponentProps<typeof MentionPrimitive.Root>) {
  return (
    <MentionPrimitive.Root
      data-slot="mention"
      className={cn(
        "**:data-tag:rounded-sm **:data-tag:bg-accent-3 **:data-tag:px-0.5 **:data-tag:text-accent-12",
        className,
      )}
      {...props}
    />
  );
}

function MentionLabel({
  className,
  ...props
}: React.ComponentProps<typeof MentionPrimitive.Label>) {
  return (
    <MentionPrimitive.Label
      data-slot="mention-label"
      className={cn(
        "mb-0.5 block text-sm font-semibold text-gray-12 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
        className,
      )}
      {...props}
    />
  );
}

function MentionInput({
  className,
  ...props
}: React.ComponentProps<typeof MentionPrimitive.Input>) {
  return (
    <MentionPrimitive.Input
      data-slot="mention-input"
      className={cn(
        "flex min-h-4 w-full rounded-md border border-gray-7 bg-gray-1 px-1 py-0.5 text-gray-12 placeholder:text-gray-11 focus-visible:border-gray-8 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-8 disabled:cursor-not-allowed disabled:opacity-50",
        className,
      )}
      {...props}
    />
  );
}

function MentionPortal({
  ...props
}: React.ComponentProps<typeof MentionPrimitive.Portal>) {
  return <MentionPrimitive.Portal data-slot="mention-portal" {...props} />;
}

function MentionContent({
  className,
  ...props
}: React.ComponentProps<typeof MentionPrimitive.Content>) {
  return (
    <MentionPrimitive.Content
      data-slot="mention-content"
      className={cn(
        "z-50 min-w-(--dice-anchor-width) origin-(--dice-transform-origin) overflow-hidden rounded-md border border-gray-7 bg-gray-1 p-0.5 text-gray-12 shadow-md data-[state=closed]:animate-mention-hide data-[state=open]:animate-mention-show",
        className,
      )}
      {...props}
    />
  );
}

function MentionItem({
  className,
  ...props
}: React.ComponentProps<typeof MentionPrimitive.Item>) {
  return (
    <MentionPrimitive.Item
      data-slot="mention-item"
      className={cn(
        "relative flex w-full cursor-default select-none items-center gap-0.5 rounded-sm px-1 py-0.5 text-sm outline-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent-3 data-highlighted:text-accent-12",
        className,
      )}
      {...props}
    />
  );
}

export {
  Mention,
  MentionContent,
  MentionInput,
  MentionItem,
  MentionLabel,
  MentionPortal,
};

Usage

CustomTrigger

export const CustomTrigger = () => {
  return (
    <Mention trigger="#" className="smosh">
      <MentionLabel>Mention a topic</MentionLabel>
      <MentionInput placeholder="Type # to mention a topic…" asChild>
        <Textarea />
      </MentionInput>
      <MentionPortal>
        <MentionContent>
          {users.map((user) => (
            <MentionItem key={user.id} value={user.name}>
              {user.name}
            </MentionItem>
          ))}
        </MentionContent>
      </MentionPortal>
    </Mention>
  );
};

CustomFilter

export const CustomFilter = () => {
  const [value, setValue] = React.useState<string[]>([]);
  const [inputValue, setInputValue] = React.useState("");

  const filterCommands = React.useCallback(
    (options: string[], term: string) =>
      options.filter((option) =>
        option.toLowerCase().startsWith(term.toLowerCase()),
      ),
    [],
  );

  return (
    <Mention
      className="smosh"
      inputValue={inputValue}
      onFilter={filterCommands}
      onInputValueChange={setInputValue}
      onValueChange={setValue}
      trigger="/"
      value={value}
    >
      <MentionLabel>Run a command</MentionLabel>
      <MentionInput placeholder="Type / to use a command…" asChild>
        <Textarea />
      </MentionInput>
      <MentionPortal>
        <MentionContent>
          {commands.map((command) => (
            <MentionItem
              key={command.name}
              label={command.name}
              value={command.name}
              className="flex-col items-start"
            >
              <span className="font-mono text-sm">{command.name}</span>
              <span className="text-xs text-gray-11">
                {command.description}
              </span>
            </MentionItem>
          ))}
        </MentionContent>
      </MentionPortal>
    </Mention>
  );
};