HextaUI

Button

Buttons in allen Varianten und Größen, mit eingebautem Lade-, Erfolgs- und Fehlerablauf, der den Spinner bei schnellen Anfragen überspringt.

"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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

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

Mit feedback gibst du aus onClick ein Promise zurück, und der Button zeigt Laden, dann Erfolg oder Fehler und setzt sich anschließend selbst zurück.

Varianten

Sieben Varianten. destructive ist ein sanfter Farbton, sodass eine gefährliche Aktion klar lesbar ist, ohne zu schreien, und ghost-destructive ist die ruhige Version für wiederkehrende Zeilenaktionen wie Abmelden oder Entfernen.

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

Größen

Textgrößen von xs bis lg und quadratische icon-*-Größen. Kleine Icon-Buttons erhalten auf Touchscreens eine größere unsichtbare Berührungsfläche.

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" rundet die Enden vollständig ab, und Icon-Größen werden zu Kreisen. Das passt zu Buttons in abgerundeten Flächen, etwa einem Chat-Composer.

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

Mit Icon

Markiere ein Icon mit data-icon="inline-start" oder "inline-end", und das Padding auf dieser Seite wird zum Ausgleich enger.

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

Deaktiviert

focusableWhenDisabled hält einen deaktivierten Button in der Tab-Reihenfolge, sodass ein Tooltip oder eine Erklärung weiterhin per Tastatur erreichbar ist.

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

Eigene Labels

loadingLabel, successLabel und errorLabel ersetzen den Text für den jeweiligen Zustand. Jedes Label kippt herein, während das alte hinauskippt.

"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>
  )
}

Sanfte Breite

Der Button gleitet auf die Breite des jeweiligen Labels, statt Platz für das längste zu reservieren, sodass sich nichts um ihn herum verschiebt.

"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>
  )
}

Fehlerdetails

Übergib errorLabel eine Funktion, um den Ablehnungsgrund anzuzeigen. Solange der Zeiger oder der Tastaturfokus auf dem Button bleibt, bleibt der Fehler sichtbar.

"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>
  )
}

Formulare

Rufe bei Submit-Buttons track() aus useButtonFeedback in onSubmit auf und spreade buttonProps auf den Button. Entferne das @, um den Fehler zu sehen.

"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>
  )
}

Icon-Buttons

Icon-Größen tauschen für jeden Zustand nur das Icon aus und behalten ihre quadratische Form. Das aria-label bleibt der zugängliche Name.

"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 für jede Variante

Gefüllte Varianten werden bei Abschluss grün oder rot. ghost und link ändern nur ihre Textfarbe.

"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>
  )
}

Gesteuertes Laden

Setze loading selbst, wenn die Arbeit anderswo nachverfolgt wird. Der Button bleibt fokussierbar und kündigt an, dass er beschäftigt ist.

"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>
  )
}

Gesteuerter Status

Steuere status direkt, zum Beispiel aus dem Submit-Zustand einer Formularbibliothek.

"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>
  )
}

Übergib einen Anker an render und setze nativeButton={false}, damit der Button die Link-Semantik behält.

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

Rechts nach links

Icons und Zustandslabels folgen der Leserichtung.

"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>
  )
}
TasteAktion
EnterSpaceAktiviert den Button. Wird ignoriert, solange eine Feedback-Anfrage läuft.
TabVerschiebt den Fokus. Ein ladender Button bleibt fokussierbar, und ein fokussierter Fehler bleibt auf dem Bildschirm, bis du weitergehst.
  • Jede Zustandsänderung wird über eine Live-Region mit polite-Priorität angesagt: erst Laden, dann das Erfolgs- oder Fehlerlabel.
  • Beim Laden setzt der Button aria-busy und bleibt fokussierbar, sodass der Fokus mitten in einer Anfrage nie verloren geht.
  • Der Spinner erscheint erst nach 150 ms und bleibt dann mindestens 400 ms, sodass schnelle Anfragen nie aufblitzen und langsame nie flackern.
  • Bei reduzierter Bewegung blenden Zustandslabels über, statt zu kippen, und das Wackeln beim Fehler entfällt.

Basiert auf dem Base UI Button. Er rendert ein <button> und akzeptiert alle seine Attribute.

PropTypStandard
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"
feedbackDas von onClick zurückgegebene Promise verfolgen und seinen Status anzeigen.
booleanfalse
onClickGib ein Promise zurück, um das Feedback zu steuern.
(event) => unknown–
loadingGesteuerter Ladezustand.
boolean–
statusGesteuerter Status. Hat Vorrang vor loading.
"idle" | "loading" | "success" | "error"–
onStatusChange
(status: ButtonStatus) => void–
onErrorWird mit dem Ablehnungsgrund aufgerufen.
(error: unknown) => void–
resetAfterMillisekunden, bevor in den Ruhezustand zurückgekehrt wird.
number | { success?: number; error?: number }{ success: 2000, error: 4000 }
loadingLabelWird neben dem Spinner angezeigt. Bei Icon-Größen ausgeblendet.
ReactNode–
successLabel
ReactNode"Done"
errorLabel
ReactNode | (error: unknown) => ReactNode"Failed"
disabled
booleanfalse
focusableWhenDisabledBeim Laden immer true.
booleanfalse
nativeButtonAuf false setzen, wenn render kein <button> ist.
booleantrue
render
ReactElement | (props, state) => ReactElement<button>
AttributBeschreibung
data-slot="button"Buttons in CSS ansprechen.
data-statusidle, loading, success oder error. Vorhanden, sobald feedback, loading oder status verwendet wird.
data-disabledVorhanden, wenn der Button deaktiviert ist.
aria-busyVorhanden, solange geladen wird.

Führt den gleichen Feedback-Ablauf von überall aus, etwa im onSubmit eines Formulars. Akzeptiert resetAfter, onStatusChange und onError. Die vollständigen Zeitabläufe findest du in der useButtonFeedback-Anleitung.

RückgabeBeschreibung
track(action)Übergib ein Promise oder eine Funktion, die eines zurückgibt. Aufrufe, während eine Anfrage läuft, werden ignoriert.
buttonPropsAuf <Button> spreaden, um den Status anzuzeigen und das Zurücksetzen bei Hover und Fokus zu pausieren.
statusDer aktuelle ButtonStatus.
errorDer letzte Ablehnungsgrund.
reset()Bricht die Anfrage ab und kehrt in den Ruhezustand zurück.
isPending()Ob eine Anfrage läuft.

In Blocks verwendet

Blocks, die auf Button aufbauen.