Skip to content
Block Kit for React
Esc
↑↓navigate↵open⌘Jpreview
On this page

Mentions and resolvers

Resolve user, channel and usergroup ids to names, show unresolved mentions like Slack, and back it with the real Slack Web API.

Slack’s mrkdwn and rich text only ever carry ids (<@U0123ABC>, <#C0123ABC>, <!subteam^S0123>), never names. @nkootstra/block-kit never fetches on your behalf; you supply resolvers and it renders whatever they return, the same way Slack’s own client resolves ids against its cache.

Your AppAPP
assigned this to @engineering in #releases.

The Resolvers shape

Pass resolvers to <BlockKitProvider>. Each function is synchronous: return a name if you have it, or undefined if you don’t (yet):

import { BlockKitProvider, Message, type Resolvers } from "@nkootstra/block-kit";

const resolvers: Resolvers = {
  user: (id) => ({ U0ADA: "Ada Lovelace" })[id],
  channel: (id) => ({ C0RELEASES: "releases" })[id],
  usergroup: (id) => ({ S0ENG: "engineering" })[id],
  userProfile: (id) =>
    id === "U0ADA"
      ? {
          name: "Ada Lovelace",
          title: "Staff Engineer",
          pronouns: "she/her",
          avatarUrl: "https://api.slack.com/img/blocks/bkb_template_images/beagle.png",
          status: { emoji: "palm_tree", text: "On vacation" },
          timeZone: "Europe/Amsterdam",
        }
      : undefined,
};

<BlockKitProvider resolvers={resolvers}>
  <Message blocks={[{ type: "section", text: { type: "mrkdwn", text: "<@U0ADA>" } }]} />
</BlockKitProvider>;
PropType
user?(id: string) => string | undefined

Display name for a user mention, e.g. <@U0ADA> → "Ada Lovelace".

Type(id: string) => string | undefined
userProfile?(id: string) => UserProfile | undefined

Details for the profile card that opens when a user mention is clicked. Falls back to the name from user when omitted.

Type(id: string) => UserProfile | undefined
channel?(id: string) => string | undefined

Name for a channel mention, e.g. <#C0RELEASES> → "releases".

Type(id: string) => string | undefined
usergroup?(id: string) => string | undefined

Name for a usergroup (subteam) mention, e.g. <!subteam^S0ENG> → "engineering".

Type(id: string) => string | undefined

UserProfile fields: name (required), realName, title, pronouns, avatarUrl, status: { emoji?, text? }, and timeZone (an IANA zone, used for the card’s “local time” row).

What unresolved ids look like

An id a resolver returns undefined for renders exactly like Slack’s client does when it hasn’t cached that id yet: not an error, not the raw id:

  • A user mention in mrkdwn or a Home tab/modal’s plain mrkdwn text (<@U0123> with no resolver hit) falls back to plain @U0123 text. Slack doesn’t show a loading pill for it either.
  • A user or usergroup mention in a rich_text block shows an empty loading pill (a small pulsing bar), matching Slack’s Builder while it’s still resolving a mention it hasn’t seen before.
  • A channel mention with no channel resolver hit and no inline label (<#C0123> without |name) renders as a locked “Private channel” pill, since Slack can’t show a name for a channel the viewer isn’t in either.
  • A usergroup mention always shows the loading state until resolved. There’s no unresolved fallback text for subteams, matching Slack.
Your AppAPP
Unresolved user: @U0UNKNOWN
Unresolved channel: Private channel

The profile card

Clicking a resolved user mention opens a popover (Slack’s p-member_profile_card) built from resolvers.userProfile. It shows the avatar (or an initial), name and pronouns, real name and title if they differ from the display name, a custom status with its emoji, and local time when timeZone is set. Without a userProfile resolver it still opens, showing just the name from user.

createWebApiResolvers

@nkootstra/block-kit/web-api builds Resolvers backed by a real @slack/web-api WebClient (or an emulator that implements the same methods): users.info and conversations.info per id, and usergroups.list once for every usergroup id (the Web API has no per-id usergroup lookup).

import { WebClient } from "@slack/web-api";
import { BlockKitProvider, Message } from "@nkootstra/block-kit";
import { createWebApiResolvers } from "@nkootstra/block-kit/web-api";

const client = new WebClient(process.env.SLACK_BOT_TOKEN);
const resolvers = createWebApiResolvers(client, { batchWindowMs: 20 });

<BlockKitProvider resolvers={resolvers}>
  <Message blocks={[{ type: "section", text: { type: "mrkdwn", text: "<@U0ADA>" } }]} />
</BlockKitProvider>;

Because the resolver functions must stay synchronous, a lookup that hasn’t finished yet returns undefined immediately and schedules a background fetch. Calls for the same kind (user or channel) made within batchWindowMs (default 20ms), for example every mention in a long message, are coalesced into one batch of concurrent requests rather than firing one at a time.

useWebApiResolvers

In a component, useWebApiResolvers wraps createWebApiResolvers and re-renders you automatically whenever a lookup completes, so a mention that started unresolved shows its name once it arrives:

import { WebClient } from "@slack/web-api";
import { BlockKitProvider, Message } from "@nkootstra/block-kit";
import { useWebApiResolvers } from "@nkootstra/block-kit/web-api";

const client = new WebClient(process.env.SLACK_BOT_TOKEN);

export function Channel({ blocks }: { blocks: AnyBlock[] }) {
  const resolvers = useWebApiResolvers(client);
  return (
    <BlockKitProvider resolvers={resolvers}>
      <Message blocks={blocks} />
    </BlockKitProvider>
  );
}

client is captured once (like createWebApiResolvers, opts is only read on first render for that client): construct the WebClient outside the component, or memoize it, so it has a stable identity across renders.

Was this page helpful?