Tooltip

Hover or focus the buttons: Copy to clipboard Visible to everyone with the link

@use 'sprucecss/scss/spruce' as *;

.tooltip-demo {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: spacer('m');
  padding-block: spacer('xl');
}

.tooltip {
  display: inline-flex;
  position: relative;

  &__content {
    background-color: color('heading');
    border-radius: radius('sm');
    box-shadow: shadow('md');
    color: color('background');
    font-size: config('font-size-sm', $typography);
    inline-size: max-content;
    inset-block-end: calc(100% + #{spacer('xs')});
    inset-inline-start: 50%;
    line-height: config('line-height-md', $typography);
    max-inline-size: 16rem;
    opacity: 0;
    padding: spacer('xxs') spacer('xs');
    pointer-events: none;
    position: absolute;
    translate: -50% 0.25rem;
    visibility: hidden;
    z-index: z-index('tooltip');
    @include transition($property: 'opacity, translate, visibility');

    &::after {
      border: 0.375rem solid transparent;
      border-block-start-color: color('heading');
      content: '';
      inset-block-start: 100%;
      inset-inline-start: 50%;
      position: absolute;
      translate: -50% 0;
    }
  }

  &:hover &__content,
  &:focus-within &__content,
  &.is-open &__content {
    opacity: 1;
    translate: -50% 0;
    visibility: visible;
  }

  &.is-dismissed &__content {
    opacity: 0;
    visibility: hidden;
  }

  &--bottom &__content {
    inset-block: calc(100% + #{spacer('xs')}) auto;
    translate: -50% -0.25rem;

    &::after {
      border-block-end-color: color('heading');
      border-block-start-color: transparent;
      inset-block: auto 100%;
    }
  }
}

[dir='rtl'] .tooltip__content {
  translate: 50% 0.25rem;
}
<p class="tooltip-demo">
    Hover or focus the buttons:
    <span class="tooltip">
        <button class="btn btn--outline-primary btn--icon" type="button" aria-describedby="tooltip-copy">
            <svg class="btn__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
            <span class="sr-only">Copy</span>
        </button>
        <span class="tooltip__content" role="tooltip" id="tooltip-copy">Copy to clipboard</span>
    </span>
    <span class="tooltip tooltip--bottom">
        <button class="btn btn--primary" type="button" aria-describedby="tooltip-publish">Publish</button>
        <span class="tooltip__content" role="tooltip" id="tooltip-publish">Visible to everyone with the link</span>
    </span>
</p>
(() => {
  document.querySelectorAll('.tooltip').forEach((tooltip) => {
    const reset = () => tooltip.classList.remove('is-dismissed');

    tooltip.addEventListener('keydown', (event) => {
      if (event.key === 'Escape') {
        tooltip.classList.add('is-dismissed');
      }
    });

    tooltip.addEventListener('mouseleave', reset);
    tooltip.addEventListener('focusout', reset);
  });
})();

Tooltip

On this page

A small text label that describes a control on hover and on keyboard focus.

Technical Details

  • The trigger points to the tooltip with aria-describedby; the tooltip has role="tooltip". Icon-only buttons still need their own accessible name (a hidden text or aria-label).
  • It opens on hover and on focus (:focus-within), so keyboard users get it too.
  • The Esc key dismisses it without moving the focus (WCAG 1.4.13 Content on Hover or Focus); the script adds .is-dismissed until the pointer or focus leaves.
  • The tooltip itself is hoverable-safe: it never covers the trigger and doesn't capture pointer events.
  • Use .tooltip--bottom to place it below the trigger. Only put short, non-interactive text in a tooltip.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.