WebTT
WebTT (Web Tooltips) are nestable, cross-domain tooltips without any server-side logic. Hover over these three words for an example tooltip, hosted at https://willowprotocol.org/.
- No server-side logic required, tooltips can be static HTML.
- Tooltips seamlessly work across domains.
- Tooltips are isolated in iframes, no need trust tooltip authors.
- Tooltips can nest arbitrarily — and even recursively.
Quickstart
To add tooltips to an HTML page, follow these steps:
- Include a javascript file implementing the client-side logic of the specification. Example:
<script src="https://codeberg.org/worm-blossom/webtt/raw/branch/main/webtt.js" defer> - Include some minimal styling for rendering tooltips. Example:
.tooltipContainer { position: absolute; box-shadow: 5px 5px 10px 0px rgba(68, 68, 68, 0.80); } - Add a tooltip to any HTML element by giving it a
data-tooltip-anchorattribute pointing to a tooltip page. Example:<span> data-tooltip-anchor="https://willowprotocol.org/previews/Path.html">hover for tooltip</span>
How it Works
To endow your website with WebTT tooltips, three components are at play.
First, the tooltip definitions. These are arbitrary self-contained HTML pages, to be fetched and displayed in an iframe by anyone who wants to render the tooltip. The iframes displaying tooltip pages are sandboxed with the sandbox="allow-scripts allow-same-origin attributes.
Second, references to tooltips. Any html element can reference up to one tooltip, by adding a data-tooltip-anchor=<tooltip URL> attribute, where <tooltip URL> points to a tooltip definition.
Finally, you need some client-side javascript to display the tooltips. We provide a single javascript file, to be included both on pages containing references to tooltips and on the tooltip definition pages themselves. See the typescript source code here, or directly use the precompiled and minified javscript.
This javascript implementation is just one specific way of handling the UI concerns around nested tooltips. To write an independent-but-compatible implementation, see the specification of required behaviour. That specification does not prescribe details of how to display tooltips or when to create or hide them, but only the required communication flow between the tooltip page and the containing page.
Styling for Our Implementation
The following information pertains not to the WebTT specification in general, but is specific to our implementation.
We place tooltip iframes as the sole children of divs of class tooltipContainer. Such divs should be styled to be positioned absolutely (.tooltipContainer { position: absolute; }), because we position them by adding dynamically computed left and top styles to these divs. The iframe itself is given a style="height: 123px; style (where 123px is a dynamically computed appropriate height).
200 milliseconds before a tooltip is fully removed, we apply the tooltipFadeout class to its tooltipContainer.
When spawning a tooltip for an anchor with a data attribute data-tooltip-class="some classes", these classes are automatically added to any corresponding tooltipContainer div.
Limitations
The following are the main limitaitons you should be aware of before using WebTT:
- Because tooltips are included as iframes, they cannot be styled from the host page.
- Tooltips cannot communicate a preferred width to the host page, they need to work at whichever width the host page decides to render them.
- Our current js implementation sometimes reports an incorrect height for a tooltip. Iframe heights are notoriously finicky — pull requests for more robust height handling would be much appreciated.
Message Specification
In the remaining part of this document, we specify the required communication between WebTT tooltip pages and host pages.
We write context when we want to refer to a tooltip page or a host page.
The crux of getting iframe-isolated, nested tooltips to work is that nested
tooltips cannot simply spawn in the iframe of their parent tooltip; they need to
be spawned on the top-level page. To get this to work, the iframes and the
top-level page exchange information via the
Window.postMessage
API.
The first challenge there is that we cannot pass DOM elements through the message API, hence we need to define a different way of referencing tooltip elements.
In order to identify tooltips, we assign each context a contextCount:
the contextCount of the host page is always 0, and
the host page page assigns arbitrary but unique numbers as ids whenever it spawns a new
iframe. This id is communicated to the iframe as a URL query parameter, for
example, https://example.org/tooltip.html?contextCount=27 (for a tooltip page hosted at
https://example.org/tooltip.html).
Each context further assigns a unique, stable anchorCount to each of its
tooltip anchors, i.e., to each HTML element with a data-tooltip-anchor attribute.
It is fine to assign these lazily the first time any anchor spawns a tooltip.
A pair of a contextCount and an anchorCount serves to uniquely identify
every anchor that has spawned a tooltip. The messages of this specification
always refer to these (long-lived) anchors, not to the short-lived iframes (which
would typically be despawned after the user pointer stops hovering over the
tooltip).
/**
* An `AnchorId` is a pair of a `contextCount` and
* an `anchorCount`, joined by a colon:
* `${contextCount}:${anchorCount}` — for example, `17:6`.
*/
type AnchorId = string; Whenever the host page spawns a new tooltip iframe, it communicates the
contextCount for that iframe to the tooltip page with a query parameter in the URL: https://example-tooltip.org/?contextCount=17. This is the only communication
from host page to iframe. The URL can of course be used to convey further
application-specific query parameters as well, for example, whether to
highlight parts of the tooltip page. Such query parameters are out of scope
of this specification.
More importantly, the tooltip page in the iframe is responsible for sending
various messages to the parent page, via window.parent.postMessage (note
that iframes are not actually nested within each other but all spawned on
the host page, so window.parent always refers to the host page).
There are four kinds of messages a tooltip page in an iframe must send:
Information about the pointer entering an anchor. Must be sent immediately whenever the pointer hovers over an anchor within the tooltip page.
type AnchorOver = {
/**
* Detailed information about the tooltip that should
* be spawned for this anchor.
*/
anchorOver: SpawnTooltipInfo;
// `SpawnTooltipInfo` is specified later.
};Information about the pointer leaving an anchor. Must be sent immediately whenever the pointer stops hovering over an anchor within the tooltip page.
type AnchorLeave = {
/**
* The `AnchorId` of the anchor being left.
*/
anchorLeave: AnchorId;
};Information about pointer movement within the tooltip page. Must be sent whenever the pointer moves. These messages are necessary so that the host* page can correctly position any newly spawned nested tooltips.
type MouseMove = {
mouseMove: {
/**
* The clientX coordinate of the pointer, as obtained
* from a mouse event within the iframe.
*
* See
* https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX
*/
clientX: number;
/**
* The clientY coordinate of the pointer, as obtained
* from a mouse event within the iframe.
*
* See
* https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientY
*/
clientY: number;
};
};Information about the scrollHeight of the body of a tooltip page. Must be sent exactly once by each tooltip page in an iframe. This information is intended to allow the host page to select an appropriate height for the iframe, i.e., large enough to fit the contents, but not too much larger.
type TooltipDimensions = {
tooltipDimensions: {
/**
* The height of the tooltip page.
*/
height: number;
/**
* The tooltip to which this message belongs.
*/
contextCount: AnchorId;
};
};With these four messages, tooltips supply enough information for the host page to know when and where to spawn and despawn new tooltips. Yay!
All that remains is to specify the details of the AnchorOver message, which
contains the SpawnTooltipInfo. First, we need a type for bounding rectangles;
these provide the same information as a
DOMRect:
/**
* Information about bounding rectangles, necessary
* to position tooltips.
*
* See
* https://developer.mozilla.org/en-US/docs/Web/API/DOMRect
*/
interface Rect {
x: number;
y: number;
top: number;
left: number;
bottom: number;
right: number;
width: number;
height: number;
}
/**
* Everything you need to know to spawn a new tooltip.
*/
type SpawnTooltipInfo = {
/**
* The anchorId of the anchor for which to spawn
* the tooltip.
*/
anchorId: AnchorId;
/**
* The result of invoking `getBoundingClientRect`
* on the anchor element.
*/
clientRect: Rect;
/**
* An array containing (a shallow copy of) the
* results of invoking `getClientRects` on
* the anchor element.
*/
clientRects: Rect[];
/**
* The URL for the tooltip page to spawn.
* Must not yet contain a `contextCount` parameter,
* the host page will add that to the URL before
* creating the tooltip iframe.
*/
url: string;
};All messages are allowed to contain additional fields, in order to communicate implementation-specific information (for example, to pass styling hints to the iframe container). Implementations should ignore fields they do not expect.
