ActionBar
ActionBar keeps contextual actions visible when people select items in a list, table, calendar, or similar surface.
Use it for common bulk actions like edit, delete, merge, export, or more options without moving people away from their current task.
Available from eds-core/1.43.0
Quick Start
- Installation
npm install @adaptavant/eds-core- Import
import { ActionBar } from '@adaptavant/eds-core';
Usage
Use ActionBar when one or more items are selected and contextual actions should remain available while the user continues scanning the surface.
const [selectedIds, setSelectedIds] = React.useState([]);
const [strategy, setStrategy] = React.useState("absolute");
const { isMobile } = useResponsiveLayout();
const customers = [
{ id: "ava", name: "Ava Johnson", email: "ava@example.com" },
{ id: "ben", name: "Ben Carter", email: "ben@example.com" },
{ id: "mia", name: "Mia Patel", email: "mia@example.com" },
];
const toggleSelection = (id) => {
setSelectedIds((current) =>
current.includes(id)
? current.filter((selectedId) => selectedId !== id)
: [...current, id]
);
};
React.useLayoutEffect(() => {
setSelectedIds(["ava"]);
}, []);
return (
<Box className="mx-auto w-full">
<Box className="mb-3 flex items-center gap-2">
<Text className="text-body-14 font-strong text-primary mr-2 ml-1">Strategy :</Text>
<Radio label="Absolute" value="absolute" checked={strategy === "absolute"} onChange={() => setStrategy("absolute")}/>
<Radio label="Fixed" value="fixed" checked={strategy === "fixed"} onChange={() => setStrategy("fixed")}/>
</Box>
<Box
className="relative min-h-80 rounded-16px border border-neutral-tertiary bg-canvas p-6 pb-24"
>
<Text className="text-heading-16">Customer list</Text>
<Text className="mt-2 text-body-14 text-secondary">
Select rows to show the action bar and update the selection count.
</Text>
<Box className="mt-4 flex flex-col gap-3">
{customers.map((customer) => (
<Checkbox
checked={selectedIds.includes(customer.id)}
key={customer.id}
label={
<Box className="flex flex-col">
<Text className="text-body-14">{customer.name}</Text>
<Text className="text-body-12 text-secondary">
{customer.email}
</Text>
</Box>
}
onChange={() => toggleSelection(customer.id)}
/>
))}
</Box>
<ActionBar
aria-label="Customer bulk actions"
closeButtonProps={{ onClick: () => setSelectedIds([]) }}
label={`${selectedIds.length} customer${
selectedIds.length === 1 ? "" : "s"
} selected`}
open={selectedIds.length > 0}
strategy={strategy}
>
<ActionBarButton iconStart={MergeIcon}>Merge</ActionBarButton>
{isMobile ? null : (
<>
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
<ActionBarButton iconStart={DeleteIcon}>Delete</ActionBarButton>
</>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More actions"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<>
<DropdownMenuItem railStart={<EditIcon />}>
Edit
</DropdownMenuItem>
<DropdownMenuItem railStart={<DeleteIcon />}>
Delete
</DropdownMenuItem>
</>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
</Box>
);
Placement
Set placement for the side where the ActionBar appears.
const [selectedIds, setSelectedIds] = React.useState([]);
const [placement,setPlacement] = React.useState('bottom')
const [strategy, setStrategy] = React.useState("absolute");
const { isMobile } = useResponsiveLayout();
const customers = [
{ id: "ava", name: "Ava Johnson", email: "ava@example.com" },
{ id: "ben", name: "Ben Carter", email: "ben@example.com" },
{ id: "mia", name: "Mia Patel", email: "mia@example.com" },
];
const toggleSelection = (id) => {
setSelectedIds((current) =>
current.includes(id)
? current.filter((selectedId) => selectedId !== id)
: [...current, id]
);
};
React.useLayoutEffect(()=>{
setSelectedIds(["ava"])
},[])
return (
<Box className="w-full">
<Box className="mb-3 flex items-center gap-5 flex-wrap">
<Box className="flex items-center gap-2">
<Text className="text-body-14 font-strong text-primary mr-2 ml-1">Strategy :</Text>
<Radio label="Absolute" value="absolute" checked={strategy === "absolute"} onChange={() => setStrategy("absolute")}/>
<Radio label="Fixed" value="fixed" checked={strategy === "fixed"} onChange={() => setStrategy("fixed")}/>
</Box>
<Box className="flex items-center gap-2">
<Text className="text-body-14 font-strong text-primary mr-2 ml-1">Placement :</Text>
<Radio label="Top" value="top" checked={"top" === placement} onChange={() => setPlacement("top")}/>
<Radio label="Bottom" value="bottom" checked={"bottom" === placement} onChange={() => setPlacement("bottom")}/>
</Box>
</Box>
<Box
className="relative min-h-80 rounded-16px border border-neutral-tertiary bg-canvas p-6 py-16"
>
<Text className="text-heading-16">Customer list</Text>
<Box className="mt-4 flex flex-col gap-3">
{customers.map((customer) => (
<Checkbox
checked={selectedIds.includes(customer.id)}
key={customer.id}
label={
<Box className="flex flex-col">
<Text className="text-body-14">{customer.name}</Text>
<Text className="text-body-12 text-secondary">
{customer.email}
</Text>
</Box>
}
onChange={() => toggleSelection(customer.id)}
/>
))}
</Box>
<ActionBar
aria-label="Customer bulk actions"
closeButtonProps={{ onClick: () => setSelectedIds([]) }}
label={`${selectedIds.length} customer${selectedIds.length === 1 ? "" : "s"} selected`}
open={selectedIds.length > 0}
placement={placement}
strategy={strategy}
>
<ActionBarButton iconStart={MergeIcon}>Merge</ActionBarButton>
{isMobile ? null : (
<>
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
<ActionBarButton iconStart={DeleteIcon}>Delete</ActionBarButton>
</>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More actions"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<>
<DropdownMenuItem railStart={<EditIcon />}>
Edit
</DropdownMenuItem>
<DropdownMenuItem railStart={<DeleteIcon />}>
Delete
</DropdownMenuItem>
</>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
</Box>
);
Size
Compare size="standard" and size="large" in same open state
Large
Standard
const [open, setOpen] = React.useState(false);
const { isMobile } = useResponsiveLayout();
React.useLayoutEffect(() => {
setOpen(true);
}, []);
return (
<Box className="w-full flex flex-col gap-3">
<Heading as="h4" className="pl-2">Large</Heading>
<Box
className="relative rounded-8px border border-neutral-tertiary bg-canvas min-h-20 w-full"
>
<ActionBar
aria-live="off"
closeButtonProps={{ onClick: () => setOpen(false) }}
label={`Size Large`}
open={open}
size={"large"}
strategy="absolute"
>
<ActionBarButton iconStart={MergeIcon}>Merge</ActionBarButton>
{isMobile ? null : (
<>
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
<ActionBarButton iconStart={DeleteIcon}>Delete</ActionBarButton>
</>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More actions"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<>
<DropdownMenuItem railStart={<EditIcon />}>
Edit
</DropdownMenuItem>
<DropdownMenuItem railStart={<DeleteIcon />}>
Delete
</DropdownMenuItem>
</>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
<Heading as="h4" className="pl-2">Standard</Heading>
<Box
className="relative rounded-8px border border-neutral-tertiary bg-canvas min-h-[72px] w-full"
>
<ActionBar
aria-live="off"
closeButtonProps={{ onClick: () => setOpen(false) }}
label={`Size Standard`}
open={open}
size={"standard"}
strategy="absolute"
>
<ActionBarButton iconStart={MergeIcon}>Merge</ActionBarButton>
{isMobile ? null : (
<>
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
<ActionBarButton iconStart={DeleteIcon}>Delete</ActionBarButton>
</>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More actions"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<>
<DropdownMenuItem railStart={<EditIcon />}>
Edit
</DropdownMenuItem>
<DropdownMenuItem railStart={<DeleteIcon />}>
Delete
</DropdownMenuItem>
</>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
</Box>
);
Variant
Compare variant="inverse" and variant="neutral" in same open state
Inverse
Neutral
const [open, setOpen] = React.useState(false);
const { isMobile } = useResponsiveLayout();
React.useLayoutEffect(() => {
setOpen(true);
}, []);
return (
<Box className="w-full flex flex-col gap-3">
<Heading as="h4" className="pl-2">Inverse</Heading>
<Box
className="relative rounded-8px border border-neutral-tertiary bg-canvas min-h-[72px] w-full"
>
<ActionBar
aria-live="off"
closeButtonProps={{ onClick: () => setOpen(false) }}
label={`Variant Inverse`}
open={open}
strategy="absolute"
variant="inverse"
>
<ActionBarButton iconStart={MergeIcon}>Merge</ActionBarButton>
{isMobile ? null : (
<>
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
<ActionBarButton iconStart={DeleteIcon}>Delete</ActionBarButton>
</>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More actions"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<>
<DropdownMenuItem railStart={<EditIcon />}>
Edit
</DropdownMenuItem>
<DropdownMenuItem railStart={<DeleteIcon />}>
Delete
</DropdownMenuItem>
</>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
<Heading as="h4" className="pl-2">Neutral</Heading>
<Box
className="relative rounded-8px border border-neutral-tertiary bg-canvas min-h-[72px] w-full"
>
<ActionBar
aria-live="off"
closeButtonProps={{ onClick: () => setOpen(false) }}
label={`Variant Neutral`}
open={open}
strategy="absolute"
variant="neutral"
>
<ActionBarButton iconStart={MergeIcon}>Merge</ActionBarButton>
{isMobile ? null : (
<>
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
<ActionBarButton iconStart={DeleteIcon}>Delete</ActionBarButton>
</>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More actions"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<>
<DropdownMenuItem railStart={<EditIcon />}>
Edit
</DropdownMenuItem>
<DropdownMenuItem railStart={<DeleteIcon />}>
Delete
</DropdownMenuItem>
</>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
</Box>
);
Mobile Usage
You can place a DropdownMenu trigger inside ActionBar for grouped actions. This is useful when secondary actions should stay available without taking too much horizontal space, especially on mobile. This matches the story pattern: keep the list-selection flow, then spread triggerProps onto ActionBarIconButton.
This is especially useful on mobile, where horizontal space is tighter. Keep most important action visible in bar, then move secondary actions into overflow menu so same actions stay available without making bar feel crowded. In this example, `useResponsiveLayout` moves `Edit` and `Delete` into the menu on mobile without responsive CSS classes.
const [selectedIds, setSelectedIds] = React.useState([]);
const [strategy, setStrategy] = React.useState("absolute");
const { isMobile } = useResponsiveLayout();
const customers = [
{ id: "ava", name: "Ava Johnson", email: "ava@example.com" },
{ id: "ben", name: "Ben Carter", email: "ben@example.com" },
{ id: "mia", name: "Mia Patel", email: "mia@example.com" },
];
const toggleSelection = (id) => {
setSelectedIds((current) =>
current.includes(id)
? current.filter((selectedId) => selectedId !== id)
: [...current, id]
);
};
React.useLayoutEffect(()=>{
setSelectedIds(["ava"])
},[])
return (
<Box className="mx-auto max-w-5xl">
<Box className="mb-3 flex items-center gap-2">
<Text className="text-body-14 font-strong text-primary mr-2 ml-1">Strategy :</Text>
<Radio label="Absolute" value="absolute" checked={strategy === "absolute"} onChange={() => setStrategy("absolute")}/>
<Radio label="Fixed" value="fixed" checked={strategy === "fixed"} onChange={() => setStrategy("fixed")}/>
</Box>
<Box
className="relative min-h-80 rounded-16px border border-neutral-tertiary bg-canvas p-6 pb-24"
>
<Text className="text-heading-16">Customer list</Text>
<Text className="mt-2 text-body-14 text-secondary">
Group secondary actions in a menu when the bar has limited space.
<br/>
This is especially useful on mobile, where horizontal space is tighter.
Keep most important action visible in bar, then move secondary actions
into overflow menu so same actions stay available without making bar
feel crowded. In this example, `useResponsiveLayout` moves `Edit` and
`Delete` into the menu on mobile without responsive CSS classes.
</Text>
<Box className="mt-4 flex flex-col gap-3">
{customers.map((customer) => (
<Checkbox
checked={selectedIds.includes(customer.id)}
key={customer.id}
label={
<Box className="flex flex-col">
<Text className="text-body-14">{customer.name}</Text>
<Text className="text-body-12 text-secondary">
{customer.email}
</Text>
</Box>
}
onChange={() => toggleSelection(customer.id)}
/>
))}
</Box>
<ActionBar
aria-label="Customer bulk actions"
closeButtonProps={{ onClick: () => setSelectedIds([]) }}
label={`${selectedIds.length} customer${selectedIds.length === 1 ? "" : "s"} selected`}
open={selectedIds.length > 0}
strategy={strategy}
>
<ActionBarButton iconStart={MergeIcon}>Merge</ActionBarButton>
{isMobile ? null : (
<>
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
<ActionBarButton iconStart={DeleteIcon}>Delete</ActionBarButton>
</>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More options"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<>
<DropdownMenuItem railStart={<EditIcon />}>
Edit
</DropdownMenuItem>
<DropdownMenuItem railStart={<DeleteIcon />}>
Delete
</DropdownMenuItem>
</>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
</Box>
);
Invoke ActionBar
You can invoke ActionBar directly from a button. In this pattern, omit the label and show only the actions.
const [open, setOpen] = React.useState(false);
const [strategy, setStrategy] = React.useState("absolute");
const { isMobile } = useResponsiveLayout();
return (
<Box className="mx-auto max-w-5xl">
<Box className="mb-3 flex items-center gap-2">
<Text className="text-body-14 font-strong text-primary mr-2 ml-1">Strategy :</Text>
<Radio label="Absolute" value="absolute" checked={strategy === "absolute"} onChange={() => setStrategy("absolute")}/>
<Radio label="Fixed" value="fixed" checked={strategy === "fixed"} onChange={() => setStrategy("fixed")}/>
</Box>
<Box
className="relative min-h-80 rounded-16px border border-neutral-tertiary bg-canvas p-6 pb-24"
>
<Text className="text-heading-16">Button trigger</Text>
<Text className="mt-2 text-body-14 text-secondary">
Use a custom trigger when the action bar should open independently of row selection.
</Text>
<Button
className="mt-6"
onClick={() => {
setOpen(true);
}}
>
Invoke action bar
</Button>
<ActionBar
closeButtonProps={{ onClick: () => setOpen(false) }}
open={open}
strategy={strategy}
>
<ActionBarButton iconStart={MergeIcon}>Merge</ActionBarButton>
{isMobile ? null : (
<>
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
<ActionBarButton iconStart={DeleteIcon}>Delete</ActionBarButton>
</>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More actions"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<>
<DropdownMenuItem railStart={<EditIcon />}>
Edit
</DropdownMenuItem>
<DropdownMenuItem railStart={<DeleteIcon />}>
Delete
</DropdownMenuItem>
</>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
</Box>
);
Custom Offset
Use offset when the ActionBar should clear fixed or container-level UI, such as a footer or pagination area. Offset values are pixels.
const [selectedIds, setSelectedIds] = React.useState([]);
const { isMobile } = useResponsiveLayout();
const customers = [
{ id: "ava", name: "Ava Johnson", email: "ava@example.com" },
{ id: "ben", name: "Ben Carter", email: "ben@example.com" },
{ id: "mia", name: "Mia Patel", email: "mia@example.com" },
];
const toggleSelection = (id) => {
setSelectedIds((current) =>
current.includes(id)
? current.filter((selectedId) => selectedId !== id)
: [...current, id]
);
};
React.useLayoutEffect(() => {
setSelectedIds(["ava"]);
}, []);
return (
<Box className="mx-auto max-w-5xl">
<Box className="relative min-h-96 overflow-hidden rounded-16px border border-neutral-tertiary bg-canvas p-6 pb-36">
<Text className="text-heading-16">Customer list</Text>
<Text className="mt-2 text-body-14 text-secondary">
Offset keeps the action bar above the reserved footer area.
</Text>
<Box className="mt-4 flex flex-col gap-3">
{customers.map((customer) => (
<Checkbox
checked={selectedIds.includes(customer.id)}
key={customer.id}
label={
<Box className="flex flex-col">
<Text className="text-body-14">{customer.name}</Text>
<Text className="text-body-12 text-secondary">
{customer.email}
</Text>
</Box>
}
onChange={() => toggleSelection(customer.id)}
/>
))}
</Box>
<Box className="absolute inset-x-0 bottom-0 border-t border-neutral-tertiary px-6 py-4">
<Text className="text-body-12 text-secondary">
Footer area reserved for pagination or summary controls.
</Text>
</Box>
<ActionBar
aria-label="Customer bulk actions"
closeButtonProps={{ onClick: () => setSelectedIds([]) }}
label={`${selectedIds.length} customer${selectedIds.length === 1 ? "" : "s"} selected`}
offset={80}
open={selectedIds.length > 0}
placement="bottom"
strategy="absolute"
>
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
{isMobile ? null : (
<ActionBarButton iconStart={DeleteIcon}>Delete</ActionBarButton>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More actions"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<DropdownMenuItem railStart={<DeleteIcon />}>
Delete
</DropdownMenuItem>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
</Box>
);
Accessibility Guidelines
ActionBar accessibility depends on a clear toolbar name and useful announcements when the visible label changes.
Assistive Labels
Use aria-label when the default Bulk actions label does not describe the toolbar clearly enough. For example, use aria-label="Customer bulk actions" when the ActionBar only applies to selected customers.
Assistive Announcements
Use aria-live to control how updates to the ActionBar label are announced. ActionBar applies aria-live to the visible label, not to the whole toolbar. See MDN guidance for choosing an aria-live value.
- Write self-contained labels, such as "3 customers selected", so screen reader users hear both the changed value and its context.
- Keep the default
aria-live="polite"for normal selection-count changes. This announces updates at the next suitable pause without interrupting current screen reader speech. - Use
aria-live="off"when the same change is already announced by a toast, status message, or focus change. This prevents duplicate announcements. - Use
aria-live="assertive"only for urgent, time-sensitive updates that the user must hear immediately. Assertive announcements can interrupt screen reader speech.
Announce changes that affect selected item count, available actions, completion status, waiting state, or errors. Do not announce decorative, layout, placement, offset, or responsive changes.
API Reference
ActionBar
| Prop | Default | Description |
|---|---|---|
open | _ | booleanControls whether the ActionBar is rendered. |
children | _ | ReactNodeActions rendered inside the ActionBar. Use ActionBarButton, ActionBarIconButton, ActionBarSeparator, or other interactive controls such as a DropdownMenu. |
label? | _ | stringOptional selection label, such as 3 customers. |
closeButtonProps? | _ | { label?: string; onClick?: () => void }Providing this prop renders the close button. label defaults to 'Close action bar'. Use onClick to handle close-button clicks or Escape dismissal. |
variant? | 'inverse' | 'neutral' | 'inverse'Visual variant for the ActionBar surface. |
size? | 'standard' | 'standard' | 'large'Size for the ActionBar. |
placement? | 'bottom' | 'top' | 'bottom'Side where the ActionBar appears. Fixed strategy uses the viewport edge; absolute strategy uses the nearest positioned ancestor. |
offset? | 16 | numberDistance in pixels from the placement edge. Fixed strategy adds the matching safe-area inset. |
strategy? | 'fixed' | 'fixed' | 'absolute'CSS positioning strategy for the ActionBar root. Use absolute inside a container with position: relative. |
shouldUsePortal? | false | booleanControls whether the ActionBar is rendered in the default EDS portal target. This only changes where the bar mounts; strategy controls positioning. |
aria-label? | 'Bulk actions' | stringAccessible label for the ActionBar. |
aria-live | 'polite' | 'off' | 'polite' | 'assertive'Announces label updates to assistive technologies. Use polite for non-urgent updates and assertive for time-sensitive updates. |
id? | _ | stringUnique identifier for the ActionBar root element. |
ActionBarButton
| Prop | Default | Description |
|---|---|---|
children | _ | ReactNodeButton label content. |
iconStart? | _ | (props: IconProps) => ReactNodeIcon rendered before the label. |
iconEnd? | _ | (props: IconProps) => ReactNodeIcon rendered after the label. |
onClick? | _ | (event: MouseEvent) => voidCallback invoked when the button is clicked. |
isDisabled? | false | booleanMarks the button as disabled for assistive technology and prevents click behavior. |
isLoading? | false | booleanShows a loading indicator and prevents click behavior. |
ActionBarIconButton
| Prop | Default | Description |
|---|---|---|
aria-label | _ | stringRequired accessible label for the icon-only action. |
icon | _ | (props: IconProps) => ReactNodeIcon rendered inside the button. |
onClick? | _ | (event: MouseEvent) => voidCallback invoked when the icon button is clicked. |
isDisabled? | false | booleanMarks the icon button as disabled for assistive technology and prevents click behavior. |
isLoading? | false | booleanShows a loading indicator and prevents click behavior. |
ActionBarSeparator
| Prop | Default | Description |
|---|---|---|
id? | _ | stringOptional id for the separator element. |
className? | _ | stringCustom class applied to the separator. |
style? | _ | CSSPropertiesCustom style applied to the separator. |
Style API
Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.
Please refer to the Style API documentation for more insights.
ActionBar parts
const [selectedIds, setSelectedIds] = React.useState([]);
const [strategy, setStrategy] = React.useState("absolute");
const { isMobile } = useResponsiveLayout();
const customers = [
{ id: "ava", name: "Ava Johnson", email: "ava@example.com" },
{ id: "ben", name: "Ben Carter", email: "ben@example.com" },
{ id: "mia", name: "Mia Patel", email: "mia@example.com" },
];
const toggleSelection = (id) => {
setSelectedIds((current) =>
current.includes(id)
? current.filter((selectedId) => selectedId !== id)
: [...current, id]
);
};
React.useLayoutEffect(()=>{
setSelectedIds(["ava"])
},[])
return (
<Box className="mx-auto max-w-5xl">
<Box className="mb-3 flex items-center justify-end gap-2">
<Text className="text-body-14 font-strong text-primary mr-2 ml-1">Strategy :</Text>
<Radio label="Absolute" value="absolute" checked={strategy === "absolute"} onChange={() => setStrategy("absolute")}/>
<Radio label="Fixed" value="fixed" checked={strategy === "fixed"} onChange={() => setStrategy("fixed")}/>
</Box>
<Box
className="relative min-h-80 rounded-16px border border-neutral-tertiary bg-canvas p-6 pb-28"
>
<Text className="text-heading-16">Styled customer list</Text>
<Text className="mt-2 text-body-14 text-secondary">
This example exaggerates each ActionBar part so the Style API is easier to inspect.
</Text>
<Box className="mt-4 flex flex-col gap-3">
{customers.map((customer) => (
<Checkbox
checked={selectedIds.includes(customer.id)}
key={customer.id}
label={
<Box className="flex flex-col">
<Text className="text-body-14">{customer.name}</Text>
<Text className="text-body-12 text-secondary">
{customer.email}
</Text>
</Box>
}
onChange={() => toggleSelection(customer.id)}
/>
))}
</Box>
<ActionBar
aria-label="Customer bulk actions"
classNames={{
root: "px-6",
toolbarContainer: "rounded-16px border-2 border-primary bg-blue-500 text-warning-onPrimary shadow-[0_16px_40px_rgba(15,23,42,0.28)]",
closeButton: "bg-critical text-critical-onPrimary ring-2 ring-white/40",
label: "rounded-8px bg-black px-3 py-1 font-stronger uppercase tracking-wide",
separator: "h-6 bg-primary",
inlineStartContainer: "gap-3 rounded-12px bg-black/10 py-2 pl-3 pr-2",
actions: "gap-2 rounded-12px bg-white/10 px-2 py-2",
}}
label={`${selectedIds.length} customer${selectedIds.length === 1 ? "" : "s"} selected`}
closeButtonProps={{ onClick: () => setSelectedIds([]) }}
open={selectedIds.length > 0}
strategy={strategy}
>
<ActionBarButton iconStart={MergeIcon}>Merge</ActionBarButton>
{isMobile ? null : (
<>
<ActionBarSeparator />
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
<ActionBarSeparator />
<ActionBarButton iconStart={DeleteIcon}>Delete</ActionBarButton>
</>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More actions"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<>
<DropdownMenuItem railStart={<EditIcon />}>
Edit
</DropdownMenuItem>
<DropdownMenuItem railStart={<DeleteIcon />}>
Delete
</DropdownMenuItem>
</>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
</Box>
);
| Part | Description |
|---|---|
root | Outer positioning wrapper. Useful for changing spacing or placement behavior around the bar. |
toolbarContainer | Main ActionBar surface. Use it for background, border, radius, and shadow changes. |
closeButton | Close IconButton that invokes closeButtonProps.onClick. |
label | Optional selection label. Useful when the label needs stronger emphasis than the default text styling. |
separator | Default separator slot rendered by ActionBar between the label and action group. Use ActionBarSeparator to add more dividers between consumer-rendered actions. |
inlineStartContainer | Inner group that wraps the close button, label, and separator. |
actions | Container for the action buttons and menu triggers. |
ActionBarButton parts
ActionBarButton renders a Button and does not add new style parts.
Use the same styling props as Button, including className, classNames, style, and styles.
Its size and variant are inherited from the parent ActionBar.
ActionBarIconButton parts
ActionBarIconButton renders an IconButton and does not add new style parts.
Use the same styling props as IconButton, including className, classNames, style, and styles.
Its size and variant are inherited from the parent ActionBar.
ActionBarSeparator parts
const [open, setOpen] = React.useState(false);
const { isMobile } = useResponsiveLayout();
React.useLayoutEffect(() => {
setOpen(true);
}, []);
return (
<Box className="w-full flex flex-col gap-3">
<Box
className="relative rounded-8px border border-neutral-tertiary bg-canvas min-h-[72px] w-full"
>
<ActionBar open strategy="absolute">
<ActionBarButton iconStart={MergeIcon}>Merge</ActionBarButton>
{isMobile ? null : (
<>
{/* Styled ActionBarSeparator */}
<ActionBarSeparator
className="h-8 bg-critical rounded-full"
style={{ width: "3px" }}
/>
<ActionBarButton iconStart={EditIcon}>Edit</ActionBarButton>
{/* Styled ActionBarSeparator */}
<ActionBarSeparator
className="h-8 rounded-full bg-[green]"
style={{ width: "3px" }}
/>
</>
)}
<DropdownMenu popoverOffset={8} popoverPlacement="bottom-end">
{({ triggerProps }) => (
<>
<ActionBarIconButton
aria-label="More actions"
icon={MoreIcon}
{...triggerProps}
/>
<DropdownMenuPopover>
<DropdownMenuList>
{isMobile ? (
<DropdownMenuItem railStart={<EditIcon />}>
Edit
</DropdownMenuItem>
) : null}
<DropdownMenuItem railStart={<InformationIcon />}>
View details
</DropdownMenuItem>
<DropdownMenuItem railStart={<ExportIcon />}>
Export data
</DropdownMenuItem>
</DropdownMenuList>
</DropdownMenuPopover>
</>
)}
</DropdownMenu>
</ActionBar>
</Box>
</Box>
);
No parts available. Only root.