Hacktron Design System

RowSelectActionBar

A floating bar that appears when table rows are selected. It shows the count and the actions for the selection.

1 selected
<template>  <RowSelectActionBar    :count="selectedIds.length"    :actions="[      { label: 'Export', icon: 'i-lucide-download', onClick: onBulkExport },      { label: 'Delete', icon: 'i-lucide-trash', destructive: true, onClick: onBulkDelete },    ]"    @clear="rowSelection = {}"  /></template>

Props

Name Type Default
countnumber-
actionsRowSelectAction[]-
clearLabelstring"Clear"
moreLabelstring"More"
maxVisibleActionsnumber2
countLabel(count: number) => stringcount => `${count} selected`

Slots

Name Description
count-trailingRendered right after the count label, outside its live region, e.g. a "Select all N matching" link-button.
action-item-leadingScoped `{ action, item }`. Replaces an inline picker action's option leading icon (e.g. a status dot). Not applied once that picker overflows into "More".

Guidelines

Do

  • Pair it with Table's createRowSelectColumn - own the rowSelection state yourself (a Record<string, boolean> keyed by row id, same as v-model:row-selection on Table) and pass its selected count in; this component only renders count + actions, it never reads or owns selection state itself.
  • Set Table's getRowId (e.g. getRowId: row => row.id) whenever the paired table paginates - see table.md's own Do note on createRowSelectColumn. Without a stable row id, the selection this bar reports can silently apply to the wrong rows once the table's data changes page.
  • Pass destructive: true on a delete/remove action so it renders with Button's destructive variant instead of secondary.
  • Keep FilterBar rendered as-is while rows are selected - search and filtering should stay usable mid-selection, matching how Linear/Notion/Gmail's own bulk-selection bars leave their search/filter chrome in place rather than hiding it.
  • It's fixed at the bottom center of the viewport internally, hugging its own content width rather than the page's, but never wider than the viewport (on a narrow screen its controls wrap onto more lines) - no extra wrapper or positioning needed, and it stays visible regardless of scroll since it isn't document-flow-relative. While it's up it also renders an empty in-flow block as tall as the bar, so place it right after the table footer: the page can then scroll its last content (Pagination) clear of the bar.
  • actions isn't Button-only - set type: 'select'/type: 'combobox' on an action (alongside items/modelValue/onChange, mirroring Select/Combobox's own props) for a bulk picker (e.g. "Change status", "Assign owner") instead of a push-button. Plain actions default to type: 'button' and need no type at all.
  • Only the first maxVisibleActions (2 by default) non-destructive actions render inline; put whichever ones matter most (and any combobox action, since its search box is lost once collapsed) among them - the rest collapse into a trailing "More" dropdown automatically.
  • Raise maxVisibleActions when the inline actions only make sense side by side - e.g. two pickers plus the Apply button that commits them needs :max-visible-actions="3", since an Apply tucked into "More" reads as unrelated to the pickers it applies.
  • Set loading: true on a button action while its async bulk mutation runs - it shows Button's spinner and ignores clicks, so a slow request can't be fired twice. Use disabled: true for an action that doesn't apply to the current selection. Both carry into "More": the item is disabled there (no spinner).
  • Put a selection-scope control in the #count-trailing slot (e.g. a variant="plain" Button: "Select all 240 matching" / "Clear selection of all 240") - it renders right after the count, outside the count's role="status" live region, so screen readers announce only the count change.
  • Use the #action-item-leading="{ action, item }" slot for a status dot in an inline picker action's options. A picker that overflows into "More" becomes a plain submenu and doesn't get it, so keep it inline (raise maxVisibleActions) when the dot matters. Once filled, the slot replaces the leading icon of every inline picker's options, so branch on action and render item.icon yourself for the others.

Don't

  • Don't build your own Clear control - it's built in (a ghost IconButton "x") and emits clear for you to reset selection.
  • Don't render it conditionally yourself - it already renders nothing when count is 0.
  • Don't hide FilterBar while this is showing - the two occupy different parts of the screen now (floating bottom bar vs. top row) and are meant to coexist.
  • Don't pass more non-destructive actions than maxVisibleActions expecting them all inline - anything past that moves into "More" on its own; a combobox action there degrades to a plain (non-searchable) submenu of its own items.
  • Don't raise maxVisibleActions just to avoid "More" - only when the inline actions work as one group. Past 3 the bar stops reading as a compact floating bar.
  • Don't expect a destructive: true action to ever render inline, however high maxVisibleActions is - it always goes into "More", so it can't be fired by a stray click on the bar.
  • Don't track an action's in-flight state with your own spinner or by removing the action - pass loading and let the bar render it.

Examples

Loading action

An async bulk mutation shows a spinner and blocks repeat clicks until it settles.

3 selected
<template>  <RowSelectActionBar    :count="3"    :actions="[      { label: 'Export', icon: 'i-lucide-download', loading: exporting, onClick: onExport },      { label: 'Archive', icon: 'i-lucide-archive', disabled: exporting, onClick: onArchive },    ]"    @clear="rowSelection = {}"  /></template>

Select all matching

A selection-scope link after the count, for widening a page selection to every matching row.

25 selected
<template>  <RowSelectActionBar :count="allMatchingSelected ? total : pageSelectedCount" :actions="actions" @clear="clearSelection">    <template #count-trailing>      <Button v-if="!allMatchingSelected" variant="plain" size="sm" @click="allMatchingSelected = true">        Select all {{ total }} matching      </Button>      <Button v-else variant="plain" size="sm" @click="allMatchingSelected = false">        Clear selection of all {{ total }}      </Button>    </template>  </RowSelectActionBar></template>

Three inline actions

Two pickers plus the Apply button that commits them, kept side by side with maxVisibleActions.

4 selected
<template>  <RowSelectActionBar    :count="4"    :max-visible-actions="3"    :actions="[      { type: 'select', label: 'Status', items: statusItems, modelValue: status, onChange: v => (status = v) },      { type: 'select', label: 'Severity', items: severityItems, modelValue: severity, onChange: v => (severity = v) },      { label: 'Apply', disabled: !status && !severity, onClick: onApply },    ]"    @clear="rowSelection = {}"  /></template>