Components/FlatDropdown

FlatDropdown

Slot-driven floating dropdown panel (`TxFlatDropdown`). Hover / click / manual triggers, flip-aware placement, and a scale + blur exit animation.

Since 0.3.9BETA

This page was migrated by AI, please review carefully

Migration is complete, but please validate against source code and manual review.

Usage

Hover the trigger to open. The gap between trigger and panel is covered by a hover bridge, and a pointer heading for the panel keeps it open through the safe triangle; once the pointer really leaves, the panel stays for closeDelay ms. close-on-content-click dismisses the panel after any click inside.

FlatDropdown (basic)

Loading demo...

Best Practices

  • Prefer trigger="click" for destructive or state-changing menus — hover-opened panels are easy to trip over on trackpads.
  • Diagonal travel into the panel is handled by the hover bridge and the safe triangle, not by closeDelay; closeDelay only decides how long the panel stays once the pointer really leaves (off the triangle, stopped, or out through the side facing away from the panel).
  • Set teleport="false" when the dropdown lives inside a container with its own stacking or clipping context and you need it to inherit that context. Note that inline rendering re-exposes the panel to ancestor overflow: hidden.
  • Use the side slot prop to flip your own decorations (arrows, shadows) when the panel flips above the trigger.
  • Do not rely on the panel being positioned synchronously after opening — placement is written back reactively, so measure in a requestAnimationFrame if you need absolute geometry.
  • The reference wrapper advertises the panel with aria-haspopup, aria-expanded, and aria-controls (pointing at the panel's generated id), so a slotted <button> trigger inherits disclosure semantics automatically.

API Reference

TxFlatDropdown

Props

PropTypeDefaultDescription
modelValuebooleanundefinedControlled open state. Omit for uncontrolled behaviour.
trigger'hover' | 'click' | 'manual''hover'How the panel is summoned.
placementPlacement'bottom-start'Floating placement relative to the trigger.
offsetnumber10Gap in px between trigger and panel.
openDelaynumber0Delay before opening on hover/focus (ms).
closeDelaynumber600Delay before closing after pointer leave (ms).
exitDurationnumber280Duration of the scale + blur exit animation (ms).
disabledbooleanfalseDisable every interaction.
teleportboolean | string'body'Teleport target; pass false to render inline.
matchTriggerWidthbooleanfalseMatch the panel's min-width to the trigger width.
widthnumber | stringundefinedFixed panel width. Overrides matchTriggerWidth.
closeOnClickOutsidebooleantrueClose when clicking outside.
closeOnEscbooleantrueClose when pressing Escape.
closeOnContentClickbooleanfalseClose after any click inside the panel.
panelClassTxFlatDropdownClassundefinedExtra class(es) merged onto the panel element.

Events

EventPayloadDescription
update:modelValuebooleanOpen state changed.
open—The panel opened.
close—The panel closed.

Slots

SlotPropsDescription
trigger{ open, toggle, show, hide }The anchor element.
default{ open, close, side }Panel body. side is the resolved side after flip.

Trigger Modes

trigger decides how the panel is summoned:

  • hover (default) — opens on pointer enter / focus, closes after closeDelay; a trip towards the panel does not count as leaving.
  • click — toggles on click; closeDelay is not applied.
  • manual — the component never opens itself. Drive it with v-model.

Use manual when the panel must follow application state rather than pointer intent — for example a dropdown that opens as the result of a keyboard shortcut.

Controlled vs Uncontrolled

Omit v-model and the component tracks its own open state. Bind v-model and you own it — the component still emits open / close, but will not change the value itself unless the interaction is allowed.

disabled blocks every opening path, including programmatic ones through the trigger slot's show().

Sizing

By default the panel sizes to its content. Two escape hatches:

  • match-trigger-width — sets the panel's min-width to the measured trigger width. Good for select-like menus.
  • width — a fixed px number or any CSS length. Overrides match-trigger-width.

Dismissal Contract

Three independent dismissal paths, each separately switchable:

PropDefaultDismisses when
closeOnClickOutsidetrueA click lands outside the trigger, the panel and the hover bridge
closeOnEsctrueEscape is pressed
closeOnContentClickfalseAny click inside the panel

closeOnClickOutside only applies to the click and hover triggers — under manual the host owns dismissal entirely.

Technologies

  • Component: packages/tuffex/packages/components/src/flat-dropdown/src/TxFlatDropdown.vue
  • Types: packages/tuffex/packages/components/src/flat-dropdown/src/types.ts
  • The hover bridge and the safe triangle come from packages/tuffex/packages/utils/hover-intent.ts, shared with the anchor family. The bridge's geometry comes from the last middleware in the Floating UI chain; it renders as the panel's sibling with the same position: fixed and z-index, and enters and leaves like the panel.
  • Rejected design: the bridge as a child or pseudo-element of the panel. The panel's transform belongs to its scale-in and blur-out motion, and a host's panelClass may give the panel overflow: hidden, which would clip a hit area reaching outside the box.
  • Verified coverage: packages/tuffex/packages/components/src/flat-dropdown/__tests__/flat-dropdown.test.ts covers the hover bridge: rendered as the panel's sibling, a pointer resting on it keeps the panel open, a press on it is not an outside click, and click mode renders none.