Skip to main content
On this pageOverview

Tooltip

Overview

A non-interactive floating label anchored to a trigger. Tooltips appear on hover after a short delay, or immediately on keyboard focus. They hide on pointer-leave, blur, Escape, or left-click of the trigger. Use tooltips for short hints about a control. For rich content or interactive panels, use Popover instead.

The positioning engine is shared with Popover and Menu. Pass anchor to control placement and spacing.

See it in an app

Check out how Tooltip is wired up in a real Foldkit app.

Examples

Hover or tab into the trigger to reveal the tooltip. Hover waits for showDelay (default 500ms); keyboard focus shows it immediately.

// Pseudocode walkthrough of the Foldkit integration points. Each labeled
// block below is an excerpt. Fit them into your own Model, init, Message,
// update, and view definitions.
import { Match as M, Option } from 'effect'
import { Command, Ui } from 'foldkit'
import { html } from 'foldkit/html'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'

// Add a field to your Model for the Tooltip Submodel:
const Model = S.Struct({
  tooltip: Ui.Tooltip.Model,
  // ...your other fields
})

// In your init function, initialize the Tooltip Submodel with a unique id:
const init = () => [
  {
    tooltip: Ui.Tooltip.init({ id: 'save-button' }),
    // ...your other fields
  },
  [],
]

// Embed the Tooltip Message in your parent Message:
const GotTooltipMessage = m('GotTooltipMessage', {
  message: Ui.Tooltip.Message,
})

// Inside your update function's M.tagsExhaustive({...}), delegate to
// Ui.Tooltip.update. The OutMessages `Shown` and `Hidden` mark the
// visibility transitions. Fire analytics or coordinate with the rest
// of your UI from the parent.
GotTooltipMessage: ({ message }) => {
  const [nextTooltip, commands, maybeOutMessage] = Ui.Tooltip.update(
    model.tooltip,
    message,
  )
  const mappedCommands = Command.mapMessages(commands, message =>
    GotTooltipMessage({ message }),
  )

  return Option.match(maybeOutMessage, {
    onNone: () => [evo(model, { tooltip: () => nextTooltip }), mappedCommands],
    onSome: M.type<Ui.Tooltip.OutMessage>().pipe(
      M.tagsExhaustive({
        Shown: () => [
          // The child has emitted `Shown`. The body commits the
          // child's next state as usual. In this arm the parent can
          // also update its own state or dispatch its own Commands,
          // for example log analytics, prefetch content, or trigger
          // a downstream Command.
          evo(model, { tooltip: () => nextTooltip }),
          mappedCommands,
        ],
        Hidden: () => [
          // The child has emitted `Hidden`. The body commits the
          // child's next state as usual. In this arm the parent can
          // also update its own state or dispatch its own Commands,
          // for example clear ephemeral state, fire analytics, or
          // trigger a downstream Command.
          evo(model, { tooltip: () => nextTooltip }),
          mappedCommands,
        ],
      }),
    ),
  })
}

// Inside your view function, embed the tooltip via h.submodel:
const view = () => {
  const h = html<Message>()

  return h.submodel({
    slotId: 'save-button',
    model: model.tooltip,
    view: Ui.Tooltip.view,
    viewInputs: {
      anchor: { placement: 'top', gap: 6, padding: 8 },
      toView: ({ trigger, panel, isVisible }) =>
        h.div(
          [h.Class('relative inline-block')],
          [
            h.button(
              [
                ...trigger,
                h.Class('rounded-lg border px-3 py-2 cursor-pointer'),
              ],
              [h.span([], ['Save'])],
            ),
            ...(isVisible
              ? [
                  h.div(
                    [
                      ...panel,
                      h.Class(
                        'rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white shadow-lg',
                      ),
                    ],
                    [h.span([], ['Save your changes (⌘S)'])],
                  ),
                ]
              : []),
          ],
        ),
    },
    toParentMessage: message => GotTooltipMessage({ message }),
  })
}

Styling

Tooltip is headless. The toView callback receives attribute bundles for the trigger and panel, and the consumer composes the markup. The panel is rendered with pointer-events: none so it never captures hover or clicks, which keeps the open/close logic tied to the trigger.

AttributeCondition
data-openPresent on trigger and panel when the tooltip is visible.
data-disabledPresent on the trigger when disabled.

Keyboard Interaction

KeyDescription
EscapeHides the tooltip while visible. It will not reopen until the user disengages by moving the pointer away or blurring the trigger.

Accessibility

The panel has role="tooltip" and the trigger is linked via aria-describedby. Focus is never moved into the tooltip, so assistive technology announces the panel contents as a description of the trigger.

API Reference

InitConfig

Configuration object passed to Tooltip.init().

NameTypeDefaultDescription
idstring-Unique ID for the tooltip instance.
showDelayDuration.InputDuration.millis(500)How long the pointer must hover before the tooltip appears. Accepts any Effect Duration input. A bare number is interpreted as milliseconds. Keyboard focus shows the tooltip immediately regardless of this value.

ViewConfig

Configuration object passed to Tooltip.view().

NameTypeDefaultDescription
modelTooltip.Model-The tooltip state from your parent Model.
toParentMessage(childMessage: Tooltip.Message) => ParentMessage-Wraps Tooltip Messages in your parent Message type for Submodel delegation.
anchorAnchorConfig-Floating positioning config: placement, gap, and padding. Required.
toView(render: RenderInfo) => Html-Callback that receives the `trigger` and `panel` attribute bundles plus a derived `isVisible` flag, and returns the composed layout.
isDisabledbooleanfalseDisables the trigger. Hover, focus, and keyboard events are ignored and the tooltip will not open.

RenderInfo

Payload delivered to the toView callback each render.

NameTypeDefaultDescription
triggerReadonlyArray<ChildAttribute>-Spread onto the trigger element. Carries `type="button"`, the hover/focus/keyboard handlers, and `aria-describedby` linking to the panel.
panelReadonlyArray<ChildAttribute>-Spread onto the panel element. Carries `role="tooltip"`, the anchor Mount that positions the panel via Floating UI, and a `data-open` attribute when visible.
isVisibleboolean-Whether the tooltip is currently visible. The consumer decides whether to render the panel conditionally on this.

Programmatic Helpers

Helper functions for driving the tooltip from parent update handlers, returning [Model, Commands].

NameTypeDefaultDescription
reflectShowDelay(model: Model, showDelay: Duration.Input) => Model-Reflects an externally-sourced hover show-delay onto the model (a user preference, a restored setting) without emitting an OutMessage. Accepts any Effect Duration input; a bare number is milliseconds. The new delay applies on the next hover. Dual: pass just the delay for a point-free setter in an evo callback.

OutMessage

Messages emitted to the parent through the third element of [Model, Commands, Option<OutMessage>]. Fire only on visibility transitions, so consumers don’t see spurious events for messages that only update internal hover/focus/delay state.

NameTypeDefaultDescription
Shown{}-Emitted once the tooltip transitions to visible (isOpen becomes true). Pattern-match the third tuple element of Tooltip.update to react. Useful for analytics, instrumentation, or coordinating with other transient UI.
Hidden{}-Emitted once the tooltip transitions to hidden (isOpen becomes false).

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson