Skip to content

Mention Chip

An inline chip for user, channel, role and app mentions, with an optional avatar.

An inline chip for user, channel, role and app mentions, with an optional avatar. 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-mention
import { Mention } from "@/registry/bases/newt/ui/mention"

/* Inline so the demo never reaches out to the network for a placeholder. */
const AVATAR =
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='10' fill='%235865f2'/%3E%3C/svg%3E"

export default function MentionDemo() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Mention variant="user">@username</Mention>
      <Mention variant="channel">#channel</Mention>
      <Mention variant="role" color="#f0b232">
        @Moderator
      </Mention>
      <Mention variant="user" avatar={AVATAR}>
        @with-avatar
      </Mention>
    </div>
  )
}
<script setup lang="ts">
import { Mention } from "~~/registry/bases/newt/ui/mention"

/* Inline so the demo never reaches out to the network for a placeholder. */
const AVATAR =
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='10' fill='%235865f2'/%3E%3C/svg%3E"
</script>

<template>
  <div class="flex flex-wrap items-center gap-2">
    <Mention variant="user">@username</Mention>
    <Mention variant="channel">#channel</Mention>
    <Mention variant="role" color="#f0b232">@Moderator</Mention>
    <Mention variant="user" :avatar="AVATAR">@with-avatar</Mention>
  </div>
</template>

Installation

pnpm dlx newtui@latest add mention
npx newtui@latest add mention
yarn dlx newtui@latest add mention
bunx newtui@latest add mention

Install the dependencies

pnpm dlx --yes class-variance-authority
npx --yes class-variance-authority
yarn dlx --yes class-variance-authority
bunx --yes class-variance-authority

Copy the source into your project

import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"

import { cn } from "@/lib/utils"

/*
 * Every variant only sets two custom properties: the tint the chip renders at
 * rest and the solid colour it fills with on hover. A role mention overrides
 * `--newt-mention-color` inline with the role's own hue, and its hover fill
 * follows automatically.
 */
const mentionVariants = cva(
  "inline-flex min-h-[22px] cursor-pointer items-center gap-0.5 rounded-sm border-0 bg-[color-mix(in_srgb,var(--newt-mention-color)_15%,transparent)] px-1 align-middle font-sans text-[14px] font-medium leading-5 text-[var(--newt-mention-color)] transition-colors duration-fast ease-newt hover:bg-[var(--newt-mention-solid)] hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link",
  {
    variants: {
      variant: {
        user: "[--newt-mention-color:var(--newt-mention-text)] [--newt-mention-solid:var(--newt-brand)]",
        channel:
          "[--newt-mention-color:var(--newt-text-link)] [--newt-mention-solid:var(--newt-brand)]",
        role: "[--newt-mention-color:var(--newt-mention-role)] [--newt-mention-solid:var(--newt-mention-color)]",
        app: "[--newt-mention-color:var(--newt-mention-text)] [--newt-mention-solid:var(--newt-brand)]",
      },
    },
    defaultVariants: {
      variant: "user",
    },
  }
)

export interface MentionProps
  extends
    Omit<React.ComponentProps<"button">, "color">,
    VariantProps<typeof mentionVariants> {
  /** Avatar URL. Rendered from 48rem up; below that the chip stays text-only. */
  avatar?: string
  /** Role colour, for `variant="role"`. Any CSS colour. */
  color?: string
}

const Mention = React.forwardRef<HTMLButtonElement, MentionProps>(
  ({ className, variant, avatar, color, children, style, ...props }, ref) => (
    <button
      ref={ref}
      type="button"
      data-variant={variant ?? "user"}
      /*
       * SAFETY: `CSSProperties` has no index signature for custom properties,
       * but React forwards unknown `--*` keys to the style attribute verbatim,
       * so the extra key is written out exactly as spelled here.
       */
      style={
        color
          ? ({ ...style, "--newt-mention-color": color } as React.CSSProperties)
          : style
      }
      className={cn(
        mentionVariants({ variant }),
        avatar && "ps-0.5",
        className
      )}
      {...props}
    >
      {avatar ? (
        <img
          src={avatar}
          alt=""
          aria-hidden="true"
          className="hidden h-5 w-5 shrink-0 rounded-full object-cover md:block"
        />
      ) : null}
      {children}
    </button>
  )
)
Mention.displayName = "Mention"

