Allow tip format functions to return DOM nodes for rich text - #2444
Open
kirthi-b wants to merge 2 commits into
Open
Allow tip format functions to return DOM nodes for rich text#2444kirthi-b wants to merge 2 commits into
kirthi-b wants to merge 2 commits into
Conversation
A custom format function for the title channel or any other channel may now return a DOM node, such as an SVG tspan or anchor element, instead of a string. The returned node is appended directly to the tip rather than being wrapped in a text node, allowing styled and hyperlinked rich text. Plain string formats are unchanged and still rendered as text, so this adds no HTML injection surface. Fixes observablehq#1767
Fil
requested changes
Jul 28, 2026
Fil
left a comment
Contributor
There was a problem hiding this comment.
Thank you for the PR. I'm sorry I haven't found enough time to fully review, but this looks promising. I noticed that the channel markup case is missing a space between the field name+colon and the contents (as you can see in tipFormatChannelMarkup.svg).
|
|
||
| The order and formatting of channels in the tip can be customized with the **format** option <VersionBadge version="0.6.11" pr="1823" />, which accepts a key-value object mapping channel names to formats. Each [format](../features/formats.md) can be a string (for number or time formats), a function that receives the value as input and returns a string, true to use the default format, and null or false to suppress. The order of channels in the tip follows their order in the format object followed by any additional channels. When using the **title** channel, the **format** option may be specified as a string or a function; the given format will then apply to the **title** channel. <VersionBadge version="0.6.15" pr="2074" /> | ||
|
|
||
| A format function may also return a DOM node instead of a string, such as an SVG tspan or anchor element (say generated with [Hypertext Literal](https://github.com/observablehq/htl)); the returned node is appended directly to the tip, allowing styled and hyperlinked rich text. <VersionBadge version="0.6.18" pr="1767" /> For example, to render the **title** channel in bold followed by a link: |
Contributor
There was a problem hiding this comment.
Suggested change
| A format function may also return a DOM node instead of a string, such as an SVG tspan or anchor element (say generated with [Hypertext Literal](https://github.com/observablehq/htl)); the returned node is appended directly to the tip, allowing styled and hyperlinked rich text. <VersionBadge version="0.6.18" pr="1767" /> For example, to render the **title** channel in bold followed by a link: | |
| A format function may also return a DOM node, such as an SVG tspan or anchor element (say generated with [Hypertext Literal](https://github.com/observablehq/htl)); the returned node is appended directly to the tip, allowing styled and hyperlinked rich text. <VersionBadge pr="2444" /> For example, to render the **title** channel in bold followed by a link: |
| }) | ||
| ``` | ||
|
|
||
| Only DOM nodes returned by a format function are treated as rich content; plain strings are always rendered as text, so this does not introduce any HTML injection. Because a returned node is appended as-is rather than measured, the **lineWidth** and **textOverflow** options do not apply to it. |
Contributor
There was a problem hiding this comment.
Suggested change
| Only DOM nodes returned by a format function are treated as rich content; plain strings are always rendered as text, so this does not introduce any HTML injection. Because a returned node is appended as-is rather than measured, the **lineWidth** and **textOverflow** options do not apply to it. | |
| When returning nodes, the **lineWidth** and **textOverflow** options are ignored. |
Comment on lines
+160
to
+163
| if (isNode(lines)) { | ||
| // A format function may return a DOM node (e.g., an SVG tspan | ||
| // or anchor) to display styled or hyperlinked rich text. | ||
| renderLine(that, {value: lines}); |
Contributor
There was a problem hiding this comment.
Suggested change
| if (isNode(lines)) { | |
| // A format function may return a DOM node (e.g., an SVG tspan | |
| // or anchor) to display styled or hyperlinked rich text. | |
| renderLine(that, {value: lines}); | |
| if (lines?.ownerDocument) { | |
| // A format function may return a DOM node (e.g., an SVG tspan | |
| // or anchor) to display styled or hyperlinked rich text. | |
| renderLine(that, {value: lines}); |
Precedent for this simpler test:
Line 364 in 356f579
Also I would like to avoid testing twice if this is a node.
Apply the suggested wording in docs/marks/tip.md and use the ownerDocument test for DOM nodes, matching createTitleElement in plot.js. The node path now adds a space between the channel label and the node, as the string path already did; the channel markup snapshot is updated accordingly. Node rendering moves into its own renderNodeLine so a node value is only tested once rather than again inside renderLine.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the long-standing request for rich text and markup in tips (#1767).
This implements the "relatively easy" direction that mbostock outlined in the thread: a custom format function for the title channel, or any other channel, may now return a DOM node instead of a string. The returned node is appended directly to the tip rather than being wrapped in a text node, so you can style and hyperlink tip content, for example with an SVG tspan or anchor element built with Hypertext Literal:
Safety: only a DOM node returned by a format function is treated as rich content. Plain string formats are unchanged and always rendered as text, so no HTML is ever parsed and this adds no injection surface. Because a returned node is appended as-is rather than measured, the lineWidth and textOverflow options do not apply to it.
Changes:
I scoped this to the format-function approach only, and did not add the separate content channel or the href channel also floated in the thread, since those are larger API surfaces you may want to design yourselves. The VersionBadge version in the docs is a placeholder for you to set to whatever you plan to release.
Fixes #1767