Skip to content

Tooltip ​

Tooltips for any element: plain HTML, light <mono-*> components, and shadow <mono-shadow-*> hosts. Positioning comes from Floating UI.

Turn the addon on once, then write the attribute. No component needs any tooltip code:

ts
// main.ts
import { createMonoTooltip } from 'mono-helper'
app.use(createMonoTooltip())
vue
<mono-button mono-tooltip-content="Save changes">Save</mono-button>
<button mono-tooltip-content="Works on plain HTML too">…</button>

For tooltips set up from code (content computed per element, a ref, manual show/hide), use controlMonoTooltip.

Install

@floating-ui/dom and @floating-ui/core are optional peers. They aren't bundled, and you install them yourself:

sh
pnpm add @floating-ui/dom @floating-ui/core

They load the first time a tooltip opens, not on import, so registering tooltips costs nothing until someone hovers. If the packages are missing, the tooltip doesn't open, and the console names the packages and the install command.

Basic ​

mono-tooltip-content holds the text; mono-tooltip-message is an alias for it. Bind it like any attribute (:mono-tooltip-content="msg"). If the value changes while the tooltip is open, the tooltip updates in place. An empty value shows nothing, so :mono-tooltip-content="''" turns one element's tooltip off.

The attributes only work after createMonoTooltip() has run: in main.ts, or through mono.helper.tooltip in Nuxt (see Global options). Nothing is looked up in advance. One shared set of document listeners checks each hover, focus or click against the attribute, so elements added later by v-if, v-for or a route change work automatically.

Attribute options ​

Every option has a mono-tooltip-<option> attribute. An element's attributes beat the global options for that element, because they are the most specific setting there is.

AttributeValue
mono-tooltip-content / mono-tooltip-messagethe text
mono-tooltip-placementtop, bottom-start, right-end, …
mono-tooltip-variantinverted, popover
mono-tooltip-colorprimary, danger, …
mono-tooltip-sizesm, md, lg
mono-tooltip-triggerhover, focus, click, manual; several separated by spaces or commas
mono-tooltip-delay300, or 300 100 for show and hide
mono-tooltip-offset / mono-tooltip-paddinga number (px)
mono-tooltip-max-width / mono-tooltip-classa CSS length / class names
mono-tooltip-arrow, -flip, -shift, -interactive, -html, -disabled, -hide-on-clickon when present; ="false" turns it off

mono-tooltip-html renders the text as HTML. Only use it with trusted strings.

Controller ​

controlMonoTooltip(target, options) attaches one set of options to every element that matches target. It doesn't need createMonoTooltip(). Use it when the content comes from code: a function per element, a Node, or manual show()/hide(). The call can go in <script setup>, before the elements exist, or in onMounted; both work.

ts
import { controlMonoTooltip } from 'mono-helper'

const tip = controlMonoTooltip('.row-action', { content: (el) => el.dataset.help, placement: 'bottom' })
onBeforeUnmount(() => tip.destroy())

Don't point a controller at an element that also has mono-tooltip-content, or both tooltips open.

Target ​

targetMatches
'.class', '#id', '[data-x]', 'mono-button'any CSS selector
elementthat element
[a, b], NodeListeach element
ref(null), () => elwhatever the ref / getter holds when the event fires

Content ​

If content is omitted, the anchor provides the text. The first of these that is set wins:

  1. mono-tooltip-content / mono-tooltip-message
  2. title
  3. aria-label

While our tooltip is open, a native title is moved aside so the browser's own tooltip doesn't appear on top of it.

content can also be a string, a Node, or a function (anchor) => string | Node. The function is called on every show, so one controller can show different text for each row. If it returns null or '', the tooltip doesn't open.

Placement ​

placement is the preferred side. If there isn't room there, flip moves the tooltip to the other side, and shift slides it along the anchor to keep it on screen. padding sets how far it stays from the viewport edge. The position is recalculated while the page scrolls or resizes, and the tooltip hides if the anchor scrolls out of view.

Triggers ​

triggerOpens onCloses on
'hover'pointer enters (mouse and pen; a touch tap counts as a click)pointer leaves
'focus'keyboard focus (:focus-visible)blur
'click'click on the anchora second click, or a click outside
'manual'tip.show()tip.hide()

The default is ['hover', 'focus']. Escape closes the tooltip; if a modal is open underneath, the next Escape closes the modal. With hideOnClick (on by default), clicking the anchor closes a hover tooltip, and it stays closed until the pointer leaves the anchor.

Mono components (light + shadow) ​

Matching uses the event's composed path, so it crosses shadow boundaries:

  • An attribute or selector on a <mono-shadow-button> host works just like on a light <mono-button>.
  • A ref can also point at a node inside an open shadow root.
  • Put mono-tooltip-* on the component's own tag. The attribute stays there, and it is never copied onto a component's dropdown panel.

The tooltip element itself is added to <body>, outside every shadow root. That means the global mono-helper/ui/index.css styles it the same way for both builds. It also joins the shared popup stack, so a tooltip on a button inside an open modal appears above the modal.

Interactive and HTML content ​