export { Mention, mentionVariants }
<script setup lang="ts">
import type { CSSProperties, HTMLAttributes } from "vue"
import { computed } from "vue"

import { cn } from "@/lib/utils"

import { mentionVariants, type MentionVariants } from "."

const props = defineProps<{
  class?: HTMLAttributes["class"]
  variant?: MentionVariants["variant"]
  /** Avatar URL. Rendered from 48rem up; below that the chip stays text-only. */
  avatar?: string
  /** Role colour, for `variant="role"`. Any CSS colour. */
  color?: string
}>()

/*
 * SAFETY: `CSSProperties` has no index signature for custom properties, but
 * Vue writes unknown `--*` keys to the style attribute verbatim, so the extra
 * key lands exactly as spelled here.
 */
const style = computed<CSSProperties | undefined>(() =>
  props.color
    ? ({ "--newt-mention-color": props.color } as CSSProperties)
    : undefined
)
</script>

<template>
  <button
    type="button"
    :data-variant="props.variant ?? 'user'"
    :style="style"
    :class="
      cn(
        mentionVariants({ variant: props.variant }),
        props.avatar && 'ps-0.5',
        props.class
      )
    "
  >
    <img
      v-if="props.avatar"
      :src="props.avatar"
      alt=""
      aria-hidden="true"
      class="hidden h-5 w-5 shrink-0 rounded-full object-cover md:block"
    />
    <slot />
  </button>
</template>
import { cva, type VariantProps } from "class-variance-authority"

export { default as Mention } from "./Mention.vue"

/*
 * Every variant only sets two custom properties: the tint the chip renders at
 * rest and the solid colour it fills with on hover. A role mention overrides
 * `--newt-mention-color` inline with the role's own hue, and its hover fill
 * follows automatically.
 */
export const mentionVariants = cva(
  "inline-flex min-h-[22px] cursor-pointer items-center gap-0.5 rounded-sm border-0 bg-[color-mix(in_srgb,var(--newt-mention-color)_15%,transparent)] px-1 align-middle font-sans text-[14px] font-medium leading-5 text-[var(--newt-mention-color)] transition-colors duration-fast ease-newt hover:bg-[var(--newt-mention-solid)] hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-newt-text-link",
  {
    variants: {
      variant: {
        user: "[--newt-mention-color:var(--newt-mention-text)] [--newt-mention-solid:var(--newt-brand)]",
        channel:
          "[--newt-mention-color:var(--newt-text-link)] [--newt-mention-solid:var(--newt-brand)]",
        role: "[--newt-mention-color:var(--newt-mention-role)] [--newt-mention-solid:var(--newt-mention-color)]",
        app: "[--newt-mention-color:var(--newt-mention-text)] [--newt-mention-solid:var(--newt-brand)]",
      },
    },
    defaultVariants: {
      variant: "user",
    },
  }
)

export type MentionVariants = VariantProps<typeof mentionVariants>

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 { Mention } from "@/components/ui/mention"

export function Example() {
  return <Mention />
}
<script setup lang="ts">
import { Mention } from "@/components/ui/mention"
</script>

<template>
  <Mention />
</template>

API reference

Mention

PropType
avatar?string | undefined

Avatar URL. Rendered from 48rem up; below that the chip stays text-only.

Typestring | undefined
color?string | undefined

Role colour, for `variant="role"`. Any CSS colour.

Typestring | undefined

The registry exports Mention. 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

Mention Chip 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-mention.