GrDropdown
Opens a floating list of actions or options next to the trigger.
Overview
When to take it
- a menu with items of your own — you write the markup of the items, and the component takes the layer, the positioning and the keyboard;
- the trigger is non-standard — an avatar, a table cell, a button with a badge: the
#triggerslot accepts anything; - the panel opens on hover —
openDelayandcloseDelayseparate an intent from an accidental movement of the mouse; - the opening is governed by the parent —
v-model:opentogether withtrigger="manual".
When to take something else
| Need | Take |
|---|---|
| The items are ordinary: text, an icon, groups, separators | GrDropdownMenu |
| There is a form, a filter or a palette inside — not a menu | GrPopover |
| A text hint without interaction | GrTooltip |
| Selecting the value of a form field | GrSelect |
| A search across the commands of the whole application | GrCommandPalette |
The component declares role="menu" rigidly and walks the focus across the items. For a
form or a confirmation that is the wrong semantics — a screen reader would announce a
menu where there is none; that case is covered by GrPopover with a configurable role.
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrDropdown placement="bottom-start" />Examples 4 each with a live demo and its code
A card on hover
trigger="hover"withopen-delaytells intent from a passing cursor, andclose-delaykeeps the panel across the gap on the way to it. Click and keyboard still open it: a panel only a mouse can reach is no panel at all.ACM-212 · 2 h ago
Retries are ready for review. Assigned to— she wrote the backoff policy.Notifications under a bell
Any trigger takes
triggerPropson the real button — here inside a counter.v-model:openhands the state to the page, which marks everything read when the panel closes. Rows aremenuitembuttons, so arrows move between them.Acme Billing 2 unread notificationsControls inside the panel
close-on-content-click="false"keeps the panel open while checkboxes change the table, and theclosefrom the slot ends it with «Done». The trigger readsopenfrom the slot to turn its chevron.InvoicesInvoice Customer Due Amount INV-0418 Northwind Traders 15 Oct €1,284.50 INV-0417 Contoso Ltd 12 Oct €432.00 Upwards from the composer
The composer lives at the bottom of a screen, so
placement="top-start"opens the panel upwards — a preference, not an order: near an edge it flips. Two-line items are markup of your own, and a typed letter jumps to the matching title.
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
placement | Placement | undefined | "bottom-end" | The placement of the panel relative to the trigger; `flip` when there is not enough room still applies. |
offset | number | undefined | 8 | The gap between the trigger and the panel, in px. |
disabled | boolean | undefined | false | The panel is opened by nothing; the trigger stays focusable. |
openv-model | boolean | undefined | undefined | The controlled state of the panel (`v-model:open`). Without the prop the panel runs itself (uncontrolled), with it — listen to `update:open` and change the prop. |
trigger | GrDropdownTrigger | undefined | "click" | What opens the panel. The click and the keyboard work in any mode. |
teleportTo | string | HTMLElement | undefined | undefined | A pointed override of the mounting point. By default — the shared overlay portal (`#gr-portal` or the `portalTarget` from `GrConfigProvider`). |
contentClass | string | undefined | "" | Extra classes for the content container. |
width | GrDropdownWidth | undefined | "12rem" | The width of the panel: a number is pixels, a string is a CSS length, `auto` is by the content. |
openDelay | number | undefined | 120 | The delay before opening on hover, in ms. |
closeDelay | number | undefined | 160 | The delay before closing after the cursor leaves, in ms. |
closeOnContentClick | boolean | undefined | true | Close the panel on a click inside the content. |
Slots
| Slot | Type | Description |
|---|---|---|
trigger |
| The trigger of the panel. `triggerProps` are obliged to land on the interactive element itself rather than on a wrapper around it: `aria-expanded` and `aria-controls` are read from the node that gets the focus, and the click arrives together with them. |
content | { close: () => void; } | The content of the menu. |
Events
| Event | Signature | Description |
|---|---|---|
update:open | (event: "update:open", value: boolean): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
close | () => void | Close the panel from code. |
toggle | () => void | Toggle the panel: close it if open, open it if closed. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrDropdownTrigger, GrDropdownProps, GrDropdownEmits } from '@feugene/granularity/components/GrDropdown'Filedist/types/components/GrDropdown/config.d.ts
const grDropdownConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDropdown">;Filedist/types/components/GrDropdown/safelist.d.ts
const grDropdownSafelist: string[];Filedist/types/components/GrDropdown/GrDropdown.vue.d.ts
type GrDropdownTrigger = 'click' | 'hover';interface GrDropdownProps {
placement?: UseFloatingPlacement;
offset?: number;
width?: GrDropdownWidth;
trigger?: GrDropdownTrigger;
openDelay?: number;
closeDelay?: number;
disabled?: boolean;
closeOnContentClick?: boolean;
contentClass?: string;
teleportTo?: string | HTMLElement;
open?: boolean;
}interface GrDropdownEmits {
(e: 'update:open', value: boolean): void;
}Filedist/types/components/GrDropdown/grDropdownStyles.d.ts
type GrDropdownWidth = number | string;Filedist/types/components/GrDropdown/index.d.ts
type GrDropdownInstance = ComponentExposed<typeof GrDropdownComponent>;Accessibility
APG pattern
menu
Keys
↑Home / EndEnter / SpaceEsc
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
The trigger: triggerProps is mandatory
The component does not know what exactly the consumer will put into the #trigger slot,
so it gives the click, the keyboard and the ARIA away as an object that has to be bound
to a real focusable element:
<GrDropdown>
<template #trigger="{ triggerProps }">
<GrButton variant="outline" v-bind="triggerProps">Actions</GrButton>
</template>
<template #content>
<button type="button" role="menuitem">Duplicate</button>
</template>
</GrDropdown>The wrapper of the slot deliberately does not catch clicks: otherwise the panel would be
toggled by any click inside it — nested buttons and links included. A forgotten v-bind
prints a warning in a dev build: a silently non-working trigger would be looked for in
someone else’s code.
The slot also gives away open, toggle and close — for markup of your own for the
state.
The placement and the width
placement— any floating-ui value (bottom-endby default). Flipping when there is not enough room (flip) always works:placementis a preference rather than an order, and thetransform-originof the panel follows the actual side;offset— the gap between the trigger and the panel, in px;width— a CSS length: a number is treated as pixels, a string goes as it is, andautogives the width to the content. The Tailwind scales (width="48"→w-48) are gone; a string without units is treated as pixels and complains in a dev build.
The opening modes
trigger="click" (the default) or "hover". In the hover mode the delays are set by
openDelay/closeDelay: without the first the panel jumps out on any crossing of the
cursor, without the second it cannot be kept while moving from the trigger to the panel —
there is an offset gap between them.
The click and the keyboard work in both modes: a menu available only with the mouse is not available from the keyboard at all.
disabled closes the panel and does not let it be opened by a click, by the keyboard or
by hovering. The trigger stays focusable in the process and gets aria-disabled — the
native disabled would throw it out of the tab order together with the explanation of
what is going on.
The imperative API
open(), close() and toggle() through a ref on the component — the same set as in
the other overlays. Unlike the modal ones, a menu keeps its state itself: it has no
model, and the methods change it directly. The imperative path does not bypass
disabled — otherwise it would open what is closed to the click and to the keyboard.
The same three methods arrive in the #trigger slot (:open, :toggle, :close) — a
ref is needed when the menu has to be opened from outside the markup of the trigger.
The keyboard
Enter/Space/↓ open the panel with the focus on the first item, ↑ on the last.
Inside: ↓/↑ across the items, Home/End to the edges, Tab closes, Esc closes and
returns the focus to the trigger (that is disposed of by the shared layer stack rather
than by the component itself).
A printable character is a typeahead by the first letter: the buffer accumulates while the pauses between presses are shorter than 600 ms, and repeating the same letter means “the next item starting with it” rather than a search for “aa”. The search runs over the visible text of the item.
Space is not intercepted while the buffer is empty: menu items are buttons, and the
space is their native activation key. It enters the query only when a search is already
running (the typeahead rule from the WAI-ARIA APG).
Controls inside the panel
With closeOnContentClick={false} it is not a menu that lives in the panel but content —
fields, checkboxes, switches. The keys the focused control can handle itself are left to
it: printable characters and Home/End do not go into the navigation across the items
while the focus is in an input, a textarea, a select or a contenteditable.
The arrows are a deliberate exception: they stay with the panel even inside a field. Tab
closes the panel, and without the arrows there would be no way out of a field inside it.
They are also how the field is reached in the first place: everything focusable enters the
navigation ring, not only the menu items.
Control from the outside: v-model:open
Besides the imperative API the panel is controllable declaratively: without the open
prop the previous uncontrolled behaviour holds, with it the parent owns the state and
update:open accompanies every change (the shared contract of panel overlays, as in
GrPopover). The hover delays in the controlled mode are the same: the component emits,
the parent applies.
Props in this example