Composables

Tooltip

A floating label component for supplementary information, composed of a content bubble and directional arrow. Supports multiple colors, visual styles, and sizes through the recipe system.

Overview

The Tooltip is a floating label element used for supplementary context shown on hover or focus. It is composed of two recipe parts: useTooltipRecipe() for the content bubble and useTooltipArrowRecipe() for the directional arrow. Each composable creates a fully configured recipe with color and variant options — plus compound variants that handle the color-variant combinations automatically. The content recipe adds a size axis for font size and padding control, while the arrow recipe uses a CSS variable for dimension control.

The Tooltip recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS.

Why use the Tooltip recipe?

The Tooltip recipe helps you:

  • Ship faster with sensible defaults: Get 3 colors, 3 visual styles, and 3 sizes out of the box with a pair of composable calls.
  • Compose coordinated parts: Two recipes (content and arrow) share the same color and variant axes, so your tooltips stay internally consistent.
  • Maintain consistency: Compound variants ensure every color-variant combination follows the same design rules, including dark mode overrides.
  • Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
  • Stay type-safe: Full TypeScript support means your editor catches invalid color, variant, or size values at compile time.
  • Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.

Usage

Register the recipes

Add the Tooltip recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:

src/components/tooltip.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import {
    useTooltipRecipe,
    useTooltipArrowRecipe,
} from '@styleframe/theme';

const s = styleframe();

const tooltip = useTooltipRecipe(s);
const tooltipArrow = useTooltipArrowRecipe(s);

export default s;

Build the component

Import the tooltip and tooltipArrow runtime functions from the virtual module and pass variant props to compute class names:

src/components/Tooltip.tsx
import { tooltip, tooltipArrow } from "virtual:styleframe";

interface TooltipProps {
    color?: "light" | "dark" | "neutral";
    variant?: "solid" | "soft" | "subtle";
    size?: "sm" | "md" | "lg";
    label?: string;
    children?: React.ReactNode;
}

export function Tooltip({
    color = "dark",
    variant = "solid",
    size = "md",
    label,
    children,
}: TooltipProps) {
    return (
        <div className="tooltip-wrapper">
            <span
                className={tooltip({ color, variant, size })}
                role="tooltip"
            >
                {children ?? label}
            </span>
            <span className={`${tooltipArrow({ color, variant })} tooltip-arrow-position`} />
        </div>
    );
}

See it in action

Colors

The Tooltip recipe includes 3 color variants: light, dark, and neutral. Like the Card recipe, the Tooltip uses neutral-spectrum colors designed for supplementary surfaces rather than status communication. Each color is combined with every visual style variant through compound variants, so you get consistent, predictable styling across all combinations — including dark mode overrides.

The default color is dark, which provides strong contrast against most page backgrounds and is the most common tooltip style across interfaces.

Color Reference

ColorTokenUse Case
light@color.white / @color.gray-*Light tooltips, stays light in dark mode
dark@color.gray-900Dark tooltips, stays dark in light mode (default)
neutralAdaptive (light ↔ dark)Adapts to the current color scheme
Pro tip: Use dark as your default tooltip color. Dark tooltips provide strong contrast against most page backgrounds, making the supplementary text easy to read at a glance.

Variants

Three visual style variants control how the tooltip is rendered. Each variant is combined with the selected color through compound variants, so you always get the correct background, text, and border colors for your chosen color.

Solid

Filled background with a subtle border. The most prominent style, ideal for standard tooltips that need clear visibility.

Soft

Light tinted background with no visible border. A gentler style that works well for tooltips in dense layouts where a bordered tooltip would feel heavy.

Subtle

Light tinted background with a matching border. Combines the softness of the soft variant with added visual definition from a border.

Sizes

Three size variants from sm to lg control the font size, padding, and border radius of the tooltip content.

Size Reference

SizeFont SizePadding (V / H)Border Radius
sm@font-size.xs@0.25 / @0.5@border-radius.sm
md@font-size.sm@0.375 / @0.625@border-radius.md
lg@font-size.md@0.5 / @0.75@border-radius.md
Good to know: The size prop only applies to useTooltipRecipe(). The arrow recipe (useTooltipArrowRecipe()) does not have a size variant — its dimensions are controlled by the @tooltip.arrow.size CSS variable (default: 5px).

Anatomy

The Tooltip recipe is composed of two independent recipes that work together to form a complete tooltip:

PartRecipeRole
ContentuseTooltipRecipe()The tooltip bubble with background, border, text styling, and shadow
ArrowuseTooltipArrowRecipe()Directional indicator using CSS border-triangle technique

