HextaUI

Button

Botões em todas as variantes e tamanhos, com um fluxo integrado de carregamento, sucesso e erro que dispensa o spinner em requisições rápidas.

"use client"

import { Button } from "@/components/ui/button"

function wait(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, ms))
}

async function fail(ms: number) {
  await wait(ms)
  throw new Error("Request failed")
}

export function ButtonDemo() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Button feedback onClick={() => wait(900)}>
        Save changes
      </Button>
      <Button feedback variant="outline" onClick={() => fail(900)}>
        Request that fails
      </Button>
      <Button feedback variant="secondary" onClick={() => wait(80)}>
        Fast request
      </Button>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/button.json

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

import { Button } from "@/components/ui/button"
<Button feedback onClick={() => saveSettings()}>
  Save changes
</Button>

Com feedback, retorne uma promise de onClick e o botão mostra o carregamento, depois sucesso ou erro, e então volta ao normal sozinho.

Variantes

Sete variants. destructive é um tom suave, para que uma ação perigosa seja lida com clareza sem gritar, e ghost-destructive é a versão discreta para ações repetidas em linhas, como Sign out ou Remove.

import { Button } from "@/components/ui/button"

export function ButtonVariants() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Button>Default</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="ghost-destructive">Ghost destructive</Button>
      <Button variant="link">Link</Button>
    </div>
  )
}

Tamanhos

Tamanhos de texto de xs a lg, e tamanhos quadrados icon-*. Botões de ícone pequenos ganham uma área de toque invisível maior em telas sensíveis ao toque.

import { IconPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

export function ButtonSizes() {
  return (
    <div className="flex flex-col items-center gap-4">
      <div className="flex flex-wrap items-center justify-center gap-2">
        <Button size="xs" variant="outline">
          Extra small
        </Button>
        <Button size="sm" variant="outline">
          Small
        </Button>
        <Button variant="outline">Default</Button>
        <Button size="lg" variant="outline">
          Large
        </Button>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-2">
        <Button size="icon-xs" variant="outline" aria-label="Add">
          <IconPlus />
        </Button>
        <Button size="icon-sm" variant="outline" aria-label="Add">
          <IconPlus />
        </Button>
        <Button size="icon" variant="outline" aria-label="Add">
          <IconPlus />
        </Button>
        <Button size="icon-lg" variant="outline" aria-label="Add">
          <IconPlus />
        </Button>
        <Button size="icon-xl" variant="outline" aria-label="Add">
          <IconPlus />
        </Button>
      </div>
    </div>
  )
}

Pill

shape="pill" arredonda totalmente as pontas, e os tamanhos de ícone viram círculos. Combina com botões dentro de superfícies arredondadas, como o composer de um chat.

import { IconArrowUp, IconPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

export function ButtonPill() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Button shape="pill">Get started</Button>
      <Button shape="pill" variant="outline">
        <IconPlus data-icon="inline-start" />
        New chat
      </Button>
      <Button shape="pill" size="icon" aria-label="Send">
        <IconArrowUp />
      </Button>
      <Button shape="pill" size="icon-sm" variant="ghost" aria-label="Add">
        <IconPlus />
      </Button>
    </div>
  )
}

Com ícone

Marque um ícone com data-icon="inline-start" ou "inline-end" e o padding daquele lado diminui para equilibrá-lo.

import { IconArrowRight, IconGitBranch } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

export function ButtonWithIcon() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Button variant="outline">
        <IconGitBranch data-icon="inline-start" />
        New branch
      </Button>
      <Button>
        Continue
        <IconArrowRight data-icon="inline-end" />
      </Button>
    </div>
  )
}

Desabilitado

focusableWhenDisabled mantém um botão desativado na ordem de tabulação, para que um tooltip ou uma explicação ainda possa ser alcançado pelo teclado.

import { Button } from "@/components/ui/button"

export function ButtonDisabled() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Button disabled>Disabled</Button>
      <Button variant="outline" disabled focusableWhenDisabled>
        Focusable when disabled
      </Button>
    </div>
  )
}

Rótulos personalizados

loadingLabel, successLabel e errorLabel substituem o texto de cada estado. Cada rótulo gira para dentro enquanto o antigo gira para fora.

"use client"

import { IconDeviceFloppy } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

function wait(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, ms))
}

async function fail(ms: number) {
  await wait(ms)
  throw new Error("Request failed")
}

export function ButtonCustomLabels() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Button
        feedback
        loadingLabel="Saving…"
        successLabel="Saved"
        errorLabel="Couldn’t save"
        onClick={() => wait(1200)}
      >
        <IconDeviceFloppy data-icon="inline-start" />
        Save
      </Button>
      <Button
        feedback
        variant="outline"
        loadingLabel="Saving…"
        successLabel="Saved"
        errorLabel="Couldn’t save"
        onClick={() => fail(1200)}
      >
        <IconDeviceFloppy data-icon="inline-start" />
        Save
      </Button>
    </div>
  )
}

