The action affordance at the end of a table row: up to a few inline icon buttons, everything else behind an overflow menu.
The qds-row-actions component provides a compact set of actions for a single row in a table or a card footer. It renders a small number of inline icon buttons (visible on row hover or keyboard focus) and an overflow menu trigger for the remaining actions.
Inline actions are revealed on row hover and on focus-within, NOT always visible. A column of repeated icons down 200 rows is visual noise that competes with the data. The overflow trigger stays visible so the affordance is never hidden, which is what makes hover-reveal acceptable rather than a discoverability bug.
Reveal uses opacity only, never display: none, visibility: hidden, or aria-hidden. Elements with opacity: 0 remain focusable, which is exactly what focus-within reveal depends on, keyboard users get the same affordances as mouse users.
Destructive actions are NEVER rendered inline, regardless of inline-count. A delete button one pixel from a duplicate button, revealed on hover, in a 200-row table, will destroy data. Destructive items sit below a divider at the end of the overflow menu and are coloured with --qds-row-actions-destructive-color (defaults to --theme-error). The red is reinforcement only, the label carries the meaning (“Delete”).
On coarse pointers (@media (pointer: coarse)) there is no hover, so inline actions are always visible and sized 44x44. This is set from the media query, not a user-agent check.
This component fires the qds-row-action event and stops. Something must confirm before a destructive action takes effect, the row disappears. There is no QDS dialog or confirm component at this time, that is a real gap and probably a work item of its own. Until then, consumers own confirmation, the README documents this so it is not forgotten.
<qds-row-actions row-id="row-1" inline-count="2"></qds-row-actions>
<script>
const el = document.querySelector('qds-row-actions');
el.actions = [
{ id: 'edit', label: 'Edit', icon: 'pi-pencil' },
{ id: 'duplicate', label: 'Duplicate', icon: 'pi-clone' },
{ id: 'export', label: 'Export', icon: 'pi-download' },
{ id: 'delete', label: 'Delete', icon: 'pi-trash', destructive: true }
];
el.addEventListener('qds-row-action', (e) => {
console.log('Action:', e.detail.action, 'Row:', e.detail.rowId);
});
</script>
row-id - string, default ''. Echoed in every event detail.inline-count - number, default 2. How many non-destructive actions render inline; 0 puts everything in the menu.size - 'sm' | 'md' | 'lg', default 'md'. Matches row density. Buttons are 28x28 at sm, 32x32 at md and lg.disabled - boolean, default false. Disables the whole group (all inline buttons and the overflow menu).always-visible - boolean, default false. Skips hover-reveal; inline actions always visible.open - boolean, default false. Reflected; overflow menu open state.actions - QdsRowAction[], default []. Property only, not an attribute. Array of action objects.row - QdsTableRow | undefined. Populated by qds-data-table when it owns the component; standalone use leaves undefined.open - boolean. Overflow menu open state (reflected to attribute).interface QdsRowAction {
id: string;
label: string; // always required, it is the accessible name
icon?: string; // `prime:pi-*`, `lucide:*`, or legacy `pi-*`; shown in inline buttons and menu items
disabled?: boolean;
disabledReason?: string; // shown as tooltip and appended to accessible name when disabled
destructive?: boolean; // never inline; sits below a divider at end of menu
group?: string; // one divider inserted between different groups, none at ends
}
container - The outer wrapper.inline-action - Each inline action button.overflow-trigger - The overflow menu trigger button.menu - The overflow menu panel.menu-item - Each menu item.menu-item-destructive - Destructive menu items (also carries menu-item).menu-divider - Divider between groups or before destructive items.menu-icon - Icon within a menu item.menu-label - Label text within a menu item.--qds-row-actions-button-size - Default 32px. Size of inline and trigger buttons.--qds-row-actions-gap - Default var(--theme-space-1, 4px). Gap between buttons.--qds-row-actions-menu-min-width - Default 180px. Minimum width of the overflow menu.--qds-row-actions-destructive-color - Default var(--theme-error). Text color for destructive menu items.qds-row-action - When an action is activated. detail: { component, action, rowId, row }. action is the action’s id. row is populated by qds-data-table; undefined standalone. Events are composed: true and cross the table’s shadow boundary.qds-open - When the overflow menu opens. detail: { component, rowId }.qds-close - When the overflow menu closes. detail: { component, rowId }.| Key | Behaviour |
|---|---|
Tab |
Inline buttons and the overflow trigger are each a tab stop |
Enter / Space |
Activates the focused button; opens the menu on the trigger |
ArrowDown / ArrowUp |
Moves through menu items when open, wrapping |
Escape |
Closes the menu, returns focus to the trigger |
Home / End |
First / last enabled menu item |
Only one row’s menu is open at a time. Opening a second row’s menu closes the first.
<qds-row-actions row-id="acme-1" inline-count="2" size="sm"></qds-row-actions>
<script>
const el = document.querySelector('qds-row-actions');
el.actions = [
{ id: 'edit', label: 'Edit', icon: 'pi-pencil' },
{ id: 'duplicate', label: 'Duplicate', icon: 'pi-clone' },
{ id: 'export', label: 'Export', icon: 'pi-download', group: 'file' },
{ id: 'archive', label: 'Archive', icon: 'pi-folder', group: 'file' },
{ id: 'delete', label: 'Delete', icon: 'pi-trash', destructive: true }
];
el.addEventListener('qds-row-action', (e) => {
if (e.detail.action === 'delete') {
// Consumer owns confirmation, QDS has no dialog component yet
if (confirm(`Delete row ${e.detail.rowId}?`)) {
// proceed with deletion
}
}
});
</script>