Tooltip

Small pop-up element that provides additional information when users hover or focus on an element.

  • Default

    Tooltips are tricky because of all the accessibility concerns. We recommend using a 3rd party solution like Tippy.js because it takes care of the anchoring, a11y, and keeps the tooltip inside the viewport.

    <div class="gap-4 grid items-center grid-cols-2 [&_[data-tippy-root]]:inline-block md:grid-cols-4"> <!-- <- this is just for demo purposes -->
      <div>
        <div data-tippy-root>
          <div class="tippy-box" data-placement="top">
            <div class="tippy-backdrop"></div>
            <div class="tippy-arrow"></div>
            <div class="tippy-content">Top</div>
          </div>
        </div>
        <div class="mt-1 pl-4 text-sm">Top</div> <!-- this is for demo purposes -->
      </div>
      <div>
        <div class="mb-1 pl-4 text-sm">Bottom</div> <!-- this is for demo purposes -->
        <div data-tippy-root>
          <div class="tippy-box" data-placement="bottom">
            <div class="tippy-backdrop"></div>
            <div class="tippy-arrow"></div>
            <div class="tippy-content">Bottom</div>
          </div>
        </div>
      </div>
      <div>
        <div data-tippy-root>
          <div class="tippy-box" data-placement="left">
            <div class="tippy-backdrop"></div>
            <div class="tippy-arrow"></div>
            <div class="tippy-content">Left</div>
          </div>
        </div>
        <span class="ml-2 text-sm">Left</span> <!-- this is for demo purposes -->
      </div>
      <div>
        <span class="mr-2 text-sm">Right</span> <!-- this is for demo purposes -->
        <div data-tippy-root>
          <div class="tippy-box" data-placement="right">
            <div class="tippy-backdrop"></div>
            <div class="tippy-arrow"></div>
            <div class="tippy-content">Right</div>
          </div>
        </div>
      </div>
    </div>
    
  • Example

    The following examples are all using Alpine.js with Tippy.js used as a directive with optional $magic.

    <button class="prs-btn prs-btn-primary" x-tooltip="'Tooltip'">Hover/Focus for Tooltip</button>
    <button class="prs-btn prs-btn-primary" @mouseenter="$tooltip('Tooltip')">Hover Only</button>
    <button class="prs-btn prs-btn-primary" @focus="$tooltip('Tooltip')">Focus Only</button>
    
    import Alpine from 'alpinejs'
    import tippy from 'tippy.js'
    
    document.addEventListener('alpine:init', () => {
      Alpine.magic('tooltip', el => message => {
        let instance = tippy(el, {
          content: message,
          trigger: 'manual',
        });
    
        // Add Escape handler *before* showing the tooltip
        const onEscape = (e) => {
          if (e.key === 'Escape') cleanup();
        };
    
        const cleanup = () => {
          instance.hide();
          setTimeout(() => instance.destroy(), 150);
          document.removeEventListener('keydown', onEscape);
          el.removeEventListener('mouseleave', cleanup);
          el.removeEventListener('blur', cleanup);
        };
    
        document.addEventListener('keydown', onEscape);
        el.addEventListener('mouseleave', cleanup);
        el.addEventListener('blur', cleanup);
    
        instance.show();
    
        // Optional fallback timeout
        setTimeout(cleanup, 2000);
      });
    
      Alpine.directive('tooltip', (el, { expression }, { evaluate }) => {
        const instance = tippy(el, {
          content: evaluate(expression),
        });
    
        const onEscape = (e) => {
          if (e.key === 'Escape') {
            instance.hide();
          }
        };
    
        document.addEventListener('keydown', onEscape);
    
        el._tippyCleanup = () => {
          document.removeEventListener('keydown', onEscape);
          instance.destroy();
        };
      });
    
      document.addEventListener('alpine:clean', (e) => {
        const el = e.target;
        if (el._tippyCleanup) {
          el._tippyCleanup();
          delete el._tippyCleanup;
        }
      });
    });
    

CSS

Full Library

Component Only

/* tippy.js tooltips */
.tippy-box {
  background-color: var(--prs-c-gray-900);
  color: var(--prs-c-white);
  white-space: normal;
  outline: 0;
  position: relative;
  opacity: 1;
  box-shadow: 0 0 0 1px rgb(255 255 255), 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -1px rgb(0 0 0 / 0.06);
  border-radius: 0.25rem;
  transform: translateY(0);
  transition-property: var(--prs-transition-property);
  transition-timing-function: var(--prs-transition-timing);
  transition-duration: var(--prs-transition-duration);

  &[data-state="hidden"] {
    opacity: 0;
    transform: translateY(0.25rem);
  }

  &[data-placement^="top"],&[data-placement^="bottom"],&[data-placement^="left"],&[data-placement^="right"] {
    > .tippy-arrow {
      width: 1rem;
      height: 1rem;
      color: var(--prs-c-gray-900);
      position: absolute;

      &::before {
        content: '';
        position: absolute;
        border-style: solid;
        border-color: transparent;
      }
    }
  }

  &[data-placement^="top"] > .tippy-arrow {
    filter: drop-shadow(0 1px 0 rgb(255 255 255));
    bottom: 0;
    transform: translateX(-50%);
    left: 50%;
    transform-origin: top;

    &::before {
      bottom: -7px;
      left: 0;
      border-top: 8px solid currentColor;
      border-right: 8px solid transparent;
      border-bottom: 0;
      border-left: 8px solid transparent;
    }
  }

  &[data-placement^="bottom"] > .tippy-arrow {
    filter: drop-shadow(0 -1px 0 rgb(255 255 255));
    top: 0;
    transform: translateX(-50%);
    left: 50%;
    transform-origin: bottom;

    &::before {
      top: -7px;
      left: 0;
      border-top: 0;
      border-right: 8px solid transparent;
      border-bottom: 8px solid currentColor;
      border-left: 8px solid transparent;
    }
  }

  &[data-placement^="left"] > .tippy-arrow {
    right: 0;
    filter: drop-shadow(1px 0 0 rgb(255 255 255));
    transform: translateY(-50%);
    top: 50%;
    transform-origin: left;

    &::before {
      right: -7px;
      border-top: 8px solid transparent;
      border-right: 0;
      border-bottom: 8px solid transparent;
      border-left: 8px solid currentColor;
    }
  }

  &[data-placement^="right"] > .tippy-arrow {
    left: 0;
    filter: drop-shadow(-1px 0 0 rgb(255 255 255));
    transform: translateY(-50%);
    top: 50%;
    transform-origin: right;

    &::before {
      left: -7px;
      border-top: 8px solid transparent;
      border-right: 8px solid currentColor;
      border-bottom: 8px solid transparent;
      border-left: 0;
    }
  }
}

[data-tippy-root] {
  max-width: calc(100vw - 10px);
}

.tippy-content {
  padding: 0.5rem 1rem;
  font-size: 16px;
  line-height: 24px;
  position: relative;
  z-index: 1;

  @media (min-width: 768px) {
    padding: 0.25rem 1rem;
    font-size: 12px;
    line-height: 18px;
  }
}

Coming soon...