A floating label to explain additional context in the UI. They're triggered by interaction events like hover, focus, tap, or click.

Importing

The component can be imported via:

import { Tooltip, TooltipContent, TooltipGroup } from '@customerio/pluma-components/react';

Usage

We use Floating UI as the main library for positioning the tooltips, and attaching the interactions.

At its most simple, a tooltip can be invoked like this:

<Tooltip content="I am the tooltip content">
 <Text>Hover me</Text>
</Tooltip>

The tooltip "trigger" will render as an unstyled button tag, to make it focusable and visible to assistive technology.

When the tooltip is attached to text, the isUnderlined prop can be used to add a bottom border to it, to draw attention:

<Tooltip content="I am the tooltip content" isUnderlined={true}>
 <Text>Hover me</Text>
</Tooltip>

Placement

The component accepts a placement argument, which accepts one of these values:

type Placement =
 | 'top'
 | 'top-start'
 | 'top-end'
 | 'right'
 | 'right-start'
 | 'right-end'
 | 'bottom'
 | 'bottom-start'
 | 'bottom-end'
 | 'left'
 | 'left-start'
 | 'left-end';
<Box display="flex" gap="500">
 <Tooltip content="I am the tooltip content" placement="top">
	<Text>Hover me</Text>
 </Tooltip>

 <Tooltip content="I am the tooltip content" placement="top-start">
	<Text>Hover me</Text>
 </Tooltip>

 <Tooltip content="I am the tooltip content" placement="right">
	<Text>Hover me</Text>
 </Tooltip>
</Box>

Delay

By default, the tooltip will show/hide after a 250ms delay after receiving focus/hovering. This can be changed with the floatingPluginOptions prop, by customizing the hover settings:

<Tooltip content="I am the tooltip content" placement="top" floatingPluginOptions={{ hover: { delay: 0 } }}>
 <Text>Open immediately</Text>
</Tooltip>

Additionally, an object can be provided with separate open and close delays, when we'd like to control the delays before opening/closing more precisely:

<Tooltip
 content="I am the tooltip content"
 placement="top"
 floatingPluginOptions={{
	hover: {
	 delay: { open: 0, close: 1000 },
	},
 }}
>
	<Text>Open immediately, close after delay</Text>
</Tooltip>

Tooltip groups

The PlumaTooltipGroup component can be used to group multiple components together, and have them sync their open/close delays. Once one tooltip in a group is open, hovering over another tooltip trigger in the same group will open it immediately, instead of having to wait for another open delay.

Only one tooltip in a group can be open at a time.

<TooltipGroup delay={{ open: 750, close: 500 }}>
	<InlineStack gap="100">
		<Tooltip 
			content="I am the first tooltip" 
			isUnderlined={true}
			placement="top"
		>
			<Text>First trigger</Text>
		</Tooltip>

		<Tooltip
			content="I am the second tooltip"
			isUnderlined={true}
			placement="top"
		>
			<Text>Second trigger</Text>
		</Tooltip>

		<Tooltip
			content="I am the third tooltip"
			isUnderlined={true}
			placement="top"
		>
			<Text>Third trigger</Text>
		</Tooltip>
	</InlineStack>
</TooltipGroup>

Custom tooltip trigger

As mentioned above, the tooltip trigger will render as a button, which may sometimes cause the layout to break. In case more customization is needed, the component accepts an isCustom prop, which will prevent it from rendering any containing elements.

When using custom tooltips, the rendered content receives a function, which contains Floating UI props/styles. You will need to forward/spread those props to your custom trigger element in order for all interactions to connect:

Hover me
<Tooltip content="I am the tooltip content" placement="right" isCustom={true}>
 {(context) => (
	<Label color="green" ref={context.reference.setReference} {...context.reference.getReferenceProps()}>
	 Hover me
	</Label>
 )}
</Tooltip>

Custom tooltip content

A tooltip's content is meant to be text-only, and it shouldn't contain any interactive elements, like links.

While not recommended, it's possible to customize the content of the tooltip as well, by not providing a content prop, and instead using the TooltipContent component. This works for both isCustom and standard tooltips.

Hover me
<Box display="flex" gap="500">
	<Tooltip>
		Hover me

		<TooltipContent>
			I can render anything, even a <Button>button</Button>
		</TooltipContent>
	</Tooltip>

	<Tooltip isCustom={true}>
		{(context) => (
			<>
				<Label color="green" ref={context.reference.setReference} {...context.reference.getReferenceProps()}>
					Hover me
				</Label>
				<TooltipContent>
					I can render anything, even a <Button>button</Button>
				</TooltipContent>
			</>
		)}
	</Tooltip>
