@svelte-put/tooltip GitHub

over-engineered, type-safe, headless, and extensible tooltip builder via Svelte action

@svelte-put/tooltip @svelte-put/tooltip @svelte-put/tooltip @svelte-put/tooltip changelog

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

Example

Hover or tab into focus to trigger tooltip

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, and aria-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

The prepare API

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.

Example

Hover or tab into focus to trigger tooltip

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?"

mouse click faster

Happy tooling & tipping! 👨‍💻

Edit this page on GitHub