Message Rendering Pipeline#
Element Web processes message content (m.text, m.emote, m.notice) through a multi-stage sanitization and transformation pipeline before display. The main stages are:
- Sanitization — HTML is cleaned through
sanitize-htmlusing a strict allowlist - Tag transformation —
mxc://URLs are resolved, inline images capped, and colors converted - Post-render DOM work — linkification, pills, spoilers, and syntax highlighting applied after mount
TextualBody.render()
└── HtmlUtils.bodyToDiv / bodyToSpan
└── bodyToNode → analyseEvent
├── sanitizeHtml(formattedBody, sanitizeHtmlParams)
│ └── transformTags (img, a, code, *)
└── formatEmojis (after sanitization)
└── TextualBody.applyFormatting() [componentDidMount]
├── activateSpoilers
├── linkifyElement
├── pillifyLinks
├── calculateUrlPreview
├── tooltipifyLinks
└── highlightCode (async)
Key source files:
| File | Role |
|---|---|
src/HtmlUtils.tsx | bodyToDiv, bodyToSpan, bodyToNode, analyseEvent, formatEmojis |
src/Linkify.tsx | sanitizeHtmlParams, transformTags, linkifyAndSanitizeHtml |
src/components/views/messages/TextualBody.tsx | Top-level render component; drives post-mount DOM processing |
src/customisations/Media.ts | Media class, mediaFromMxc, getThumbnailOfSourceHttp |
HTML Sanitization#
The canonical sanitization configuration is sanitizeHtmlParams in Linkify.tsx, used whenever a formatted_body with format: "org.matrix.custom.html" is present .
Allowed tags follow the Matrix spec : font, del, h1–h6, blockquote, p, a, ul, ol, li, b, i, u, strong, em, strike, code, hr, br, div, table, thead, tbody, tr, th, td, pre, span, img, details, summary. Tags not in this list (script, iframe, style, object, etc.) are stripped entirely.
Allowed attributes are specified per tag :
font:color,data-mx-bg-color,data-mx-color,stylespan:data-mx-maths,data-mx-bg-color,data-mx-color,data-mx-spoiler,stylea:href,name,target,relimg:src,alt,title,style
Allowed URL schemes come from PERMITTED_URL_SCHEMES — an allowlist of ~25 schemes including http, https, matrix, mailto, irc, ssh, tel, etc. Protocol-relative URLs are blocked (allowProtocolRelative: false) . Nesting is capped at 50 levels deep .
Variant configs :
composerSanitizeHtmlParams— same allowedTags/attrs but only thecodeand*transformTags, used when quoting into the composertopicSanitizeHtmlParams— stripped-down tag set (no headings, tables, or images) for room topics
Tag Transformation & mxc:// URL Conversion#
transformTags runs before attribute filtering during sanitization. It has four handlers:
"img" — inline image processing#
This is the most security-sensitive transform :
- If the
showImagesaccount setting isfalse, the image is dropped entirely (empty attribs returned) . - If
srcdoesn't already start withmxc://,MEDIA_API_MXC_REGEXtries to convert a Matrix Media API HTTP URL (/_matrix/media/r0/(download|thumbnail)/{server}/{id}) back tomxc://form . - If
srcis still notmxc://, the image is dropped — no external HTTP image sources are permitted . - Width/height are capped at 800×600 px and written as
max-width/max-heightCSS . - The final
srcis resolved viamediaFromMxc(src).getThumbnailOfSourceHttp(width, height), which callsMedia.getThumbnailOfSourceHttp— scaling bywindow.devicePixelRatioand callingclient.mxcUrlToHttp(mxc, w, h, "scale")to produce a/media/v3/thumbnail/…HTTP URL.
"a" — external link handling#
Adds target="_blank" to all links by default, then removes it for Matrix permalink hosts (e.g. matrix.to) and Element domain links. Always adds rel="noreferrer noopener" .
"code" — class filtering#
Strips all classes except those matching language-* (for syntax highlighting), and excludes language-_* .
"*" — color attributes#
Deletes any pre-existing style attribute (except on img). Converts data-mx-color and data-mx-bg-color to inline CSS color / background-color, validating values against #[0-9a-fA-F]{6} .
Emoji & Big Emoji#
Emoji rendering runs after HTML sanitization in bodyToNode.
EMOJI_REGEX uses Unicode v-mode \p{RGI_Emoji} sequences (with a safe fallback to /(?!)/ on unsupported platforms). formatEmojis() segments the message string via graphemeSegmenter and wraps each emoji in a <span class="mx_Emoji" title=":shortcode:">. Two output modes exist: HTML strings (for dangerouslySetInnerHTML paths) and JSX elements (for plain text paths).
Big emoji: if the entire trimmed message body (after stripping zero-width joiners and spaces) matches BIGEMOJI_REGEX and the message contains no HTTP links, the body gets the mx_EventTile_bigEmoji CSS class for oversized display . This is suppressed for emote messages or if the TextualBody.enableBigEmoji setting is off .
LaTeX: if feature_latex_maths is enabled, data-mx-maths attributes in <div> and <span> are rendered via katex.renderToString before emoji processing .
Custom Emoticons (MSC2545)#
MSC2545 defines im.ponies.room_emotes / m.room.image_pack room state events for custom image emoticon packs. Element has basic receiving support: it can render inline custom emoticons sent by other clients (Cinny, FluffyChat, Nheko).
The rendering goes through the same img transform path in transformTags — the emoticon's mxc:// URL is resolved to a thumbnail HTTP URL via getThumbnailOfSourceHttp. No dedicated data-mx-emoticon attribute handling exists in matrix-react-sdk; the standard <img> sanitization pipeline handles display.
Known limitation: animated GIF emoticons render as a static single frame. The root cause is the thumbnail-fallback behavior in getThumbnailOfSourceHttp (which requests a scaled/static thumbnail instead of the original animated file), the same issue as regular pasted GIFs (#32311). This is tracked in element-web issue #34339.
Post-Sanitization DOM Processing#
After render() mounts the sanitized HTML, applyFormatting() runs a series of DOM passes in order:
activateSpoilers— walks the DOM for[data-mx-spoiler]spans and replaces them with<Spoiler>React components (rendered viaReactDOM.renderinto placeholder nodes).linkifyElement— linkifies plain-text URLs in the DOM (wraps them in<a>elements) usinglinkify-reactwith Matrix-specific options fromlinkify-matrix.pillifyLinks— converts Matrix mention links into interactive user/room pill components.calculateUrlPreview— collects previewable<a>href values from the DOM (deduplicated viaSet, skipping<pre>/<code>/<blockquote>descendants and Matrix permalink hosts) and stores them in state to triggerLinkPreviewGrouprendering.tooltipifyLinks— adds tooltip overlays to pill links; runs aftercalculateUrlPreviewso it doesn't interfere with anchor discovery.- Code block enhancement (HTML messages only, ): wraps
<pre>elements in a container div, adds expand/collapse and copy buttons, and asynchronously invokeshighlight.jsfor syntax highlighting. Highlight is skipped for blocks > 4096 characters .