</Box>

Note: it is not necessary to call setFloating or spread any props on the content component - it does so automatically by getting its props and refs from context.

Conditional tooltips

The isDisabled prop can be used to conditionally enable/disable a tooltip. When isDisabled is true, the tooltip will not show up on hover/focus.

import { Tooltip } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [isDisabled, setIsDisabled] = useState(false);

	return (
		<>
			<Tooltip content="I am the tooltip content" isDisabled={isDisabled}>
				<Text>Hover me</Text>
			</Tooltip>{' '}
			<Button onClick={() => setIsDisabled(!isDisabled)}>{isDisabled ? 'Enable' : 'Disable'}</Button>
		</>
	);
}

Custom middleware

In addition to the existing middlewareOptions prop for customizing the behavior of the tooltip, it's possible to provide entire custom middleware functions, which can be used for more complex scenarios or special use cases.

For example, we can create a middleware that changes the position of the tooltip's arrow:

import { Tooltip } from '@customerio/pluma-components/react';

const arrowMiddleware = {
	name: 'customArrowMiddleware',
	fn: (state) => {
		if (state.middlewareData?.arrow?.x != null) {
			state.middlewareData.arrow.x = 2;
		}

		return state;
	}
}

export default function Example() {
	return (
		<Tooltip content="I am the tooltip content" additionalMiddleware={[arrowMiddleware]}>
			<Text>Hover me</Text>
		</Tooltip>
	);
}

API

Default:125

The duration (in ms) for the tooltip's show/hide animation.

The text content to show in the tooltip

Default:false

Whether the tooltip should be open on first render

Whether the tooltip will be applied to a custom element

Default:false

Whether the tooltip should be disabled. Can be used to conditionally enable/disable a tooltip depending on state.

Default:true

Whether to apply a flex style to the trigger (by default it's inline-flex)

Whether the tooltip should be open. Only use this along with onOpenChange if you want to control the state of the tooltip

Whether the trigger element should show a bottom border for emphasis

middlewareOptions  { arrow?: Partial<{ padding?: Padding | undefined; element: Element; }> | ((state: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; }) => Partial<{ padding?: Padding | undefined; element: Element; }>) | undefined; offset?: Partial<{ mainAxis: number; crossAxis: number; alignmentAxis: number | null; }> | ((state: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; }) => Partial<{ mainAxis: number; crossAxis: number; alignmentAxis: number | null; }>) | undefined; flip?: { padding?: Padding | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; mainAxis?: boolean | undefined; crossAxis?: boolean | undefined; fallbackPlacements?: Placement[] | undefined; fallbackStrategy?: "initialPlacement" | "bestFit" | undefined; fallbackAxisSideDirection?: "none" | "end" | "start" | undefined; flipAlignment?: boolean | undefined; boundary?: Boundary | undefined; } | ((state: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; }) => { padding?: Padding | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; mainAxis?: boolean | undefined; crossAxis?: boolean | undefined; fallbackPlacements?: Placement[] | undefined; fallbackStrategy?: "initialPlacement" | "bestFit" | undefined; fallbackAxisSideDirection?: "none" | "end" | "start" | undefined; flipAlignment?: boolean | undefined; boundary?: Boundary | undefined; }) | undefined; shift?: { padding?: Padding | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; mainAxis?: boolean | undefined; crossAxis?: boolean | undefined; limiter?: { fn: (state: MiddlewareState) => Coords; options?: any; } | undefined; boundary?: Boundary | undefined; } | ((state: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; }) => { padding?: Padding | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; mainAxis?: boolean | undefined; crossAxis?: boolean | undefined; limiter?: { fn: (state: MiddlewareState) => Coords; options?: any; } | undefined; boundary?: Boundary | undefined; }) | undefined; hide?: { padding?: Padding | undefined; strategy?: "referenceHidden" | "escaped" | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; boundary?: Boundary | undefined; } | ((state: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; }) => { padding?: Padding | undefined; strategy?: "referenceHidden" | "escaped" | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; boundary?: Boundary | undefined; }) | undefined; } | undefined

Additional options to pass into Floating UI middleware

Called when the open state of the tooltip is changing

Default:bottom

Where the tooltip should be placed in relation to the trigger

Default:absolute

The position CSS property to use on the floating element

Additional classes to apply on the tooltip component (the floating element, not the trigger). Should only be used as a last resort

Additional inline styles to apply on the tooltip component (the floating element, not the trigger). Should only be used as a last resort

Default:true

Whether the tooltip content should show an arrow.