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.
newt-mentionimport { 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 mentionnpx newtui@latest add mentionyarn dlx newtui@latest add mentionbunx newtui@latest add mentionInstall the dependencies
pnpm dlx --yes class-variance-authoritynpx --yes class-variance-authorityyarn dlx --yes class-variance-authoritybunx --yes class-variance-authorityCopy 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
avatar?string | undefined
Avatar URL. Rendered from 48rem up; below that the chip stays text-only.
string | undefinedcolor?string | undefined
Role colour, for `variant="role"`. Any CSS colour.
string | undefinedThe 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.