Skip to main content

Actionbar

Actionbar is a widget that keeps contextual actions at the edge of a container, such as deleting selected rows or saving unsaved changes.

Import

import { Actionbar } from '@vortexlabs/vortex';
import type { ActionbarProps } from '@vortexlabs/vortex';

Allied components

Other links

2 selected
import { Actionbar, Button, Checkbox, Modal, Stack } from "@vortexlabs/vortex";
import { Download, Trash } from "@vortexlabs/vortex-icons";
import React from "react";

const members = ["Alex Chen", "Jordan Park", "Mia Rossi", "Priya Nair", "Sam Lee", "Noa Levy"];

export const Example = () => {
  const [items, setItems] = React.useState(members);
  const [selected, setSelected] = React.useState(["Alex Chen", "Jordan Park"]);
  const [confirmDelete, setConfirmDelete] = React.useState(false);

  return (
    <Stack direction="column" height="240px" overflow="auto" width="100%">
      <Stack gap={3} padding={4}>
        {items.map((name) => (
          <Checkbox
            key={name}
            name="member"
            label={name}
            checked={selected.includes(name)}
            onChange={({ checked }) =>
              setSelected((prev) =>
                checked ? [...prev, name] : prev.filter((item) => item !== name),
              )
            }
          />
        ))}
      </Stack>
      <Actionbar
        active={selected.length > 0}
        ariaLabel="Bulk actions"
        text={`${selected.length} selected`}
        closeAriaLabel="Clear selection"
        onClose={() => setSelected([])}
        actionsSlot={
          <>
            <Button startIcon={Download}>Download</Button>
            <Button startIcon={Trash} tone="critical" onClick={() => setConfirmDelete(true)}>
              Delete
            </Button>
          </>
        }
      />
      <Modal
        active={confirmDelete}
        onClose={() => setConfirmDelete(false)}
        width="32rem"
        padding={6}
      >
        <Stack gap={6}>
          <Modal.Title>Delete members</Modal.Title>
          <Modal.Description>
            Are you sure you want to delete {selected.length}{" "}
            {selected.length === 1 ? "member" : "members"}?
          </Modal.Description>
          <Stack direction="row" gap={3} justify="end">
            <Button onClick={() => setConfirmDelete(false)}>Cancel</Button>
            <Button
              variant="primary"
              tone="critical"
              onClick={() => {
                setItems((prev) => prev.filter((name) => !selected.includes(name)));
                setSelected([]);
                setConfirmDelete(false);
              }}
            >
              Delete
            </Button>
          </Stack>
        </Stack>
      </Modal>
    </Stack>
  );
};

Features

  • Shows a summary and actions for the current context.
  • Supports floating and docked variants.
  • Sticks to the top or bottom edge of its container.
  • Can swap its content to confirm an action in place.
  • Announces its summary to screen readers when it changes.

Variants

The default is floating, which reserves space so the bar never covers the last rows. docked fills the container width.

2 selected
2 selected
<Actionbar
  active
  closable={false}
  ariaLabel="Floating actions"
  text="2 selected"
  actionsSlot={<Button>Download</Button>}
/>
<Actionbar
  active
  variant="docked"
  closable={false}
  ariaLabel="Docked actions"
  text="2 selected"
  actionsSlot={<Button>Download</Button>}
/>

Tone

The default is neutral. inverse flips the color mode of the bar to stand out from the page.

3 selected
<Actionbar
  active
  tone="inverse"
  closable={false}
  ariaLabel="Bulk actions"
  text="3 selected"
  actionsSlot={<Button startIcon={Download}>Download</Button>}
/>

Position

The default is bottom. Render the bar as the matching first or last child of a flex column, which keeps it at the edge when content is short.

Scroll this content under the bar.
3 selected
3 selected
Scroll this content under the bar.
<Stack gap={3} width="100%">
  <Stack direction="column" height="160px" overflow="auto" borderColor="default" borderRadius="md">
    <Stack minHeight="400px" padding={4}>
      <Text>Scroll this content under the bar.</Text>
    </Stack>
    <Actionbar
      active
      closable={false}
      ariaLabel="Bottom actions"
      text="3 selected"
      actionsSlot={<Button>Download</Button>}
    />
  </Stack>
  <Stack direction="column" height="160px" overflow="auto" borderColor="default" borderRadius="md">
    <Actionbar
      active
      position="top"
      closable={false}
      ariaLabel="Top actions"
      text="3 selected"
      actionsSlot={<Button>Download</Button>}
    />
    <Stack minHeight="400px" padding={4}>
      <Text>Scroll this content under the bar.</Text>
    </Stack>
  </Stack>
</Stack>

Actions