Largura suave

O botão se ajusta suavemente à largura de cada rótulo em vez de reservar espaço para o mais longo, então nada ao redor dele salta.

"use client"

import { Button } from "@/components/ui/button"

function wait(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, ms))
}

export function ButtonSmoothWidth() {
  return (
    <Button
      feedback
      loadingLabel="Publishing to 3 regions…"
      successLabel="Live"
      onClick={() => wait(1600)}
    >
      Publish
    </Button>
  )
}

Detalhes do erro

Passe uma função para errorLabel para exibir o motivo da rejeição. Enquanto o ponteiro ou o foco do teclado permanecer no botão, o erro continua na tela.

"use client"

import { Button } from "@/components/ui/button"

export function ButtonErrorDetails() {
  return (
    <Button
      feedback
      variant="outline"
      errorLabel={(error) =>
        error instanceof Error ? error.message : "Failed"
      }
      onClick={async () => {
        await new Promise((resolve) => setTimeout(resolve, 700))
        throw new Error("Card declined")
      }}
    >
      Pay $24
    </Button>
  )
}

Formulários

Para botões de envio, chame track() de useButtonFeedback em onSubmit e espalhe buttonProps no botão. Remova o @ para ver o erro.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { useButtonFeedback } from "@/hooks/use-button-feedback"

function wait(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, ms))
}

async function fail(ms: number) {
  await wait(ms)
  throw new Error("Invalid email")
}

export function ButtonForm() {
  const save = useButtonFeedback()
  const [email, setEmail] = React.useState("[email protected]")

  return (
    <form
      className="flex w-full max-w-sm items-center gap-2"
      onSubmit={(event) => {
        event.preventDefault()
        save.track(email.includes("@") ? wait(900) : fail(600))
      }}
    >
      <input
        aria-label="Email"
        value={email}
        onChange={(event) => setEmail(event.target.value)}
        className="h-9 min-w-0 flex-1 rounded-md border border-input bg-transparent px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
      />
      <Button
        type="submit"
        {...save.buttonProps}
        successLabel="Subscribed"
        errorLabel="Invalid email"
      >
        Subscribe
      </Button>
    </form>
  )
}

Botões de ícone

Os tamanhos de ícone trocam apenas o ícone de cada estado e mantêm o formato quadrado. O aria-label continua sendo o nome acessível.

"use client"

import { IconDeviceFloppy } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

function wait(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, ms))
}

async function fail(ms: number) {
  await wait(ms)
  throw new Error("Request failed")
}

export function ButtonIconFeedback() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Button
        feedback
        size="icon"
        variant="outline"
        aria-label="Save"
        onClick={() => wait(900)}
      >
        <IconDeviceFloppy />
      </Button>
      <Button
        feedback
        size="icon"
        variant="outline"
        aria-label="Save"
        onClick={() => fail(900)}
      >
        <IconDeviceFloppy />
      </Button>
    </div>
  )
}

Feedback em todas as variants

As variants preenchidas ficam verdes ou vermelhas ao terminar. ghost e link só mudam a cor do texto.

"use client"

import { Button } from "@/components/ui/button"

function wait(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, ms))
}

const variants = [
  "default",
  "outline",
  "secondary",
  "ghost",
  "destructive",
  "link",
] as const

export function ButtonVariantsFeedback() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      {variants.map((variant) => (
        <Button
          key={variant}
          feedback
          variant={variant}
          onClick={() => wait(900)}
        >
          {variant}
        </Button>
      ))}
    </div>
  )
}

Carregamento controlado

Defina loading você mesmo quando o trabalho for acompanhado em outro lugar. O botão continua focável e anuncia que está ocupado.

"use client"

import * as React from "react"
import { IconSend } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

export function ButtonControlledLoading() {
  const [loading, setLoading] = React.useState(false)

  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <Button loading={loading} onClick={() => setLoading(true)}>
        <IconSend data-icon="inline-start" />
        Send invite
      </Button>
      <Button variant="ghost" onClick={() => setLoading(false)}>
        Stop loading
      </Button>
    </div>
  )
}

Status controlado

Controle status diretamente, por exemplo a partir do estado de envio de uma biblioteca de formulários.

"use client"

import * as React from "react"
import { IconRocket } from "@tabler/icons-react"

import { Button, type ButtonStatus } from "@/components/ui/button"

const statuses: ButtonStatus[] = ["idle", "loading", "success", "error"]

