Link pills
EnrichedMarkdownText can present a link as a pill: a rounded box with an optional icon and a label, drawn by the native text stack and wrapped as one unit. A file link becomes a chip with a file icon and a short name, a mention becomes an avatar with the user's name, and the Markdown underneath stays an ordinary link.
Pills are drawn by the native text stack, so they render on iOS and Android only. The web build and macOS render the same links as ordinary links - with the variant's colors and font - and ignore everything on this page. See Web support and macOS support.
Two halves: presentation and content
A pill is configured in two places, on purpose:
- Presentation lives in
markdownStyle.linkVariants[pattern].pill: which links are pills, their colors and geometry, and optionally a label and icon shared by every link the pattern matches. - Per-link content lives in the
linkPillContentprop: a label, icon and tint for one exact URL.
This split keeps markdownStyle a stable constant while the content map changes as links appear, which is what keeps the re-renders cheap.
Enabling pills
A variant turns its links into pills through its pill key. pill: true gives the default look; an object overrides it. The variant's color, underline, backgroundColor and fontFamily still apply, so a pill is styled the way its links already were. Every field of the object, with its default, is listed under Link-specific style properties.
import { EnrichedMarkdownText } from 'react-native-enriched-markdown';
import { useColorScheme } from 'react-native';
import { useMemo } from 'react';
import { defaultMarkdownStyle } from './theme';
const markdown =
'Open [src/components/Button.tsx](https://example.com/files/src/components/Button.tsx) or ask [@gregory](user:gregory).';
export default function App() {
const isDark = useColorScheme() === 'dark';
const markdownStyle = useMemo(
() => ({
...defaultMarkdownStyle(isDark),
linkVariants: {
// Colors come from the variant; geometry and a shared icon from `pill`.
'^https://example\\.com/files/': {
color: '#3730A3',
backgroundColor: '#EEF2FF',
underline: false,
pill: {
iconUri: 'file_icon',
borderRadius: 10,
paddingHorizontal: 7,
paddingVertical: 2,
borderWidth: 1,
borderColor: '#C7D2FE',
maxWidth: 220,
},
},
// The default look needs nothing more than `pill: true`.
'^user:': {
color: '#065F46',
backgroundColor: '#ECFDF5',
underline: false,
pill: true,
},
},
}),
[isDark],
);
return (
<EnrichedMarkdownText markdown={markdown} markdownStyle={markdownStyle} />
);
}
Per-link content
linkPillContent maps an exact link URL to { label?, iconUri?, iconTintColor? }. It only affects links whose variant enables pill. For both label and icon the order is: the linkPillContent entry, then the variant's pill.label / pill.iconUri, then the link's own text (and no icon).
import { EnrichedMarkdownText } from 'react-native-enriched-markdown';
import { useColorScheme } from 'react-native';
import { useMemo } from 'react';
import { defaultMarkdownStyle } from './theme';
const markdown =
'Open [src/components/Button.tsx](https://example.com/files/src/components/Button.tsx) or ask [@gregory](user:gregory).';
// Content, not style: this map can change on every render while
// `markdownStyle` stays a stable object.
const content = {
'https://example.com/files/src/components/Button.tsx': {
label: 'Button.tsx',
},
'user:gregory': {
iconUri: 'https://example.com/avatars/gregory.png',
},
};
export default function App() {
const isDark = useColorScheme() === 'dark';
const markdownStyle = useMemo(
() => ({
...defaultMarkdownStyle(isDark),
linkVariants: {
'^https://example\\.com/files/': {
color: '#3730A3',
backgroundColor: '#EEF2FF',
underline: false,
pill: { iconUri: 'file_icon', borderRadius: 10 },
},
'^user:': {
color: '#065F46',
backgroundColor: '#ECFDF5',
underline: false,
pill: true,
},
},
}),
[isDark],
);
return (
<EnrichedMarkdownText
markdown={markdown}
markdownStyle={markdownStyle}
linkPillContent={content}
/>
);
}
The variant's pill.iconTintColor tints the variant's own icon only. An icon set per link, such as an avatar, keeps its colors unless its entry has an iconTintColor of its own. An entry with only iconTintColor recolors the variant's icon for that one link.
Lookup is by exact URL, so it stays cheap with hundreds of links, and changing the map re-renders without touching markdownStyle. Keep the object reference stable between renders when its content has not changed.
Line height
A pill is as tall as its font's line plus paddingVertical and borderWidth on both sides. A line grows to fit a pill, but only to the pill's own height, so pills on consecutive lines end up with no space between them when the line height leaves no room - touching on iOS, a hairline apart on Android, where the line box is measured in whole pixels. pill.lineHeight is for that case: a paragraph, list item, heading or quote that holds a pill gets lines at least that tall, on every line, so its spacing stays even. It only ever raises the block's own lineHeight; it does not size the pill, and blocks without pills are unaffected.
For text that streams in, set the block's own lineHeight to that value instead. With pill.lineHeight a paragraph's line height depends on whether it holds a pill, so while streaming it changes the moment a link completes and becomes one, and the text around it moves. A block lineHeight is the same before and after, so nothing moves.
Behavior
- The label uses the font of the surrounding text, including bold and italic applied around the link. Long labels truncate at the tail; the pill never grows past the available text width.
- The label is presentation only. Plain copy, Copy as Markdown, HTML and RTF export, and the iOS system actions (Look Up, Translate, Share) receive the original link text.
onLinkPressandonLinkLongPressreceive the original URL. - The accessible name is the visible label, followed by the original link text when they differ.
- Pills work everywhere links render, including GitHub-flavor table cells and blockquotes.
- Links whose text contains an image, inline math, a hard line break or a spoiler stay ordinary links.
- A pill inside an unrevealed spoiler is hidden with the rest of the spoiler.
- A long-pressed pill can open a link menu on iOS 17+; the menu is titled with the pill's label.
Icon sources
Both pill.iconUri and a linkPillContent entry's iconUri take the same sources. Icons are drawn at the label's font size.
| Source | iOS | Android |
|---|---|---|
File path, file:// URI, bundled asset name | Yes | Yes |
http://, https:// (including Image.resolveAssetSource(require(...)).uri in dev) | Yes | Yes |
data: (base64) | Yes | Yes |
Drawable/raw resource name, asset://, res://, content:// | No | Yes |
Local files show on the first layout. Remote icons load in the background through the same pipeline as Markdown images, using imageRequestHeaders, and the pill keeps room for them while they load. A source that fails to load gives that room back, so the pill shrinks to its label, and is retried after 30 seconds.
Reference
- Link-specific style properties -
linkVariantsand every field of thepillkey. linkPillContent- the per-link content prop.- Mentions - the mention links that pills are most often used for.
- Link menus - long-press menus for links, which pick up a pill's label as their title.