The color and variant props should be passed consistently to both recipes so that the arrow colors match the tooltip bubble. The arrow recipe registers a @tooltip.arrow.size CSS variable (default 5px) that controls the arrow dimensions.

<!-- Both parts working together -->
<div class="tooltip-wrapper">
    <span class="tooltip(...)" role="tooltip">Tooltip text</span>
    <span class="tooltipArrow(...) tooltip-arrow-position" />
</div>
Pro tip: The arrow positioning (top, bottom, left, right) is not part of the recipe. Use your positioning library (e.g., Floating UI) or custom CSS to place the arrow relative to the tooltip content and its trigger element.

Accessibility

  • Use role="tooltip" and aria-describedby. The tooltip element needs role="tooltip" and a unique id. The trigger element references it with aria-describedby.
<!-- Correct: trigger linked to tooltip via aria-describedby -->
<button aria-describedby="tooltip-1">Hover me</button>
<div role="tooltip" id="tooltip-1" class="...">Helpful description</div>
  • Show on focus, not just hover. Tooltips must be accessible via keyboard. Show the tooltip when the trigger receives focus, and hide it on blur.
<!-- Correct: tooltip shown on both hover and focus -->
<button
    aria-describedby="tooltip-2"
    onfocus="showTooltip()"
    onblur="hideTooltip()"
    onmouseenter="showTooltip()"
    onmouseleave="hideTooltip()">
    Hover or focus me
</button>
  • Allow dismissal with Escape. Users should be able to dismiss the tooltip by pressing Escape without moving focus (WCAG 1.4.13).
  • Keep content concise. Tooltips are for supplementary information, not essential content. If the information is critical, use an inline label or a callout instead.
  • Verify contrast ratios. The solid variant with dark color places light text on a dark background. Default tokens meet WCAG AA 4.5:1 contrast. If you override colors, verify with the WebAIM Contrast Checker.

Customization

Overriding Defaults

Each tooltip composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change:

src/components/tooltip.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import {
    useTooltipRecipe,
    useTooltipArrowRecipe,
} from '@styleframe/theme';

const s = styleframe();

const tooltip = useTooltipRecipe(s, {
    base: {
        borderRadius: '@border-radius.lg',
        maxWidth: '320px',
    },
    defaultVariants: {
        color: 'neutral',
        variant: 'subtle',
        size: 'lg',
    },
});

const tooltipArrow = useTooltipArrowRecipe(s, {
    defaultVariants: {
        color: 'neutral',
        variant: 'subtle',
    },
});

export default s;

Filtering Variants

If you only need a subset of the available variants, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:

src/components/tooltip.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useTooltipRecipe, useTooltipArrowRecipe } from '@styleframe/theme';

const s = styleframe();

// Only generate dark color with solid style
const tooltip = useTooltipRecipe(s, {
    filter: {
        color: ['dark'],
        variant: ['solid'],
    },
});

const tooltipArrow = useTooltipArrowRecipe(s, {
    filter: {
        color: ['dark'],
        variant: ['solid'],
    },
});

export default s;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

API Reference

useTooltipRecipe(s, options?)

Creates the tooltip content bubble recipe with background, border, text styling, padding, and shadow.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the tooltip content
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.compoundVariantsCompoundVariant[]Custom compound variant definitions for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
colorlight, dark, neutraldark
variantsolid, soft, subtlesolid
sizesm, md, lgmd

useTooltipArrowRecipe(s, options?)

Creates the tooltip arrow recipe using a CSS border-triangle technique with a pseudo-element for the inner fill. Registers the @tooltip.arrow.size CSS variable (default: 5px).

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the tooltip arrow
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.compoundVariantsCompoundVariant[]Custom compound variant definitions for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
colorlight, dark, neutraldark
variantsolid, soft, subtlesolid

Learn more about recipes →

Best Practices

  • Pass color and variant consistently: Both the content and arrow recipes need the same color and variant values so the arrow colors match the tooltip bubble.
  • Use dark for general-purpose tooltips: Dark tooltips provide strong contrast against most page backgrounds, making them the most readable default.
  • Keep tooltip text short: Tooltips should provide brief supplementary context, not paragraphs of content. The default maxWidth of 240px enforces reasonable line lengths.
  • Never put essential information in tooltips: Tooltips are hidden by default and inaccessible on touch devices. Critical information should be visible inline.
  • Use a positioning library for placement: The recipe handles visual styling only. Use Floating UI or a similar library for dynamic positioning and collision detection.
  • Filter what you don't need: If your application only uses dark tooltips, pass a filter option to reduce generated CSS.
  • Override defaults at the recipe level: Set your most common variant combination as defaultVariants so component consumers write less code.

FAQ