With interactive: true, the tooltip stays open while the pointer is over it, so links and buttons inside it can be clicked. allowHTML: true renders string content as HTML. Only pass trusted HTML. Without it, strings are set as plain text.

Variants, colours, sizes ​

Global options ​

createMonoTooltip does two things:

  • it turns on the mono-tooltip-* attributes;
  • it sets options for every tooltip in the app.

Global options override per-call options, so a single controlMonoTooltip call can't drift from the app-wide design. An element's own mono-tooltip-* attributes still override the global options for that element. Keys a layer leaves unset fall through to the layer below.

Precedence: defaults ← controlMonoTooltip(target, options) ← createMonoTooltip(options) ← the element's mono-tooltip-* attributes.

createMonoTooltip({ attributes: false }) keeps the global options but turns the attributes off.

ts
import { createApp } from 'vue'
import { createMonoTooltip } from 'mono-helper'
import App from './App.vue'

createApp(App)
  .use(createMonoTooltip({ delay: [300, 0], variant: 'popover' })) // or just call it
  .mount('#app')
ts
export default defineNuxtConfig({
  modules: ['mono-helper/nuxt'],
  mono: {
    helper: {
      tooltip: { delay: [300, 0], variant: 'popover' }, // or `true` for no options
    },
  },
})

In Nuxt, the options are passed to a client-only plugin that calls createMonoTooltip. That means they must be plain JSON: a content function, onShow/onHide callbacks or DOM nodes won't survive. The module also pre-bundles @floating-ui/dom, so the first hover doesn't make Vite re-optimize and reload the page.

On the server, controlMonoTooltip does nothing, so calling it in <script setup> under SSR is safe.

Options ​

OptionTypeDefault
contentstring | Node | (anchor) => string | Node | nullfrom the anchor
allowHTMLbooleanfalse
placement'top' | 'top-start' | 'top-end' | 'right…' | 'bottom…' | 'left…''top'
offsetnumber (px gap)6
paddingnumber (px kept from the viewport edge)8
flip / shiftbooleantrue
arrowbooleantrue
trigger'hover' | 'focus' | 'click' | 'manual' or an array['hover', 'focus']
delaynumber | [show, hide] (ms)[100, 0]
interactivebooleanfalse
maxWidthCSS length--mono-tooltip-max-width β†’ 20rem
variant'inverted' | 'popover''inverted'
colorprimary, secondary, success, danger, warning, info, teal, purple, neutral, darkβ€”
size'sm' | 'md' | 'lg''md'
classextra class(es) on the tooltip elementβ€”
appendToElement | () => Elementdocument.body
disabledbooleanfalse
hideOnClickbooleantrue
onShow / onHide(anchor, tooltip) => void | false (return false to cancel)β€”

Controller API ​

ts
const tip = controlMonoTooltip('.x', options)

await tip.show(el?)     // open on el (default: the first element matching the target)
tip.hide()
await tip.toggle(el?)
tip.update({ placement: 'left' })  // merge into this call's options
tip.setContent('New text')         // re-renders an open tooltip in place
tip.enable() / tip.disable()
tip.isOpen; tip.anchor; tip.tooltip
tip.destroy()           // call from onBeforeUnmount

monoTooltip is an alias for controlMonoTooltip. destroyAllMonoTooltips() closes every tooltip and releases every controller.

Theme ​

Every colour comes from theme tokens:

  • --foreground and --background for the default inverted bubble;
  • --popover and --popover-foreground, with the select panel's ring and shadow, for variant: 'popover';
  • the palette pairs for color.

Because of this, flavours, colour presets and .dark all apply automatically. The corner radius follows each flavour's --mono-tooltip-radius.

A section with its own theme, such as <div class="mono-theme dark theme-color-rose">, is handled too: the tooltip copies the nearest such wrapper's theme classes, so it matches the section even though the element itself sits on <body>.

Customising ​

VariableDefault
--mono-tooltip-bg / --mono-tooltip-color--foreground / --background
--mono-tooltip-radiusper flavour, else --mono-radius-md
--mono-tooltip-ring / --mono-tooltip-shadownone (inverted), dropdown ring/shadow (popover)
--mono-tooltip-max-width20rem
--mono-tooltip-arrow-sizecalc(var(--mono-spacing) * 2.5)
--mono-tooltip-font-size, -line-height, -padding-x, -padding-ytext-xs, 1.5 Γ— 3 spacing
--mono-tooltip-font-family--font-sans

The element is <div class="mono-tooltip" role="tooltip" mono-tooltip data-side="top">. Its attributes are:

  • mono-variant, mono-color, mono-size, mono-interactive, mono-arrow;
  • data-open while shown.

Target those attributes (or your own class option) for anything the variables above don't cover.

Accessibility ​

  • The tooltip has role="tooltip", and the anchor gets aria-describedby pointing at it while it is open. The attribute is restored afterwards.
  • Keyboard focus opens it; Escape closes it (WCAG 1.4.13).
  • A mouse click that moves focus does not reopen it.

Optional dependency ​

@floating-ui/* is left external in both the light and the shadow builds, so your bundler resolves and splits it. mono-helper/tooltip (and the same exports from mono-helper) contain no custom elements, so importing them registers nothing.