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.
<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.
<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.
<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.
<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.
<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.
<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.
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
onConfirmClose while confirming, otherwise onClose.ariaLabel to name the toolbar, and closeAriaLabel to name the close button.ariaLabel.