@akhatmirzo/ui

Button

Triggers an action — 6 variants, 3 sizes and a loading state.

Import

import { Button } from '@akhatmirzo/ui';

When to use

  • For actions — saving, submitting, opening a dialog. Navigation needs a real link: pass an <a> with asChild (see the example below).
  • One primary per view. Reserve it for the action you expect the user to take. Other buttons should be soft (the default), outline or ghost.
  • danger only for destructive actions: deleting, anything that can't be undone. Usually paired with confirm().
  • Icon only? Use IconButton instead of Button — it makes aria-label required.
  • Label with a verb: "Save", "Send", "Delete" — not "OK" or "Yes".

Variants

variant sets the look and the visual weight. The default is soft.

VariantUse it for
primaryThe main action — at most one per view
softSecondary actions (default)
outlineNeutral, bordered — "Cancel", filters
ghostNo background — inside toolbars and table rows
dangerDelete and other destructive actions
linkA 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.

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).

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.

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.

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.

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.

Props

Button also accepts every other <button> attribute (onClick, type, form, aria-*, data-* …) and a ref.

PropTypeDefaultDescription
variant'primary' | 'soft' | 'outline' | 'ghost' | 'danger' | 'link''soft'Look and visual weight
size'sm' | 'md' | 'lg''md'Height: 32 / 40 / 48px
loadingbooleanfalseShows a spinner, blocks clicks and sets aria-busy
disabledbooleanfalseDisables the button (50% opacity)
leftIconReactNode—Icon before the label
rightIconReactNode—Icon after the label
fullWidthbooleanfalseStretches to the width of the container
asChildbooleanfalseRenders 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:

AttributeValue
data-variantprimary · soft · outline · ghost · danger · link
data-sizesm · md · lg
data-disabledPresent when disabled
data-loadingPresent 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 to Enter and Space.
  • The focus ring only shows up for keyboard focus (:focus-visible).
  • Loading. loading sets aria-busy="true" and aria-disabled="true" and keeps the button focusable. Since many screen readers ignore aria-busy, a hidden aria-live region announces "Yuklanmoqda" — change the text with labels.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 no disabled attribute, so the button sets aria-disabled="true" and tabIndex={-1} and cancels the click.
  • Small buttons. size="sm" looks 32px tall but has a 40px hit area — easy to tap with a finger.

On this page