Tooltip

CSS-only tooltip pulled from [data-tooltip="…"]. No JS, no extra markup: the bubble is a ::after pseudo-element whose text is read via content: attr(data-tooltip). Revealed on :hover and :focus-within so keyboard users get the same affordance as pointer users. No !important on the trigger's position rule — see the accessibility section for why.

How it works

  • The trigger declares position: relative so the absolutely positioned ::after anchors to it. The rule is not marked !important: if a consumer has already positioned the trigger (e.g. an absolutely placed icon), stomping on that author intent would silently break layouts. When the trigger is position: static, the bubble anchors to the nearest positioned ancestor instead.

  • The bubble's text comes from content: attr(data-tooltip). When the attribute is empty (data-tooltip=""), the bubble is suppressed via :not([data-tooltip=""]) — a useful toggle pattern (blank the attribute to hide without removing the hook).

  • Default position is above the trigger, horizontally centred. Three alternative positions are available via the data-tooltip-position attribute:

    PositionAttribute
    top(default)
    bottomdata-tooltip-position="bottom"
    leftdata-tooltip-position="left"
    rightdata-tooltip-position="right"

    The helper is keyed off [data-tooltip] already (the bubble text comes from attr(data-tooltip)), so keeping position on the same axis keeps the API single-shape. Earlier releases shipped class hooks (.tooltip-bottom, .tooltip-left, .tooltip-right) — those have been removed; migrate to the attribute form.

  • z-index defaults to 1080 to sit above Bootstrap-style modals (1050) and popovers (1070) when frameworks are mixed.

  • The bubble uses pointer-events: none so it never intercepts clicks meant for content underneath.

Live

Four positions — hover or focus to reveal

<div style="display: flex; gap: 1.5rem; justify-content: center; padding: 3rem 1rem;">
<button class="button" data-tooltip="Default (top)">Top</button>
<button class="button" data-tooltip="Below the trigger" data-tooltip-position="bottom">Bottom</button>
<button class="button" data-tooltip="To the left" data-tooltip-position="left">Left</button>
<button class="button" data-tooltip="To the right" data-tooltip-position="right">Right</button>
</div>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'helper-tooltip', 'element-button')
);
Playground

Markup

markup
<!-- Default position: top -->
<button class="button" data-tooltip="Save the current draft">Save</button>

<!-- Below the trigger -->
<button class="button" data-tooltip="Permanently delete" data-tooltip-position="bottom">Delete</button>

<!-- To the left -->
<button class="button" data-tooltip="Previous step" data-tooltip-position="left">Back</button>

<!-- Hide the tooltip without removing the hook: blank the attribute -->
<button class="button" data-tooltip="">No tooltip yet</button>

Accessibility

  • Activated on both :hover and :focus-within, so keyboard users reach the tooltip by tabbing to the trigger (or any focusable descendant). Pointer users get the same content on hover.
  • The prefers-reduced-motion: reduce media query disables the opacity transition so the bubble appears instantly for users who opt out of motion.
  • The bubble is rendered via ::after and content: attr(...), which means assistive technologies that read pseudo-element content will announce it. If you need guaranteed screen-reader exposure regardless of AT pseudo-content support, also mirror the value into aria-describedby or aria-label on the trigger.
  • content: attr(...) for string values is supported across every evergreen browser in scope (per .browserslistrc), so no fallback is required. The wider CSS Values 5 attr() type-aware syntax is not used here.
  • The default position: relative on the trigger is intentionally not !important — a tooltip that silently broke an author's positioning would be worse than a tooltip that anchors to an ancestor.
  • For a tooltip that flips, shifts, or stays in the viewport on edge cases, you need a JS-positioned library such as Floating UI (the spiritual successor to Popper). The CSS-only helper is intentionally scoped to the static four-position case.

Variables

VariableDefaultNotes
$selector'[data-tooltip]'The selector the rule keys off. Rename to [data-hint] etc. if it conflicts with another library.
$bgrgba(0, 0, 0, 0.85)Bubble background fallback. Themes ship --kc-tooltip-bg to override per-theme.
$fg#fffBubble text-color fallback. Themes ship --kc-tooltip-fg to override per-theme.
$position-bottom-selector'[data-tooltip-position="bottom"]'Selector that flips the bubble below the trigger.
$position-left-selector'[data-tooltip-position="left"]'Selector that anchors the bubble to the inline-start edge.
$position-right-selector'[data-tooltip-position="right"]'Selector that anchors the bubble to the inline-end edge.
$padding0.4em 0.8emBubble inner padding. em-based so it scales with $font-size.
$border-radius4pxBubble corner radius.
$font-size0.8emBubble text size, relative to the trigger.
$offset0.5remDistance from the bubble to the trigger edge.
$z-index1080Matches Bootstrap's tooltip layer; sits above modals (1050) and popovers (1070).
$transition-duration120msFade-in / fade-out duration. Suppressed under prefers-reduced-motion: reduce.

Override example

app.scss
@use '@adnap/krysalicss/helper/tooltip' with (
  $selector:            '[data-tooltip]',
  $bg:                  rgba(20, 20, 24, 0.92),
  $fg:                  #fff,
  $padding:             0.5em 0.9em,
  $border-radius:       6px,
  $font-size:           0.85em,
  $offset:              0.6rem,
  $z-index:             1080,
  $transition-duration: 120ms,
);

/* Theme-switchable: ship per-theme tokens that the bubble picks up. */
:root {
  --kc-tooltip-bg: rgba(20, 20, 24, 0.92);
  --kc-tooltip-fg: #fff;
}
:root[data-theme="dark"] {
  --kc-tooltip-bg: rgba(245, 245, 250, 0.95);
  --kc-tooltip-fg: #111;
}

Tokens consumed

TokenUsed for
--kc-tooltip-bgBubble background. Falls back to $bg when undefined.
--kc-tooltip-fgBubble text color. Falls back to $fg when undefined.

Sizes and timings (padding, radius, offset, duration) are baked at SCSS-compile time — use the SCSS override above to change them.

Forced-colors fallback

Under forced-colors: active (Windows high-contrast and similar OS settings), the bubble switches to system Canvas / CanvasText colours with a 1 px CanvasText border. This keeps the tooltip legible against any user-imposed palette regardless of the framework's default ink-on-black bubble.