@svelte-put/tooltip
Deprecation Notice
This package will be deprecated when Svelte 5 lands, in favor for @svelte-put/popover - a more generic package that builds upon the new Popover API. If you are using Svelte 5 today, head over to @svelte-put/popover for more information.
Introduction
This library is not a plug-and-play, prebuilt component. Rather, it is designed to help build composable tooltips, utilizing Svelte action.
Installation
npm install --save-dev @svelte-put/tooltip
pnpm add -D @svelte-put/tooltip
yarn add -D @svelte-put/tooltip
Quick Start
The following example shows usage of @svelte-put/tooltip
’s out-of-the-box tooltip
action, paired with @floating-ui.
<script lang="ts">
import { computePosition } from '@floating-ui/dom';
import { tooltip } from '@svelte-put/tooltip';
</script>
<button
class="c-btn relative"
use:tooltip={{
content: 'An example tooltip',
class: 'c-tooltip',
compute: async ({ node, tooltip, content }) => {
console.log(content);
const { x, y } = await computePosition(node, tooltip, {
placement: 'top',
});
tooltip.style.left = `${x}px`;
tooltip.style.top = `${y}px`;
},
}}
>
A button with tooltip
</button>
CSS Code for c-tooltip
in above code snippet is omitted for conciseness, but will be discussed in a later section.
Design Decisions
@svelte-put/tooltip
adopts a headless approach. By default, it will take care of:
- creating a tooltip container element,
- handling logics for inserting elements & events for changing visibility state of the tooltip,
- managing
pointer-events
style on tooltip container element, - managing
role
,id
, andaria-describedBy
accessibility attributes,
and leave tooltip UI and positioning to be determined by library users. Thanks to this approach, @svelte-put/tooltip
can be configured to fit a lot of different applications.
For the same reason, however, the out-of-the-box use:tooltip
action does not do much. As seen in Quick Start, the styling is provided through CSS .c-tooltip
, and positioning logics is provided by pairing with @floating-ui in the compute
callback.
prepare
API
The Following the aforementioned design decisions, @svelte-put/tooltip
provides a prepare
API to help create reusable tooltip actions with their own UI and positioning logics.
import { prepare } from '@svelte-put/tooltip';
export const myTooltip = prepare({ content: 'placeholder' });
// later in Svelte: <button use:myTooltip>...</button>
Argument passed to prepare
share the same TooltipParameter
interface as that passed to use:tooltip
(seen in Quick Start)
type TooltipParameter = TooltipContainer & {
content: Content;
compute?: TooltipCompute;
};
export function tooltip(node: HTMLElement, param: TooltipParameter);
export function prepare(param: TooltipParameter);
Below is a comprehensive example of how to use prepare
to build highly custom tooltip actions. If you feel overwhelmed, don’t get too caught up with the code; more information is provided in the next sections.
There are four parts to this setup:
set up Svelte tooltip component (
Tooltip.svelte
):set up CSS for tooltip component (
c-tooltip.css
):create reusable tooltip action with the
prepare
API (tooltips.ts
):use the prepared tooltip action (
usage.svelte
):
<script>
// (optional) props injected by @svelte-put/tooltip at runtime
export let visible = false;
// your props
export let content = 'Placeholder content';
$: console.log('visibility state', visible);
</script>
<p class="m-0 p-0 text-gradient-brand text-lg">{content}</p>
.c-tooltip {
/* Float on top of the UI */
position: absolute;
z-index: theme('zIndex.popup');
top: 0;
left: 0;
/* Avoid layout interference */
width: max-content;
padding: 4px 8px;
opacity: 0;
background-color: theme('colors.bg.100');
border-radius: 4px;
transition: opacity 150ms ease-in-out;
&[data-visible='true'] {
opacity: 1;
}
}
.c-tooltip-arrow {
position: absolute;
z-index: -1;
transform: rotate(45deg);
width: 14px;
height: 14px;
background-color: theme('colors.bg.100');
}
import { arrow, autoUpdate, computePosition, offset } from '@floating-ui/dom';
import { prepare } from '@svelte-put/tooltip';
import Tooltip from './Tooltip.svelte';
export const helloTip = prepare({
content: {
component: Tooltip,
props: {
content: 'Hello world!',
},
},
target: 'parent',
debounce: 120,
class: 'c-tooltip',
compute: async ({ node, tooltip, content }) => {
console.log('Content rendered, string or Svelte component instance', content);
const arrowEl = document.createElement('div');
arrowEl.className = 'c-tooltip-arrow';
tooltip.prepend(arrowEl);
autoUpdate(node, tooltip as HTMLElement, async () => {
const { x, y, middlewareData, placement } = await computePosition(node, tooltip as HTMLElement, {
placement: 'right',
middleware: [
offset(16),
arrow({
element: arrowEl,
}),
],
});
(tooltip as HTMLElement).style.left = `${x}px`;
(tooltip as HTMLElement).style.top = `${y}px`;
const staticSide = (
{
top: 'bottom',
right: 'left',
bottom: 'top',
left: 'right',
} as const
)[placement.split('-')[0]];
arrowEl.style.left = `${middlewareData.arrow?.x ?? 0}px`;
arrowEl.style.top = `${middlewareData.arrow?.y ?? 0}px`;
if (staticSide) arrowEl.style[staticSide] = '-4px';
});
},
});
<script lang="ts">
import { helloTip } from './tooltips';
</script>
<button use:helloTip class="c-btn relative">Hello Button</button>
Tooltip Content
This is the only required field for both the out-of-the-box tooltip
action and the prepare
function. Besides setting the default content, it also helps generate type inference for the parameter of the prepared action.
Content can be provided as either a string
(inserted as innerHTML
to the tooltip container), …
<script>
import { prepare } from '@svelte-put/tooltip';
const tooltipWithText = prepare({ content: 'Placeholder' });
</script>
<button use:tooltipWithText>A Button</button>
… or as any Svelte component (optionally with default props)…
<script>
import { prepare } from '@svelte-put/tooltip';
import Tooltip from './Tooltip.svelte';
const tooltipWithComponent = prepare({
content: {
component: TooltipComponent,
// optionally with default props
props: {
content: 'Default content',
},
},
});
</script>
<button use:tooltipWithComponent>A Button</button>
Note that if a component has required props and but no default prop is provided, you will get warning in browser console (and language server error if set up), and potentially runtime error if internal component logics depends on such props.
There is no restriction on Svelte component for tooltip content. You can optionally declare a visible
prop which will be injected at runtime for you by @svelte-put/tooltip
.
<script>
// (optional) props injected by @svelte-put/tooltip at runtime
export let visible = false;
// your props
export let content = 'Placeholder content';
$: console.log('visibility state', visible);
</script>
<p class="m-0 p-0 text-gradient-brand text-lg">{content}</p>
Tooltip Container
The tooltip container is rendered by @svelte-put/tooltip
. The following customizations are available as top-level properties of the TooltipParameter
interface.
type TooltipParameter = TooltipContainer & { /* truncated */ };
export type TooltipContainer = {
/**
* class name(s) to assign to tooltip container. Typically needed depending
* on the positioning strategy
*/
class?:
| string
| {
default?: string;
/** toggled on when tooltip is visible */
visible?: string;
};
/**
* HTML tag to render the tooltip container.
* Defaults to `div`
*/
tag?: string;
/**
* `HTMLElement` to render the tooltip container as child.
* Defaults to `parent` of the node action is placed on
*/
target?:
| 'parent'
| 'self'
| 'body'
| HTMLElement
| ((node: HTMLElement, tooltip: HTMLElement) => void);
/**
* number of milliseconds to debounce show / hide state of the tooltip.
* Defaults to `false` (show / hide immediately)
*/
debounce?: false | number;
/**
* config for handling of `pointer-events` on the container element
* Defaults to `true`
*
* By default `pointer-events` is set to `none` by default, and `auto` when triggered.
* Set to `false` to disable default behavior, or provide string(s) to
* corresponding states
*/
pointerEvents?:
| boolean
| {
default?: string;
/** value when tooltip is visible */
visible?: string;
};
/**
* the attribute to toggle in respond to tooltip's visibility state.
* Defaults to `data-visible`.
*
* Set to `false` to disable, or provide a string to use as attribute name.
*/
visibleAttribute?: boolean | string;
/**
* config for accessibility
* Defaults to `true`
*
* By default:
* - (for container element) `role` is set to `tooltip`,
* - (for container element) `id` is taken from `aria-describedby` of
* the node action is placed on (if any),
* or auto-generated from a global counter,
* - (for node on which action is used) `aria-describedby` is set to the `id` of
* the container element (if not already exists)
*
* Set to `false` to disable default behavior, or provide string(s) to
* the corresponding attributes
*/
aria?:
| boolean
| {
role?: string;
id?: string;
};
};
Typically, you should specify a class name with enough css depending on your rendering & positioning strategy. See examples in Quick Start and Prepare.
Tooltip Compute
The positioning logics is not handled by the library but left up to you via the compute
method. This is to avoid complicating the public api of the library, which otherwise would often try to do either too much or not enough, in my personal experience.
export type Compute = ({
node: HTMLElement,
tooltip: HTMLElement,
content: string | SvelteComponent, // inferred from the content parameter
}) => void | (() => void) | Promise<void | (() => void)>
In Prepare, the action setup integrates @floating-ui (previously popperjs
) for handling positioning logics. @floating-ui
is a minimal and extensive library that pairs well with @svelte-put/tooltip
. I recommend giving it a try.
To tooltip or not to tooltip?
Although this library opens a lot of doors for composing UI by accepting any Svelte component, tooltip should be kept minimal. Nielsen Norman Group’s article on tooltip is a recommended read. To quote MDN docs on tooltip:
If the information is important enough for a tooltip, isn’t it important enough to always be visible?
Happy tooling & tipping! 👨💻