npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@m1z23r/ngx-mentions

v0.1.3

Published

A signals-first Angular mentions editor with atomic pills, multiple triggers, async sources and template variables

Readme

@m1z23r/ngx-mentions

A signals-first Angular mentions editor. It renders atomic, non-editable pills inside a contenteditable surface, supports any number of triggers at once (including multi-character ones like {{ for template variables), and can back each trigger with a static array or an async source. Custom item templates are supported per trigger or globally. The library has zero runtime dependencies beyond Angular itself, uses no rxjs, and is zoneless-compatible.

Install

npm i @m1z23r/ngx-mentions

Peer dependencies: @angular/common and @angular/core at ^21.0.0.

Quick start

import { Component, signal } from '@angular/core';
import { IMentionItem, IMentionTriggerConfig, NgxMentionsComponent } from '@m1z23r/ngx-mentions';

@Component({
  selector: 'app-comment-box',
  standalone: true,
  imports: [NgxMentionsComponent],
  template: `<ngx-mentions [(value)]="body" [triggers]="triggers" placeholder="Say something..." />`,
})
export class CommentBox {
  protected readonly body = signal('');

  private readonly users: IMentionItem[] = [
    { id: 'u1', label: 'John Doe' },
    { id: 'u2', label: 'Jane Smith' },
  ];

  protected readonly triggers: IMentionTriggerConfig[] = [{ trigger: '@', source: this.users }];
}

Typing Hey @ opens a dropdown filtered by what follows. Picking "John Doe" inserts an atomic pill and body() becomes the markup string:

Hey @[John Doe](u1)!

The markup is what you persist. Use parseMentions or mentionsToPlainText (documented below) to turn it back into structured segments or a human-readable string wherever you render it.

Trigger configuration

Each entry in the triggers input is an IMentionTriggerConfig:

| Field | Type | Default | Description | | --- | --- | --- | --- | | trigger | string | required | The character(s) that open the dropdown, e.g. @, #, {{. Must be unique across configs. | | suffix | string | none | Closes a multi-char trigger, e.g. }}. When set, the item is serialized as trigger + id + suffix instead of the default markdown-like format. | | source | IMentionItem[] \| (query: string) => IMentionItem[] \| Promise<IMentionItem[]> | required | A static list (filtered client-side by label) or a function returning results for a query, sync or async. | | displayWith | (item: IMentionItem) => string | item.label | Text rendered inside the pill. | | serialize | (item: IMentionItem) => string | @[label](id) or trigger+id+suffix | How a chosen item is written into the stored value string. | | pattern | RegExp | derived from trigger/suffix | Regex used to find and parse mentions back out of a stored value. | | deserialize | (match: RegExpExecArray) => IMentionItem | derived | Builds an IMentionItem from a pattern match. | | pillClass | string | none | Extra CSS class applied to pills produced by this trigger, for per-trigger styling. | | minChars | number | 0 | Minimum characters typed after the trigger before search runs. | | allowSpaces | boolean | false | Whether the query may contain spaces before the trigger session is cancelled. | | debounceMs | number | 150 | Debounce applied before calling an async source. |

Template variables

Use a suffix to support multi-character wrappers such as {{ }} template variables:

const variableTrigger: IMentionTriggerConfig = {
  trigger: '{{',
  suffix: '}}',
  source: [
    { id: 'firstName', label: 'First Name' },
    { id: 'lastName', label: 'Last Name' },
  ],
};

Picking "First Name" serializes as {{firstName}} instead of the default @[label](id) shape, which keeps the stored value compatible with plain template engines.

Async sources

Pass a function instead of an array to fetch results as the user types:

const userTrigger: IMentionTriggerConfig = {
  trigger: '@',
  debounceMs: 200,
  source: (query: string): Promise<IMentionItem[]> =>
    fetch(`/api/users?q=${encodeURIComponent(query)}`).then((res) => res.json()),
};

The dropdown shows a built-in loading indicator (three animated dots) while the promise is pending. Requests are debounced by debounceMs, and results from a stale request (superseded by newer keystrokes) are discarded automatically, so out-of-order responses never overwrite the latest search.

Custom serialization

serialize, pattern, and deserialize work together and should be overridden as a set so parsing round-trips correctly:

const ticketTrigger: IMentionTriggerConfig = {
  trigger: '#',
  source: tickets,
  serialize: (item) => `#TICKET-${item.id}`,
  pattern: /#TICKET-(\w+)/g,
  deserialize: (match) => tickets.find((t) => t.id === match[1]) ?? { id: match[1], label: match[1] },
};

Component API

<ngx-mentions> (NgxMentionsComponent):

| Member | Kind | Type | Description | | --- | --- | --- | --- | | value | input/output (model) | string | Two-way bound markup string, e.g. [(value)]="body". | | triggers | input (required) | IMentionTriggerConfig[] | The list of triggers the editor listens for. | | placeholder | input | string | Placeholder text shown when empty. | | disabled | input | boolean | Disables editing and closes any open dropdown. | | multiline | input | boolean | When false, Enter emits submitted instead of inserting a line break. | | segments | signal (readonly) | MentionSegment[] | value parsed into text and mention segments. | | plainText | signal (readonly) | string | value rendered as human-readable text (pills as their display label). | | mentionAdded | output | IMentionSegment | Emitted when a pill is inserted, by picking or auto-converting a suffix trigger. | | mentionRemoved | output | IMentionSegment | Emitted when a pill is deleted. | | submitted | output | void | Emitted on Enter when multiline is false. | | insertMention(item) | method | (item: IMentionItem) => void | Programmatically insert an item into the currently open trigger session. |

Custom item templates

Provide an ng-template with ngxMentionItem to override how dropdown items render. Give it a trigger string to scope it to that trigger only, or leave it empty (or omit the value) to use it as the fallback for every trigger that has no dedicated template:

<ngx-mentions [(value)]="body" [triggers]="triggers">
  <ng-template ngxMentionItem="@" let-item let-query="query" let-trigger="trigger">
    <img [src]="item.data.avatar" alt="" />
    <span>{{ item.label }}</span>
  </ng-template>
</ngx-mentions>

The template context is IMentionItemTemplateContext: $implicit (the IMentionItem), query (current search text), and trigger (the trigger string that opened the dropdown).

Helpers

parseMentions(value, triggers) turns a stored markup string into MentionSegment[] (a union of ITextSegment and IMentionSegment). mentionsToPlainText(value, triggers) renders the same value as a plain string using each trigger's displayWith. Both are useful for rendering a previously stored message outside of the editor:

import { mentionsToPlainText, parseMentions } from '@m1z23r/ngx-mentions';

const stored = 'Hey @[John Doe](u1), welcome to {{company}}!';
const segments = parseMentions(stored, triggers);
const readable = mentionsToPlainText(stored, triggers);
// readable === 'Hey John Doe, welcome to Company!'

segments can be iterated with @for in a template to render text and mention chips with your own markup, without instantiating the editor at all.

Theming

All visual styling is controlled by CSS custom properties, so no ::ng-deep or SCSS overrides are needed:

| Variable | Fallback | | --- | --- | | --ngx-mentions-editor-color | inherit | | --ngx-mentions-editor-bg | #fff | | --ngx-mentions-editor-border | #d1d5db | | --ngx-mentions-editor-radius | 6px | | --ngx-mentions-editor-padding | 0.5em 0.75em | | --ngx-mentions-editor-min-height | 2.5em | | --ngx-mentions-focus-border | #6366f1 | | --ngx-mentions-focus-ring-width | 2px | | --ngx-mentions-focus-ring-color | rgba(99, 102, 241, 0.25) | | --ngx-mentions-placeholder-color | #9ca3af | | --ngx-mentions-disabled-opacity | 0.6 | | --ngx-mentions-editor-disabled-bg | #f3f4f6 | | --ngx-mentions-pill-padding | 0 0.25em | | --ngx-mentions-pill-radius | 4px | | --ngx-mentions-pill-bg | #e0e7ff | | --ngx-mentions-pill-color | #3730a3 | | --ngx-mentions-dropdown-z | 1000 | | --ngx-mentions-dropdown-min-width | 180px | | --ngx-mentions-dropdown-max-width | 320px | | --ngx-mentions-dropdown-max-height | 240px | | --ngx-mentions-dropdown-bg | #fff | | --ngx-mentions-dropdown-border | #e5e7eb | | --ngx-mentions-dropdown-radius | 8px | | --ngx-mentions-dropdown-shadow | 0 4px 12px rgba(0, 0, 0, 0.1) | | --ngx-mentions-item-padding | 6px 10px | | --ngx-mentions-item-active-bg | #eef2ff | | --ngx-mentions-loading-gap | 4px | | --ngx-mentions-loading-color | #9ca3af | | --ngx-mentions-loading-dot-size | 5px |

.my-scope {
  --ngx-mentions-pill-bg: #dcfce7;
  --ngx-mentions-pill-color: #166534;
  --ngx-mentions-focus-border: #22c55e;
}

For per-trigger pill colors (rather than a global override), set pillClass on a trigger config and style that class instead.

Known constraints

  • With the default @[label](id) format, item id values should avoid ).
  • With a suffix-based format (e.g. {{ }}), item id values should match [\w.-]+.
  • Two trigger configs must not share the same trigger string.
  • Undo/redo across a programmatically inserted pill (via insertMention or suffix auto-conversion) is best-effort; the browser's native undo stack does not always track DOM mutations made outside of direct typing.

Development

yarn install
yarn start        # serve the demo app
yarn build:lib    # production build of the library into dist/ngx-mentions

To publish a new version:

yarn bv             # bump patch version in root and library package.json
yarn build:lib
yarn publish:lib

License

MIT