Tooltip: the smallest component
components/react-tooltip in the frame_demo subtheme. Wraps the page title in a react-tippy tooltip. It is the minimal reference for the pattern.
Files
react-tooltip/
react-tooltip.component.yml # prop `text`, slot `content`, libraryOverrides
react-tooltip.twig # <span class="react-tooltip" data-text="..."> + slot
index.jsx # behavior: reads data-text and innerText, renders TextWithTooltip
TextWithTooltip.jsx # <Tooltip html={...}>{content}</Tooltip>
react-tooltip.scss # Umami look for the bubble
__tests__/react-tooltip.test.js # snapshot testFull source
That is the whole component. Click the tabs.
yaml
$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
name: React Tooltip
status: stable
description: 'A React-based tooltip shown on hover. The trigger is an icon, a short text, or anything placed in the content slot.'
group: Interactive
props:
type: object
properties:
text:
type: string
title: Tooltip Text
description: 'The text to display in the tooltip'
default: 'Tooltip text'
examples:
- 'This is the tooltip text'
label:
type: string
title: Trigger text
description: 'Simple text that shows the tooltip on hover. Leave empty to use only the icon or the content slot.'
icon:
type: string
title: Trigger icon
description: 'Shown before the trigger text.'
# The builder shows an icon picker; every value is the name of a Bootstrap Icons glyph.
x-builder-widget: icon
x-builder-icon-class: 'bi bi-{name}'
enum:
- info-circle
- question-circle
- exclamation-triangle
- lightbulb
- star
- heart
- check-circle
- x-circle
- bell
- bookmark
- calendar
- clock
- envelope
- telephone
- geo-alt
- link-45deg
- download
- search
- gear
- person
- cart
- tag
- chat
- globe
required:
- text
slots:
content:
title: Content
description: 'The content that triggers the tooltip when hovered'
examples:
- 'Hover me to see the tooltip'
# Page builder: the icon in the list of blocks. Features (spacing, position...) follow the theme; a component can
# switch one off or on here: `features: { background: false }`.
thirdPartySettings:
frame_builder:
icon: tooltip
libraryOverrides:
js:
../../assets/react-tooltip.js: { attributes: { type: module } }
css:
theme:
../../assets/react-tooltip.css: { }
dependencies:
- frame/reacttwig
{# The icon and the label are printed by Twig too: the trigger is readable before React mounts and without JavaScript. #}
<span {{ attributes.addClass('react-tooltip').setAttribute('data-text', text).setAttribute('data-icon', icon|default(false)) }}>
{%- if icon %}<i class="bi bi-{{ icon }}" aria-hidden="true"></i> {% endif -%}
{{- label -}}
{%- block content %}{% endblock -%}
</span>jsx
import 'react-tippy/dist/tippy.css';
// Icon font for the `icon` prop.
import 'bootstrap-icons/font/bootstrap-icons.css';
import {TextWithTooltip} from './TextWithTooltip';
import './react-tooltip.scss'
import { createRoot } from 'react-dom/client';
// Props of the SDC (`text`, `icon`) arrive as data-* attributes on the mount element, the trigger text (the `label`
// prop and the content slot) as its text.
(function (Drupal, once) {
const attachTooltip = (element) => {
createRoot(element).render(
<TextWithTooltip text={element.dataset.text} icon={element.dataset.icon} content={element.innerText.trim()}/>
);
};
Drupal.behaviors.reactTooltip = {
attach(context) {
once('react', '.react-tooltip', context)
.forEach((element) => executeWhenVisible(element, attachTooltip, "tooltip"))
},
};
})(Drupal, once);jsx
import { Tooltip } from "react-tippy";
export const TextWithTooltip = ({ text, content, icon }) => (
<Tooltip
html={<span className="react-tooltip__text">{text}</span>}
className="react-tooltip__container"
position="right"
delay={100}
animateFill={false}
>
{icon && <i className={`bi bi-${icon}`} aria-hidden="true" />}
{icon && content ? ' ' : null}
{content}
</Tooltip>
);scss
.react-tooltip {
margin-left: .4rem;
margin-right: .4rem;
&__container {
display: flex;
max-width: 20rem;
line-height: 1.5;
padding: .5rem;
}
}
// Umami look for the react-tippy bubble.
.tippy-popper .tippy-tooltip.dark-theme {
background-color: #008068;
font-family: "Source Sans Pro", Verdana, sans-serif;
font-size: .95rem;
border-radius: .35rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, .25);
.tippy-arrow {
border-top-color: #008068;
border-bottom-color: #008068;
border-left-color: #008068;
border-right-color: #008068;
}
}Used from PHP
frame_demo.theme wraps the page title in the component, so every page title gets the tooltip:
php
$variables['title'] = [
'#type' => 'component',
'#component' => 'frame_demo:react-tooltip',
'#props' => ['text' => 'Tooltip text'],
'#slots' => ['content' => $title],
];Hover the title on /recipe-explorer to see it:

react-tippy
animateFill={false} is set because tippy's fill animation paints a dark circle over a themed bubble background.