Buttons in actionsSlot default to the tertiary variant and sm size. Name icon-only actions with ariaLabel and a Tooltip, and move extra actions into a DropdownMenu.

2 selected
<Actionbar
  active
  closable={false}
  ariaLabel="Bulk actions"
  text="2 selected"
  actionsSlot={
    <>
      <Tooltip text="Download">
        {(attributes) => (
          <Button
            variant="tertiary"
            size="sm"
            onlyIcon={Download}
            ariaLabel="Download"
            attributes={attributes}
          />
        )}
      </Tooltip>
      <DropdownMenu>
        <DropdownMenu.Trigger>
          {(attributes) => (
            <Button
              variant="tertiary"
              size="sm"
              onlyIcon={DotsHorizontal}
              ariaLabel="More"
              attributes={attributes}
            />
          )}
        </DropdownMenu.Trigger>
        <DropdownMenu.Content>
          <DropdownMenu.Item>Duplicate</DropdownMenu.Item>
          <DropdownMenu.Item>Archive</DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu>
    </>
  }
/>

Close Button

The bar is closable by default, which requires closeAriaLabel and onClose. Set closable to false when closing has no meaning, such as for unsaved changes.

Unsaved changes
<Actionbar
  active={value !== initial}
  variant="docked"
  closable={false}
  ariaLabel="Unsaved changes"
  text="Unsaved changes"
  actionsSlot={
    <>
      <Button onClick={() => setValue(initial)}>Discard</Button>
      <Button variant="primary" onClick={() => setValue(initial)}>
        Save changes
      </Button>
    </>
  }
/>

Inline Confirmation

Set confirmActive to swap the bar content for confirmText and confirmActionsSlot. Put the safe action first, since it receives focus.

2 selected
<Actionbar
  active={selected.length > 0}
  ariaLabel="Bulk actions"
  text={`${selected.length} selected`}
  closeAriaLabel="Clear selection"
  onClose={() => setSelected([])}
  actionsSlot={
    <Button startIcon={Trash} tone="critical" onClick={() => setConfirming(true)}>
      Delete
    </Button>
  }
  confirmActive={confirming}
  confirmText={`Delete ${selected.length} ${selected.length === 1 ? "member" : "members"}?`}
  onConfirmClose={() => setConfirming(false)}
  confirmActionsSlot={
    <>
      <Button onClick={() => setConfirming(false)}>Cancel</Button>
      <Button
        variant="primary"
        tone="critical"
        onClick={() => {
          setItems((prev) => prev.filter((name) => !selected.includes(name)));
          setSelected([]);
          setConfirming(false);
        }}
      >
        Yes, delete
      </Button>
    </>
  }
/>

For several actions, give each its own confirmation. Keep the last action in state so its content stays rendered while the confirmation slides out.

2 selected
const confirmations = {
  archive: { verb: "Archive", tone: undefined },
  delete: { verb: "Delete", tone: "critical" },
} as const;

type Action = keyof typeof confirmations;

const [pending, setPending] = React.useState<Action | null>(null);
const [lastAction, setLastAction] = React.useState<Action>("delete");
const { verb, tone } = confirmations[lastAction];

const ask = (action: Action) => {
  setLastAction(action);
  setPending(action);
};

<Actionbar
  active={selected.length > 0}
  ariaLabel="Bulk actions"
  text={`${selected.length} selected`}
  closeAriaLabel="Clear selection"
  onClose={() => setSelected([])}
  actionsSlot={
    <>
      <Button onClick={() => ask("archive")}>Archive</Button>
      <Button startIcon={Trash} tone="critical" onClick={() => ask("delete")}>
        Delete
      </Button>
    </>
  }
  confirmActive={pending !== null}
  confirmText={`${verb} ${selected.length} ${selected.length === 1 ? "member" : "members"}?`}
  onConfirmClose={() => setPending(null)}
  confirmActionsSlot={
    <>
      <Button onClick={() => setPending(null)}>Cancel</Button>
      <Button
        variant="primary"
        tone={tone}
        onClick={() => {
          setItems((prev) => prev.filter((name) => !selected.includes(name)));
          setSelected([]);
          setPending(null);
        }}
      >
        Yes, {verb.toLowerCase()}
      </Button>
    </>
  }
/>

Use Modal when a confirmation needs more explanation than fits in the bar.

Accessibility

Key
Description
Tab
Moves focus into the bar, then out of it.
Left arrowRight arrow
Moves between the controls in the bar, wrapping at the ends (reversed in right-to-left layouts).
HomeEnd
Moves to the first or last control in the bar.
Escape
Calls onConfirmClose while confirming, otherwise onClose.
Set ariaLabel to name the toolbar, and closeAriaLabel to name the close button.
Give icon-only actions an accessible name with ariaLabel.