Tooltip
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:
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:
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>
);
}
<script setup lang="ts">
import { computed } from "vue";
import { tooltip, tooltipArrow } from "virtual:styleframe";
const props = withDefaults(
defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "solid" | "soft" | "subtle";
size?: "sm" | "md" | "lg";
label?: string;
}>(),
{},
);
const classes = computed(() =>
tooltip({
color: props.color,
variant: props.variant,
size: props.size,
}),
);
const arrowClasses = computed(() =>
tooltipArrow({
color: props.color,
variant: props.variant,
}),
);
</script>
<template>
<div class="tooltip-wrapper">
<span :class="classes" role="tooltip">
<slot>{{ props.label }}</slot>
</span>
<span :class="[arrowClasses, 'tooltip-arrow-position']" />
</div>
</template>
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
| Color | Token | Use Case |
|---|---|---|
light | @color.white / @color.gray-* | Light tooltips, stays light in dark mode |
dark | @color.gray-900 | Dark tooltips, stays dark in light mode (default) |
neutral | Adaptive (light ↔ dark) | Adapts to the current color scheme |
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
| Size | Font Size | Padding (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 |
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:
| Part | Recipe | Role |
|---|---|---|
| Content | useTooltipRecipe() | The tooltip bubble with background, border, text styling, and shadow |
| Arrow | useTooltipArrowRecipe() | 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>
Accessibility
- Use
role="tooltip"andaria-describedby. The tooltip element needsrole="tooltip"and a uniqueid. The trigger element references it witharia-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
solidvariant withdarkcolor 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:
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:
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;
API Reference
useTooltipRecipe(s, options?)
Creates the tooltip content bubble recipe with background, border, text styling, padding, and shadow.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the tooltip content |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | dark |
variant | solid, soft, subtle | solid |
size | sm, md, lg | md |
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:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the tooltip arrow |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | dark |
variant | solid, soft, subtle | solid |
Best Practices
- Pass
colorandvariantconsistently: Both the content and arrow recipes need the samecolorandvariantvalues so the arrow colors match the tooltip bubble. - Use
darkfor 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
maxWidthof240pxenforces 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
filteroption to reduce generated CSS. - Override defaults at the recipe level: Set your most common variant combination as
defaultVariantsso component consumers write less code.
FAQ
@tooltip.arrow.size CSS variable (default 5px) rather than discrete size variants. This gives you continuous control over the arrow dimensions and avoids coupling the arrow size to the tooltip content size. Override the variable to change the arrow size: s.variable('tooltip.arrow.size', '8px');dark default ensures high visibility out of the box. Use neutral if you want the tooltip to adapt to light and dark modes automatically.light always uses white and gray-100 backgrounds regardless of the color scheme. dark always uses gray-800 and gray-900 backgrounds. neutral adapts to the current color scheme: it appears light in light mode and dark in dark mode. Use neutral when you want the tooltip to blend naturally with the surrounding interface.subtle also adds a matching border, giving the tooltip more visual definition. Use soft when you want a borderless, gentler appearance, and subtle when the tooltip needs slightly more structure.@tooltip.arrow.size CSS variable with a default value of 5px. Override it by calling s.variable('tooltip.arrow.size', '8px') after registering the recipe, or by targeting the variable in your CSS. Both the outer border (used for the border color) and the inner pseudo-element (used for the background fill) reference this variable.The Tooltip recipe uses compound variants to map each color-variant combination to specific styles. For the 3 colors and 3 variants, 9 compound variant entries define the background, text color, and border color — each with dark mode overrides via the &:dark selector. The arrow recipe uses matching compound variants so its border-triangle colors align with the content bubble.
@color.white, @font-size.sm, and @border-radius.md through string refs. These tokens need to be defined in your Styleframe instance for the recipe to generate valid CSS. The easiest way is to use useDesignTokensPreset(s), but you can also define the required tokens manually.Modal
A dialog component for focused interactions, composed of overlay, container, header, body, and footer sections. Supports multiple colors, visual styles, and sizes through the recipe system.
Overview
Explore Styleframe's utility composables for generating CSS utility classes. Create flexible, reusable styling primitives with full type safety.