@svelte-put/tooltip
over-engineered, type-safe, headless, and extensible tooltip builder via Svelte action
This library is not a typical one with a pre-built component. It has a rather 'lower-level' api that helps setup composable tooltips, utilizing svelte action .
Installation
terminal
npm install --save-dev @svelte-put/tooltip
Quick Start
CSS code for c-tooltip
is omitted for conciseness, but will be discussed in a later
section.
out-of-the-box tooltip action
<script lang="ts">
import { computePosition } from '@floating-ui/dom';
import { tooltip } from '@svelte-put/tooltip';
</script>
<button
class="c-btn-primary 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>
Design Decisions
@svelte-put/tooltip
adopts a headless approach. By default, it will take care of:
- creating a container element,
- handling logics for inserting elements & events for changing visibility state,
- 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
users. Thanks to this, @svelte-put/tooltip
can be configured to use in a lot of different
sites and 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 the c-tooltip
class, and positioning logics is provided by paring
with @floating-ui in the
compute
callback.
Preparing Your Own Tooltip Action
prepare
API
The prepare a reusable tooltip action
import { prepare } from '@svelte-put/tooltip';
export const myTooltip = prepare({ content: 'placeholder' });
// later: <button use:myTooltip>...</button>
The same interface is used for both the builtin tooltip
action &
prepare
helper.
simplified parameter interface
type TooltipParameter = TooltipContainer & {
content: Content;
compute?: TooltipCompute;
};
export function tooltip(node: HTMLElement, param: TooltipParameter);
export function prepare(param: TooltipParameter);
Example
This section provides a comprehensive example for building a custom tooltip action. 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 of code for this demo: , , , . Select corresponding tab to see each.
using prepared action
<script lang="ts">
import { helloTip } from './prepare.code';
</script>
<button use:helloTip class="c-btn-primary 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), ...
content as string
<script>
import { prepare } from '@svelte-put/tooltip';
const tooltipWithText = prepare({ content: 'Placeholder' });
</script>
<button use:tooltipWithText>A Button</button>
... or as any Svelte component, ...
content as component
<script>
import { prepare } from '@svelte-put/tooltip';
import TooltipComponent from './Tooltip.component.svelte';
export const tooltipWithComponent = prepare({ content: TooltipComponent });
</script>
<button use:tooltipWithComponent>A Button</button>
... and optionally with default props.
Note that if a component has required props and no default props are 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.
content as component with default props
<script>
import { prepare } from '@svelte-put/tooltip';
import TooltipComponent from './Tooltip.component.svelte';
const tooltipWithComponentAndDefaultProps = prepare({
content: {
component: TooltipComponent,
props: {
content: 'Default content',
},
},
});
</script>
<button use:tooltipWithComponentAndDefaultProps>A Button</button>
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
.
Svelte component as tooltip content
<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
customization properties is available.
container interface
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`
*
* @remarks
* 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`.
*
* @remarks
* Set to `false` to disable, or provide a string to use as attribute name.
*/
visibleAttribute?: boolean | string;
/**
* config for accessibility
* Defaults to `true`
*
* @remarks
* 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 CSS Setup from example above .
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.
simplified compute interface
export Compute = ({
node: HTMLElement,
tooltip: HTMLElement,
content: string | SvelteComponent, // inferred from the content parameter
}) => void | (() => void) | Promise<void | (() => void)>
In Action Setup from example above ,
@floating-ui (previously
popperjs
) is used to handle the positioning logics. It's a minimal & extensible
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 door 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! 👨💻