@edux-design/buttons
Headless-ish button primitives wired to the Edux design tokens. The package ships five components:
Button- primary action button with size + variant helpers.IconButton- circular icon-only button surface.MenuButton- semantic menu trigger button.ButtonGroup- segmented control with a sliding active state meant for contextual filters.ButtonCounter- increment/decrement control with optional editable value.
Everything is built with Tailwind utilities via the shared cx helper so styling stays consistent with the rest of the system.
Installation
pnpm add @edux-design/buttons @edux-design/utils
# or
npm install @edux-design/buttons @edux-design/utils
Peer deps: @edux-design/tokens@>=0.2.0 <0.3.0, react@^19.1.0, react-dom@^19.1.0.
Usage
import {
Button,
ButtonCounter,
ButtonGroup,
ButtonGroupItem,
IconButton,
MenuButton,
} from "@edux-design/buttons";
import { Close, Warning } from "@edux-design/icons";
export function Actions({ menuOpen, setMenuOpen }) {
return (
<div className="flex gap-4">
<Button size="small">Save</Button>
<Button isSecondary isStretched>
Cancel
</Button>
<Button isDanger disabled>
Delete
</Button>
<IconButton icon={<Close aria-hidden="true" />} aria-label="Close dialog" />
<MenuButton
aria-expanded={menuOpen}
aria-controls="actions-menu"
onClick={() => setMenuOpen((prev) => !prev)}
>
Options
</MenuButton>
<ButtonCounter defaultValue={2} min={1} aria-label="Quantity" />
</div>
);
}
export function SegmentedFilter() {
const [value, setValue] = React.useState("inbox");
return (
<ButtonGroup value={value} onChange={(nextValue) => setValue(nextValue)}>
<ButtonGroupItem value="inbox">Inbox (11)</ButtonGroupItem>
<ButtonGroupItem value="drafts">Drafts (11)</ButtonGroupItem>
<ButtonGroupItem
value="alerts"
endAdornment={<Warning className="h-4 w-4 text-warning-600" />}
>
Alerts
</ButtonGroupItem>
</ButtonGroup>
);
}
Accessibility notes
IconButtonrequires an accessible name viaaria-labeloraria-labelledby.MenuButtonrenders a native<button>and defaultsaria-haspopupto"menu".ButtonGroupuses radiogroup semantics with rovingtabIndexand supports arrow keys plus home/end.ButtonCounterexposes its value through a labelled output in display mode and a nativespinbuttonin editable mode.
Key props
| Component | Important props |
|---|---|
Button | `size="small" |
IconButton | icon, size, isBare, isDanger, disabled, aria-label / aria-labelledby |
MenuButton | Native button props, size, caret, aria-expanded, aria-controls |
ButtonGroup | value / defaultValue, onChange, size, isStretched, per-option startAdornment / endAdornment, disabled |
ButtonCounter | value / defaultValue, min, max, step, isEditable, inputProps |
Buttons warn in development if you pass conflicting variant booleans so you know which style wins. IconButton also warns when it is missing an accessible name.
Shared control sizes
Button components map their public size names to the semantic control-size contract from @edux-design/tokens:
| Public size | Token utility | Computed size |
|---|---|---|
small | control-sm | 36px |
medium | control-md | 44px |
large | control-lg | 56px |
Button uses the shared height, IconButton and ButtonCounter controls use the shared square size, and MenuButton uses the shared minimum height. Component-specific content widths remain owned by their component.
Development
pnpm --filter @edux-design/buttons lint
pnpm --filter @edux-design/buttons check-types
pnpm --filter @edux-design/buttons build
Stories live under src/demo (for example Button.stories.jsx and ButtonGroup.stories.jsx) for quick visual verification.
Additional components
Button merges className after its defaults using the shared cx contract, as
IconButton does. Host utilities follow the existing shared merge rules. Use size for standard
control heights; a deliberate Tailwind important utility such as h-32! or a
native style can override semantic geometry. Keyboard focus keeps the normal semantic shadow and gains a 2px system
Highlight outline with a 2px offset in forced colours. Keep a visible focus
indicator when deliberately overriding focus utilities.