Button
Triggers an action — 6 variants, 3 sizes and a loading state.
'use client';
import { Button } from '@akhatmirzo/ui';
export default function ButtonVariants() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button variant="primary">Saqlash</Button>
<Button>Soft</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">O'chirish</Button>
<Button variant="link">Batafsil</Button>
</div>
);
}
Import
import { Button } from '@akhatmirzo/ui';When to use
- For actions — saving, submitting, opening a dialog. Navigation needs a real link: pass an
<a>withasChild(see the example below). - One
primaryper view. Reserve it for the action you expect the user to take. Other buttons should besoft(the default),outlineorghost. dangeronly for destructive actions: deleting, anything that can't be undone. Usually paired withconfirm().- Icon only? Use
IconButtoninstead ofButton— it makesaria-labelrequired. - Label with a verb: "Save", "Send", "Delete" — not "OK" or "Yes".
Variants
variant sets the look and the visual weight. The default is soft.
| Variant | Use it for |
|---|---|
primary | The main action — at most one per view |
soft | Secondary actions (default) |
outline | Neutral, bordered — "Cancel", filters |
ghost | No background — inside toolbars and table rows |
danger | Delete and other destructive actions |
link | A button that looks like text, no padding |
Sizes
Three sizes — 32, 40 and 48px. They match Input and Select, so controls line up in a row.
The sm size gets an invisible hit area extended to 40px.
'use client';
import { Button } from '@akhatmirzo/ui';
export default function ButtonSizes() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button size="sm">Kichik · 32</Button>
<Button size="md">O'rta · 40</Button>
<Button size="lg">Katta · 48</Button>
</div>
);
}
Icons
leftIcon and rightIcon take any ReactNode. The icon size follows the button size:
16 / 18 / 20px. Icons are hidden from screen readers (aria-hidden).
'use client';
import { Button } from '@akhatmirzo/ui';
import { ArrowRight, Plus, Trash2 } from 'lucide-react';
export default function ButtonIcons() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button variant="primary" leftIcon={<Plus />}>
Qo'shish
</Button>
<Button rightIcon={<ArrowRight />}>Davom etish</Button>
<Button variant="danger" leftIcon={<Trash2 />}>
O'chirish
</Button>
</div>
);
}
Loading and disabled
While loading, the label keeps its place (the width doesn't jump), a spinner sits on top and
clicks are ignored. Click the first button to try it.
'use client';
import { Button, toast } from '@akhatmirzo/ui';
import { useState } from 'react';
export default function ButtonLoading() {
const [saving, setSaving] = useState(false);
const save = () => {
setSaving(true);
setTimeout(() => {
setSaving(false);
toast.success('Saqlandi');
}, 1500);
};
return (
<div className="flex flex-wrap items-center gap-3">
<Button variant="primary" loading={saving} onClick={save}>
Saqlash
</Button>
<Button variant="outline" loading labels={{ loading: 'Yuborilmoqda' }}>
Yuborish
</Button>
<Button disabled>O'chirilgan</Button>
</div>
);
}
loading vs. disabled
A disabled button leaves the tab order and can't be clicked. A loading button stays
focusable, so keyboard users don't lose their place — only clicks are blocked.
As a link (asChild)
With asChild, Button renders its child instead of its own <button> — the styles come from
the button, the element is your <a>. In Next.js you can pass <Link> the same way.
'use client';
import { Button } from '@akhatmirzo/ui';
import { ArrowRight } from 'lucide-react';
export default function ButtonAsChild() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button asChild variant="outline" rightIcon={<ArrowRight />}>
<a href="#accessibility">Accessibility bo'limiga</a>
</Button>
<Button asChild variant="link">
<a href="https://github.com/Akhatmirzo/akhatmirzo-toolkit">GitHub</a>
</Button>
</div>
);
}
import Link from 'next/link';
<Button asChild variant="primary">
<Link href="/orders">Go to orders</Link>
</Button>;Full width
For mobile forms and sign-in screens — fullWidth.
'use client';
import { Button } from '@akhatmirzo/ui';
export default function ButtonFullWidth() {
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<Button variant="primary" fullWidth>
Kirish
</Button>
<Button variant="outline" fullWidth>
Ro'yxatdan o'tish
</Button>
</div>
);
}
Props
Button also accepts every other <button> attribute (onClick, type, form, aria-*,
data-* …) and a ref.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'soft' | 'outline' | 'ghost' | 'danger' | 'link' | 'soft' | Look and visual weight |
size | 'sm' | 'md' | 'lg' | 'md' | Height: 32 / 40 / 48px |
loading | boolean | false | Shows a spinner, blocks clicks and sets aria-busy |
disabled | boolean | false | Disables the button (50% opacity) |
leftIcon | ReactNode | — | Icon before the label |
rightIcon | ReactNode | — | Icon after the label |
fullWidth | boolean | false | Stretches to the width of the container |
asChild | boolean | false | Renders the child element instead (for example, an <a>) |
labels | { loading?: string } | { loading: 'Yuklanmoqda' } | Screen reader text |
type | 'button' | 'submit' | 'reset' | 'button' | Defaults to 'button' so it never submits a form by accident |
Data attributes
State is exposed through data-* attributes, so you can hook your own styles onto them:
| Attribute | Value |
|---|---|
data-variant | primary · soft · outline · ghost · danger · link |
data-size | sm · md · lg |
data-disabled | Present when disabled |
data-loading | Present when loading |
Need the button look on another element? buttonVariants is exported too:
import { buttonVariants } from '@akhatmirzo/ui';
<label className={buttonVariants({ variant: 'outline', size: 'sm' })}>Choose a file</label>;Accessibility
- Native element. By default it renders a real
<button type="button">, so it never submits a form by accident and responds toEnterandSpace. - The focus ring only shows up for keyboard focus (
:focus-visible). - Loading.
loadingsetsaria-busy="true"andaria-disabled="true"and keeps the button focusable. Since many screen readers ignorearia-busy, a hiddenaria-liveregion announces "Yuklanmoqda" — change the text withlabels.loading(for example,'Loading'in an English app). - Icons and the spinner are
aria-hidden, so screen readers read only the label. asChild+disabled. An<a>has nodisabledattribute, so the button setsaria-disabled="true"andtabIndex={-1}and cancels the click.- Small buttons.
size="sm"looks 32px tall but has a 40px hit area — easy to tap with a finger.