HextaUI

Button

すべてのバリアントとサイズのボタンです。読み込み、成功、エラーのフローを内蔵し、高速なリクエストではスピナーを省略します。

"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

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

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

feedback を指定して onClick から Promise を返すと、ボタンは読み込み中、続いて成功またはエラーを表示し、その後ひとりでにリセットされます。

バリアント

7 つの variant。destructive は淡い色付けで、危険なアクションを主張しすぎずに明確に伝えます。ghost-destructive は、Sign out や 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>
  )
}

サイズ

テキストサイズは xs から lg まで、正方形の icon-* サイズもあります。小さいアイコンボタンは、タッチスクリーンでは見えないタッチ領域が大きくなります。

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" は両端を完全に丸め、アイコンサイズは円になります。チャットのコンポーザーのように、角丸の面の内側に置くボタンに向いています。

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

アイコン付き

アイコンに data-icon="inline-start" または "inline-end" を付けると、その側のパディングが小さくなってバランスが取れます。

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

無効

focusableWhenDisabled は無効なボタンをタブ順に残すため、ツールチップや説明にキーボードで到達できます。

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

カスタムラベル

loadingLabel、successLabel、errorLabel は、状態ごとのテキストを置き換えます。古いラベルが反転して消えるのと同時に、新しいラベルが反転して現れます。

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

なめらかな幅

ボタンは、最も長いラベルのために幅を確保せず、各ラベルの幅にイージングして変化するため、周囲が跳ねることはありません。

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

エラーの詳細

errorLabel に関数を渡すと、拒否の理由を表示できます。ポインターまたはキーボードフォーカスがボタンにある間は、エラーが表示されたままになります。

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

フォーム

送信ボタンでは、onSubmit の中で useButtonFeedback の track() を呼び、buttonProps をボタンにスプレッドします。@ を削除するとエラーを確認できます。

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

アイコンボタン

アイコンサイズでは、状態ごとにアイコンだけが入れ替わり、正方形の形は保たれます。aria-label がアクセシブルな名前のままになります。

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

すべての variant でのフィードバック

塗りの variant は、完了時に緑または赤に変わります。ghost と link は、テキストの色だけが変わります。

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

制御された loading

作業が別の場所で追跡されている場合は、自分で loading を設定します。ボタンはフォーカス可能なままで、処理中であることを通知します。

"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

たとえばフォームライブラリの送信状態から、status を直接制御します。

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

render にアンカーを渡し、nativeButton={false} を設定すると、ボタンがリンクのセマンティクスを保ちます。

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

右から左

アイコンと状態のラベルは、読む方向に従います。

"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>
  )
}
キーアクション
EnterSpaceボタンを実行します。フィードバックのリクエスト実行中は無視されます。
Tabフォーカスを移動します。読み込み中のボタンもフォーカス可能なままで、エラーにフォーカスしている間は、フォーカスを外すまで表示されたままになります。
  • 状態の変化はすべて polite なライブリージョンで通知されます。読み込み中、続いて成功またはエラーのラベルです。
  • 読み込み中、ボタンは aria-busy を設定したままフォーカス可能な状態を保つため、リクエストの途中でフォーカスが失われることはありません。
  • スピナーは 150ms 後に初めて表示され、その後は最低 400ms 表示され続けます。そのため、高速なリクエストで一瞬表示されることも、遅いリクエストでちらつくこともありません。
  • モーションの低減が有効な場合、状態のラベルは反転せずにフェードし、エラー時の揺れはスキップされます。

Base UI の button をベースにしています。<button> をレンダリングし、その属性をすべて受け付けます。

プロパティ型デフォルト
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"
feedbackonClick から返された Promise を追跡し、そのステータスを表示します。
booleanfalse
onClickフィードバックを制御するには Promise を返します。
(event) => unknown–
loading制御された loading の状態。
boolean–
status制御された status。loading より優先されます。
"idle" | "loading" | "success" | "error"–
onStatusChange
(status: ButtonStatus) => void–
onError拒否の理由とともに呼ばれます。
(error: unknown) => void–
resetAfter待機状態に戻るまでのミリ秒。
number | { success?: number; error?: number }{ success: 2000, error: 4000 }
loadingLabelスピナーの隣に表示されます。アイコンサイズでは非表示になります。
ReactNode–
successLabel
ReactNode"Done"
errorLabel
ReactNode | (error: unknown) => ReactNode"Failed"
disabled
booleanfalse
focusableWhenDisabled読み込み中は常に true です。
booleanfalse
nativeButtonrender が <button> でない場合は false に設定します。
booleantrue
render
ReactElement | (props, state) => ReactElement<button>
属性説明
data-slot="button"CSS でボタンを指定します。
data-statusidle、loading、success、error のいずれか。feedback、loading、status を使うと付与されます。
data-disabledボタンが無効なときに付与されます。
aria-busy読み込み中に付与されます。

フォームの onSubmit など、どこからでも同じフィードバックの流れを実行します。resetAfter、onStatusChange、onError を受け付けます。タイミングの詳細は useButtonFeedback のガイドを参照してください。

戻り値説明
track(action)Promise、または Promise を返す関数を渡します。リクエスト実行中の呼び出しは無視されます。
buttonProps<Button> にスプレッドすると、ステータスを表示し、ホバー時とフォーカス時にリセットを一時停止します。
status現在の ButtonStatus。
error直近の拒否の理由。
reset()リクエストをキャンセルして、待機状態に戻ります。
isPending()リクエストが実行中かどうか。

使用しているブロック

Button の上に構築されるブロック。