export function ButtonControlledStatus() {
  const [status, setStatus] = React.useState<ButtonStatus>("idle")

  return (
    <div className="flex flex-col items-center gap-3">
      <Button
        status={status}
        onStatusChange={setStatus}
        successLabel="Deployed"
        errorLabel="Deploy failed"
      >
        <IconRocket data-icon="inline-start" />
        Deploy
      </Button>
      <div className="flex flex-wrap justify-center gap-2">
        {statuses.map((next) => (
          <Button
            key={next}
            size="xs"
            variant={status === next ? "secondary" : "ghost"}
            onClick={() => setStatus(next)}
          >
            {next}
          </Button>
        ))}
      </div>
    </div>
  )
}

Passe um âncora para render e defina nativeButton={false} para que o botão mantenha a semântica de link.

import { IconArrowUpRight } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

export function ButtonLink() {
  return (
    <Button variant="outline" nativeButton={false} render={<a href="#" />}>
      Read the docs
      <IconArrowUpRight data-icon="inline-end" />
    </Button>
  )
}

Da direita para a esquerda

Os ícones e os rótulos de estado seguem a direção de leitura.

"use client"

import { IconDeviceFloppy } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

function wait(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, ms))
}

export function ButtonRtl() {
  return (
    <div dir="rtl" className="flex flex-wrap items-center justify-center gap-2">
      <Button
        feedback
        successLabel="تم الحفظ"
        errorLabel="فشل الحفظ"
        onClick={() => wait(900)}
      >
        <IconDeviceFloppy data-icon="inline-start" />
        حفظ
      </Button>
    </div>
  )
}
TeclaAção
EnterSpaceAtiva o botão. Ignorado enquanto uma requisição de feedback está em andamento.
TabMove o foco. Um botão em carregamento continua focável, e focar um erro o mantém na tela até você sair.
  • Toda mudança de estado é anunciada por uma região live educada (polite): carregando, e depois o rótulo de sucesso ou de erro.
  • Durante o carregamento, o botão define aria-busy e continua focável, então o foco nunca se perde no meio da requisição.
  • O spinner só aparece após 150ms e então permanece por pelo menos 400ms, de modo que requisições rápidas nunca piscam e as lentas nunca tremulam.
  • Com movimento reduzido, os rótulos de estado aparecem com fade em vez de girar e o tremor de erro é ignorado.

Construído sobre o botão do Base UI. Renderiza um <button> e aceita todos os seus atributos.

PropTipoPadrão
variant
"default" | "outline" | "secondary" | "ghost" | "ghost-destructive" | "destructive" | "link""default"
size
"xs" | "sm" | "default" | "lg" | "icon-xs" | "icon-sm" | "icon" | "icon-lg" | "icon-xl""default"
shape
"default" | "pill""default"
feedbackAcompanha a promise retornada de onClick e exibe seu status.
booleanfalse
onClickRetorne uma promise para controlar o feedback.
(event) => unknown–
loadingEstado de carregamento controlado.
boolean–
statusStatus controlado. Tem prioridade sobre loading.
"idle" | "loading" | "success" | "error"–
onStatusChange
(status: ButtonStatus) => void–
onErrorChamado com o motivo da rejeição.
(error: unknown) => void–
resetAfterMilissegundos antes de voltar ao estado idle.
number | { success?: number; error?: number }{ success: 2000, error: 4000 }
loadingLabelExibido ao lado do spinner. Oculto nos tamanhos de ícone.
ReactNode–
successLabel
ReactNode"Done"
errorLabel
ReactNode | (error: unknown) => ReactNode"Failed"
disabled
booleanfalse
focusableWhenDisabledSempre true durante o carregamento.
booleanfalse
nativeButtonDefina como false quando render não for um <button>.
booleantrue
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescrição
data-slot="button"Seleciona os botões no CSS.
data-statusidle, loading, success ou error. Presente quando feedback, loading ou status é usado.
data-disabledPresente quando o botão está desativado.
aria-busyPresente durante o carregamento.

Executa o mesmo fluxo de feedback de qualquer lugar, como o onSubmit de um formulário. Aceita resetAfter, onStatusChange e onError. Veja o guia do useButtonFeedback para os tempos completos.

RetornaDescrição
track(action)Passe uma promise ou uma função que retorne uma. Chamadas feitas enquanto uma requisição está em andamento são ignoradas.
buttonPropsEspalhe em <Button> para exibir o status e pausar o reset ao passar o mouse e ao receber foco.
statusO ButtonStatus atual.
errorO último motivo de rejeição.
reset()Cancela a requisição e volta ao estado idle.
isPending()Se há uma requisição em andamento.

Usado em blocos

Blocos que se baseiam em Button.