Button
Triggers an action. Six variants, density-aware sizes, a loading state that keeps keyboard focus, and an icon-only form the types force you to name.
Usage
import { Button } from "@azmr/ui/button";import { Button } from "@azmr/ui/button";
export default function ButtonVariants() {
return (
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-center gap-3">
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button size="sm">Small</Button>
<Button>Medium</Button>
<Button size="lg">Large</Button>
<Button disabled>Disabled</Button>
</div>
</div>
);
}Use default for the primary action on a screen, secondary and outline for supporting ones, ghost for toolbars, destructive for irreversible actions and link for navigation that should read as text. The md size follows the density token: switch the preview above to Compact or Spacious and it resizes, exactly as it will inside an app that sets data-density.
A button is type="button" unless you say otherwise, so one inside a form never submits by accident. Pass type="submit" to opt in.
Loading
"use client";
import { Button } from "@azmr/ui/button";
import { useEffect, useRef, useState } from "react";
export default function ButtonLoading() {
const [saving, setSaving] = useState(false);
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => () => clearTimeout(timer.current), []);
function save() {
setSaving(true);
timer.current = setTimeout(() => setSaving(false), 1500);
}
// While loading the button ignores clicks and keys but stays focusable, so a keyboard user
// keeps their place.
return (
<Button loading={saving} loadingText="Saving" onClick={save}>
Save changes
</Button>
);
}loading shows a spinner, sets aria-busy, and ignores clicks and keyboard activation. It stays in the tab order, so a keyboard user who pressed Enter does not lose their place and a screen reader still finds the button. loadingText replaces the label while it is busy.
Icons and links
import { Button } from "@azmr/ui/button";
import { Icon } from "@azmr/ui/icons";
import { ArrowRight, Plus, X } from "lucide-react";
export default function ButtonIcons() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button startIcon={<Icon icon={Plus} />}>New customer</Button>
<Button variant="outline" endIcon={<Icon icon={ArrowRight} />}>
Continue
</Button>
{/* An icon-only button needs an accessible name; the types require it. */}
<Button size="icon" variant="ghost" aria-label="Close">
<Icon icon={X} />
</Button>
{/* Render as a link, with button styling and the right semantics. */}
<Button
variant="link"
nativeButton={false}
render={<a href="https://platform.azmara.io/docs" />}
>
Read the docs
</Button>
</div>
);
}startIcon and endIcon sit either side of the label; while loading, the spinner replaces the start icon. An icon-only button must be given an accessible name: with size="icon" TypeScript requires aria-label or aria-labelledby. To render a link with button styling, pass render={<a href="..." />} and nativeButton={false}.
Props
Everything a native button accepts passes through (onClick, type, disabled, aria-*, ref, ...), plus these from the underlying primitive:
| Prop | Type | Description |
|---|---|---|
render | ReactElement or function | Render a different element (such as <a>) with the button's behaviour and styles. |
nativeButton | boolean | Set to false when render is not a native <button>. |
focusableWhenDisabled | boolean | Keep a disabled button in the tab order. loading does this for you. |
className | string | Merged last, so it can override defaults: className="h-12" replaces the default height. |
Accessibility
- It is a real
<button>(or the element yourender), so it has the right role, name from its content, and disabled semantics with no ARIA added. - Focus uses a full-strength ring that meets WCAG 2.2 focus appearance; the same ring is used on every control.
loadingcommunicates busy state witharia-busyrather than changing the label silently.
| Key | Action |
|---|---|
| EnterorSpace | Activates the button. Ignored while loading. |
| Tab | Moves focus to the button. A loading button stays in the tab order. |
| ShiftorTab | Moves focus to the previous control. |