Lume is a lightweight, zero-dependency JavaScript tooltip engine providing mutation-aware DOM lifecycle management, collision detection, and hardware-accelerated positioning.
Note
Lume.js core is stable and ready for production use for bugs or feature requests please open an issue.
🔹 Read the full documentation here
🔹 Use the playground here
📚 Table of Contents
- Ultra-Lightweight & Zero-Dependency: Pure vanilla JavaScript with no runtime overhead.
- Single-Node DOM Recycling: Reuses a single
.lume-tooltipnode across all triggers to eliminate DOM bloat and thrashing. - Hardware-Accelerated Positioning: Coordinates calculated using
translate3d(x, y, 0)andwill-change. - Priority-Aware Collision Detection: Evaluates the complete tooltip rectangle, tries ordered fallback sides, and clamps only as a final safeguard when viewport or boundary space is constrained.
- Mutation-Aware Lifecycle: Automatically binds dynamically inserted elements and cleans up ghost tooltips when elements are deleted.
- Easily Themed & Animated: Customizable with standard CSS Custom Properties and active state transitions.
- Formatting Tokens: Safely renders supported text tokens, including emphasis, code, lists, line breaks, and composite pastel styles terminated with
{/lme}. - Editor Syntax Support: Ships LIRL syntax definitions for VS Code, Neovim, and Zed under
editors/sodata-lume*attributes remain readable in source files. - Trusted HTML Mode: Renders application-owned
data-lume-contentmarkup only whenallowHTML: trueis explicitly enabled. - Configurable Interaction: Supports hover/focus and click/tap activation, alignment, delays, motion intensity, custom containers, boundaries, and per-trigger overrides.
- Reference Presentation Tiers: Minimal, simple, and advanced tooltip layouts support structured LIRL content for compact facts, title/body help, and data-rich cards.
- Visual Decorations: Supports a CSS-only outlined
infoicon and custom-color circular status dots rendered as softened pastel indicators. - Viewport-Safe Content: Constrains tooltip width to the viewport and scrolls unusually tall content inside the content region without exposing unnecessary scrollbars.
- Optional Diagnostics:
@staticcanvas/lume/metricsprovides counters and timings;@staticcanvas/lume/debugintegrates lifecycle logging with Logcad. - Accessible: Built-in keyboard navigation support (
focus/blur) and ARIA roles (role="tooltip",aria-hidden).
| Library | Gzip size | Runtime deps | Smart positioning | Auto-binds dynamic DOM |
|---|---|---|---|---|
| Lume | 5.2 KB | None | Yes | Yes (MutationObserver) |
| Floating UI | 8.2 KB | None | Positioning only | No |
| Tippy.js | 14.4 KB | Popper | Yes | No |
| React Tooltip | 14.6 KB | Floating UI, clsx (React not included) | Yes | No |
| Radix Tooltip | 18.2 KB | Floating UI, Radix internals (React not included) | Yes | No |
| Hint.css | ~1.5 KB | None | No | N/A |
Sizes verified against bundlephobia and Lume's own production build. Use Lume for framework-agnostic sites needing smart positioning at a small footprint, especially where triggers are injected dynamically. Use Floating UI when building a custom positioning primitive from scratch, Radix/React Tooltip in an existing React app, or Hint.css for plain static text with no positioning needs. Full decision matrix and feature comparison: Comparison guide.
Lume operates as a singleton or instance-based manager that binds declarative HTML5 data attributes to viewport-aware positioning routines. DOM additions and removals are intercepted using a MutationObserver to prevent memory leaks and orphan tooltip instances.
flowchart TD
A[Trigger Event: mouseenter / focus] --> B[Resolve Data Attributes]
B --> C[Check Reusable Tooltip Element]
C --> D[Calculate Target & Viewport Coordinates]
D --> E{Collision Detected?}
E -- Yes --> F[Invert Direction Vector]
E -- No --> G[Maintain Preferred Direction]
F --> H[Apply translate3d Coordinates]
G --> H
H --> I[Attach Passive Scroll & Resize Listeners]
I --> J[Set aria-hidden false & lume-active class]
# npm
npm install @staticcanvas/lume
# pnpm
pnpm add @staticcanvas/lume
# bun
bun add @staticcanvas/lume# deno
deno add jsr:@staticcanvas/lume<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@staticcanvas/lume@0.36.7/src/lume.css" />
<script type="module">
import { Lume } from 'https://cdn.jsdelivr.net/npm/@staticcanvas/lume@0.36.7/dist/lume.mjs';
new Lume();
</script>The core bundle has no diagnostics overhead. Add metrics when measuring an integration, and add the debug helper when inspecting lifecycle behavior:
npm install @staticcanvas/logcadimport { Lume } from '@staticcanvas/lume';
import { createLumeMetrics } from '@staticcanvas/lume/metrics';
import { createLumeDebug } from '@staticcanvas/lume/debug';
const lume = new Lume();
const metrics = createLumeMetrics(lume);
const debug = createLumeDebug(lume);
console.table(metrics.snapshot());
// metrics.dispose();
// debug.dispose();Metrics include activation and dismissal counts, show/reposition timing, active state, trigger count, and direction or motion totals. Debug messages use @staticcanvas/logcad v0.6+ and can be detached at any time.
Developers contributing to Lume or embedding customized builds can compile the library, run the full test suite, verify bundle size budgets, and host the local documentation portal directly from source.
Ensure the following tools are installed in your environment:
- Node.js:
v18.0.0or higher (v20+LTS recommended). - Package Manager:
npm(v9.0.0+),pnpm, orbun. - Git: For source control and version synchronization.
- Hugo (Extended Edition):
v0.125.0+(required only for building and serving the static documentation site). - PowerShell (pwsh):
v7.0+(optional, for release automation tooling and documentation telemetry scripts).
Clone the repository and install developer dependencies:
# Clone the repository
git clone https://gitlab.com/staticcanvas/lume.git
cd lume
# Install dependencies with npm
npm install
# Alternatively, using bun or pnpm
bun install
# or
pnpm installLume uses Vite with Rollup and Terser under the hood to compile multi-format bundles, TypeScript definitions, stylesheets, and documentation assets.
# 1. Build all production bundles (core, metrics, and debug modules)
npm run build
# 2. Start Vite local development server with Hot Module Replacement (HMR)
npm run dev
# 3. Launch local development server and immediately open interactive test demo
npm run serve:demoThe primary npm run build script performs three coordinated passes:
- Core Library: Builds
lume.js,lume.min.js,lume.cjs,lume.mjs,lume.esm.js, andlume.esm.min.jsalong with sourcemaps. - Metrics Module: Runs
vite.optional.config.jswithnpm_config_mode=metricsto output decoupled telemetry bundles. - Debug Module: Runs
vite.optional.config.jswithnpm_config_mode=debugto compile Logcad-integrated diagnostic bundles. - Staging & Packaging: Copies bundled distribution files, styles (
src/lume.css), TypeScript definitions (src/lume.d.ts), and assets intodist/lume/and synchronizes documentation libraries indocs/static/webutil/libs/.
Upon running npm run build, the structured output is generated inside dist/lume/:
dist/lume/
├── dist/
│ ├── lume.mjs # Native ES module bundle (primary import)
│ ├── lume.esm.js # Standard ES module alias
│ ├── lume.esm.min.js # Minified ES module bundle
│ ├── lume.cjs # CommonJS module (Node.js / SSR)
│ ├── lume.js # UMD bundle with global 'Lume' export
│ ├── lume.min.js # Production minified UMD bundle
│ ├── lume-metrics.* # Optional performance telemetry bundles (ESM, CJS, UMD)
│ ├── lume-debug.* # Optional diagnostic logging bundles (ESM, CJS, UMD)
│ └── *.map # Generated source maps for all formats
├── src/
│ ├── lume.css # Core CSS custom properties and layout rules
│ └── lume.d.ts # TypeScript type declarations
├── branding/ # Official SVGs, logos, and vector assets
├── package.json # Sanitized distribution manifest
├── jsr.jsonc # JSR registry configuration
├── README.md # Package README
└── LICENSE # MIT License
Lume enforces rigorous testing, strict linting, and a hard 6 KiB gzip bundle budget to maintain zero runtime bloat.
# Run the complete Vitest unit test suite (Node.js / JSDOM)
npm test
# Run tests with V8 code coverage report
npm run test:coverage
# Run tests under Bun test runner
npm run test:bun
npm run test:coverage:bun
# Check code formatting with Prettier
npm run format
# Run ESLint validation (or fix auto-fixable issues)
npm run lint
npm run lint:fix
# Validate production bundle size against the gzip budget (max 6 KiB)
npm run check:size
# Verify package metadata, exports parity, and release files
npm run validate:release
# Execute full quality gate (build + check:size + test)
npm run check:qualityThe documentation portal is built with Hugo and styled with static utility harnesses:
# Start documentation server with live reload, draft pages, and release data
npm run docs:serve
# Compile static documentation portal into docs/public/
npm run docs:build// main.js
import '@staticcanvas/lume/css';
import { Lume } from '@staticcanvas/lume';
// Automatically discovers and binds [data-lume] and [data-lume-content] elements
const lume = new Lume();Or via HTML:
<link rel="stylesheet" href="./node_modules/@staticcanvas/lume/src/lume.css" />
<script type="module">
import { Lume } from './node_modules/@staticcanvas/lume/dist/lume.mjs';
new Lume();
</script><button data-lume="Save project changes">Save</button><button
data-lume="Export dataset as CSV, JSON, or XML format."
data-lume-title="Export Options"
data-lume-direction="bottom"
>
Export
</button>Use auto with a comma-separated priority list. Lume tries each side in order only when the complete tooltip rectangle fits; fixed directions use the same list when their preferred side cannot contain the tooltip.
<button
data-lume="This tooltip chooses the first fitting side."
data-lume-direction="auto"
data-lume-priority="top,left,bottom,right"
>
Safe placement
</button><!-- Position tooltip 24px away from target instead of default 14px -->
<button data-lume="Notifications and alerts" data-lume-direction="right" data-lume-offset="24">
Alerts
</button>Lume parses a small formatting vocabulary into DOM nodes without parsing HTML. Supported tokens include {b}, {i}, {c}, {s}, {l}, {u}, {mark}, {kbd}, {emp}, {ol}, {li}, and {br}. Compatible inline styles can be combined with colon-separated names, for example {sp-blue:b:u:i}Pastel emphasis{/lme}. Composite styles use the safe {/lme} terminator; unsupported styles and malformed tokens remain text.
<button
data-lume-title="User Status"
data-lume-content="Status: {b}Online{/b}{br}{s}Last active 2m ago{/s}"
data-lume-direction="top"
>
Profile
</button>Use {ol} with {li} for a numbered list: {ol}{li}First{/li}{li}Second{/li}{/ol}.
Use {emp} for an important note, feature callout, or extra context. It renders a compact text block with a theme-colored square marker: {emp}Important note{/lme}.
For application-owned markup such as a trusted user card, pass allowHTML: true when creating the instance. This is an explicit trusted-content mode, not an HTML sanitizer; never pass user-supplied or unsanitized server content. Interactive cards with links or controls belong in a popover or dialog rather than a tooltip.
import { Lume } from '@staticcanvas/lume';
import '@staticcanvas/lume/css';
new Lume({ allowHTML: true });<button data-lume-content="<strong>Status:</strong> <span class='status'>Online</span>">
Account status
</button><p>
The service uses an in-memory
<span
class="lume-inline"
data-lume="In-memory key-value data structure store used as a distributed cache."
data-lume-title="Redis Engine"
data-lume-direction="top"
>
Redis Cache
</span>
to handle high throughput.
</p><button
data-lume="This action cannot be reversed!"
data-lume-title="Danger"
data-lume-direction="top"
data-lume-class="lume-danger"
>
Delete Database
</button>You can manually trigger, reposition, hide, or destroy tooltips programmatically.
import { Lume } from '@staticcanvas/lume';
// Initialize with custom options
const lume = new Lume({
offset: 16,
smart: true,
className: 'my-global-theme',
});
const saveBtn = document.querySelector('#save-btn');
// Manually display tooltip on a specific element
lume.show(saveBtn);
// Recalculate position (e.g. after animation, layout shift, or dynamic content resize)
lume.reposition();
// Hide the active tooltip
lume.hide();
// Tear down all listeners, observers, and remove DOM node
lume.destroy();Lume uses MutationObserver to automatically observe dynamically created elements and cleans up ghost popovers when triggers are removed.
const lume = new Lume();
// Elements added dynamically are bound automatically without calling init() again
const newBtn = document.createElement('button');
newBtn.setAttribute('data-lume', 'Dynamically created action');
newBtn.textContent = 'Dynamic Button';
document.body.appendChild(newBtn);import { useEffect } from 'react';
import { Lume } from '@staticcanvas/lume';
import '@staticcanvas/lume/css';
export function App() {
useEffect(() => {
const lume = new Lume();
return () => {
lume.destroy(); // Clean up listeners and observers on unmount
};
}, []);
return (
<button data-lume="Interactive button" data-lume-title="React App">
Click Me
</button>
);
}<script setup>
import { onMounted, onUnmounted } from 'vue';
import { Lume } from '@staticcanvas/lume';
import '@staticcanvas/lume/css';
let lumeInstance = null;
onMounted(() => {
lumeInstance = new Lume();
});
onUnmounted(() => {
lumeInstance?.destroy();
});
</script>
<template>
<button data-lume="Vue 3 tooltip" data-lume-direction="right">Hover Me</button>
</template>Override CSS custom properties on :root or .lume-tooltip:
:root {
--lume-bg: #0f172a;
--lume-text: #f8fafc;
--lume-accent: rgba(255, 255, 255, 0.12);
--lume-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
--lume-radius: 12px;
--lume-font-size: 12px;
--lume-max-width: 320px;
}/* Danger variant */
.lume-tooltip.lume-danger {
--lume-bg: #dc2626;
--lume-text: #ffffff;
--lume-accent: rgba(255, 255, 255, 0.25);
}
/* Success variant */
.lume-tooltip.lume-success {
--lume-bg: #059669;
--lume-text: #ffffff;
--lume-accent: rgba(255, 255, 255, 0.25);
}.lume-tooltip {
transform: scale(0.92);
transition:
opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),
transform 200ms cubic-bezier(0.16, 1, 0.3, 1),
visibility 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
.lume-tooltip.lume-active {
transform: scale(1);
}:root {
--lume-transition: 280ms cubic-bezier(0.34, 1.56, 0.64, 1); /* playful bounce */
}Repository automation is stored under tools/. The scripts support CI release metadata, generated badges, and repeatable repository maintenance.
rivet is installed as an ignored repository-local checkout by the tracked rivet.ps1 launcher. The launcher clones rivet when absent and forwards commands with Lume selected as the consuming repository. Rivet calculates versions from Conventional Commits, synchronizes manifests and README metadata, generates release documents, offers reviewed commits, and can synchronize GitLab metadata.
Preview local metadata changes without writing files:
./rivet.ps1 -DryRunFor day-to-day work, start with the interactive command center:
./rivet.ps1 menuThe usual repository workflow is:
- Run
./rivet.ps1 doctor, then./rivet.ps1 status, to validate the repository and see its release state. - Choose Prep Release to preview or synchronize manifests, README metadata, changelog, release notes, and
mr.mdin one reviewed workflow. - Choose Conventional Commit to select a type, scope, and exact files. rivet suggests paths from the scope configuration, then shows the subject, extracted release note, diff totals, and patch before staging anything.
- Use
./rivet.ps1 versionto explain the next version or-glofor enriched history. Add-CommitScope corefor an exact scope filter. - Preview the complete release set with
./rivet.ps1 prep-release -DryRun. Entries include a linked short commit hash and GitLab@usernamemention by default. - Create a CI-ignored checkpoint with
./rivet.ps1 -Tag -Snapshot, then use./rivet.ps1 -Tagfor atomic branch-and-tag production publication.
Use ./rivet.ps1 help for the grouped manual or ./rivet.ps1 help CommitScope for parameter-specific guidance. Every writing or remote workflow offers a preview path; GitLab synchronization is never enabled by the default local run.
Remote GitLab changes are opt-in and use GITLAB_API_KEY. Configuration, command switches, script responsibilities, and dry-run examples are documented in the rivet reference.
Run ./rivet.ps1 -Commit to select a Conventional Commit type and scope, accept or replace the configured description, review the subject and changed files, and explicitly confirm submission.
Run ./rivet.ps1 -glo for the read-only detailed Git history view with enriched one-line commits, branch and upstream state, aggregate history statistics, and current worktree details.
Run ./rivet.ps1 help for the colored terminal manual or ./rivet.ps1 help <parameter> for full behavior, constraints, interactions, and examples for an individual runner parameter. Direct help.ps1 invocation remains available for reuse.
Run ./rivet.ps1 menu to open the reusable interactive command center. It covers release preparation, local metadata, GitLab merge-request creation and updating, repository initialization, GitLab metadata synchronization, Conventional Commits, snapshot and production tags, Git history, and help, with workflow-specific safe defaults and command review before execution.
Use ./rivet.ps1 -Tag -Snapshot for a pushed checkpoint tag ignored by CI, or ./rivet.ps1 -Tag for a strict SemVer production tag that starts publishing and release pipelines. Add -DryRun to either command for a non-writing review.
Run npm run docs:serve for local documentation development. It refreshes docs/data/releases.json from public GitLab Releases, rebuilds Lume, and starts Hugo with cache disabling. Set GITLAB_API_KEY when accessing release metadata that is not public.
| Script | Purpose |
|---|---|
Calculate-Runtime.ps1 |
Calculates elapsed pipeline runtime from CI_PIPELINE_CREATED_AT for generated release metadata. |
Generate-Badge.ps1 |
Generates flat or for-the-badge SVG status badges with configurable labels, colors, messages, and an optional icon. |
Get-ReleaseDownloads.ps1 |
Collects GitLab Releases and their generic-package files and writes the release-only download data consumed by the documentation site. |
Request-GenericPackage.ps1 |
Resolves a GitLab generic package and returns its files with metadata and browser download URLs. It is used by Get-ReleaseDownloads.ps1. |
| Key | Type | Default | Description |
|---|---|---|---|
selector |
string |
"[data-lume], [data-lume-content]" |
CSS selector used to discover and bind trigger elements. |
offset |
number |
14 |
Pixel offset distance between the target element and tooltip body. |
smart |
boolean |
true |
Enables boundary collision detection and auto-direction flipping. |
priority |
Array<top | bottom | left | right> |
top,bottom,right,left |
Ordered sides used by automatic placement and smart fallback selection. |
className |
string |
"" |
Additional CSS class string appended to the tooltip container. |
align |
left | center | right |
center |
Horizontal alignment for top and bottom tooltip placement. |
action |
hover | click |
hover |
Trigger activation model; click supports mouse, touch, and keyboard activation. |
motion |
Preset | fade |
Entry motion: fade, lift, scale, slide, pop, blur, bounce, swing, flip, or none. |
icon |
string |
"" |
Application-owned SVG URL or info for the outlined info icon. |
dot |
string |
"" |
Optional custom-color status dot rendered as a softened pastel indicator. |
dotPosition |
left | right | top | bottom |
left |
Status-dot position around the tooltip shell. |
motionIntensity |
number |
1 |
Motion strength clamped from 0 through 3. |
showDelay |
number |
0 |
Delay before showing, in milliseconds. |
hideDelay |
number |
0 |
Delay before hiding, in milliseconds. |
allowHTML |
boolean |
false |
Renders trusted data-lume-content as HTML. |
container |
HTMLElement |
document.body |
Parent container to which the tooltip element is appended. |
boundary |
HTMLElement |
null |
Element whose visible bounds constrain placement. |
padding |
number |
10 |
Minimum distance from the positioning boundary. |
autoInit |
boolean |
true |
Executes initial DOM query and registers MutationObserver automatically. |
observeMutations |
boolean |
true |
Binds matching elements added after initialization. |
| Attribute | Type | Default | Description |
|---|---|---|---|
data-lume |
string |
"" |
Primary plain-text tooltip content. |
data-lume-content |
string |
"" |
Explicit content source; rendered as text with tokens unless allowHTML is enabled. |
data-lume-title |
string |
"" |
Optional header title text rendered inside .lume-header. |
data-lume-icon |
string |
"" |
Optional application-owned SVG URL rendered before the title. |
data-lume-align |
left | center | right |
center |
Horizontal alignment for top and bottom placement. |
data-lume-direction |
string |
"top" |
Preferred placement: "auto", "top", "bottom", "left", or "right". |
data-lume-priority |
string |
"top,bottom,right,left" |
Comma-separated fallback order used by auto and smart collision handling. |
data-lume-offset |
number |
14 |
Per-element pixel offset distance override. |
data-lume-class |
string |
"" |
Per-element custom CSS class override. |
data-lume-action |
hover | click |
hover |
Per-element hover/focus or click/tap activation. |
data-lume-motion |
Preset | fade |
Per-element motion override. |
data-lume-intensity |
number |
1 |
Per-element motion intensity from 0 through 3. |
data-lume-smart |
boolean |
true |
Per-element collision detection override. |
| Method | Parameters | Return Type | Description |
|---|---|---|---|
init() |
None | void |
Scans the container and binds matching selector elements. |
show(el) |
el: HTMLElement |
void |
Activates and positions tooltip for the given target element. |
hide() |
None | void |
Hides active tooltip and unbinds scroll/resize listeners. |
reposition() |
None | void |
Recalculates bounding rect coordinates and applies hardware transforms. |
destroy() |
None | void |
Disconnects observers, unbinds all events, and removes tooltip DOM nodes. |
| Variable | Default | Description |
|---|---|---|
--lume-bg |
#1a1a1b |
Background color of tooltip body and directional arrow. |
--lume-text |
#ffffff |
Foreground text color. |
--lume-accent |
rgba(255, 255, 255, 0.15) |
Border color separating header from content body. |
--lume-shadow |
0 10px 15px -3px rgba(0, 0, 0, 0.25), 0 4px 6px -2px rgba(0, 0, 0, 0.12) |
Drop shadow applied to tooltip container. |
--lume-radius |
8px |
Border radius of tooltip container. |
--lume-transition |
180ms cubic-bezier(0.4, 0, 0.2, 1) |
Timing function for opacity and visibility transitions. |
--lume-arrow-size |
6px |
Half-width dimension of directional indicator arrow. |
--lume-font-family |
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif |
Font family applied to tooltip container. |
--lume-font-size |
13px |
Base typography size. |
--lume-line-height |
1.5 |
Base line height. |
--lume-max-width |
280px |
Maximum container width before word wrapping. |
Distributed under the MIT License. See LICENSE for more information.