Button
shadcn Button with in-action loading and a first-class icon slot. Not a second LoadingButton or IconButton.
Button is the f-ui action control: all shadcn Button variants and sizes, a first-class icon slot, and loading for wait-able actions and host-mapped “job already running.” It wraps vendored ui/button and ui/spinner — do not edit those primitives to add loading or icons.
For which variant to use in a group (one solid primary, ghost tools, document utilities), see Button And Action Emphasis. For page / region first fetch, use skeleton / Empty / Result — see Page And Region Status — not this control’s spinner.
When To Use
- Any in-app action that needs shadcn Button chrome and optional in-flight feedback.
- Universal verbs that need a glyph (Download, Next, Refresh) — pass
icon, not a child SVG. - Map React Query
isPending, Formily submit, orjob.status === "running"intoloading. - Keep
disabledfor cannot-start (permission, empty selection) — never as the only in-flight signal. - Prefer Spin for unknown-shape waits or app lock; prefer page Skeleton for CRUD first paint.
- Do not invent a second
LoadingButtonorIconButton— this is both surfaces.
Features
| Area | Behavior |
|---|---|
| Base | Full shadcn Button API (variant, size, asChild, …) via wrapper |
| Icon | icon slot, iconPlacement start (default) or end. Kit stamps data-icon and aria-hidden |
| Loading | Spinner at 70% opacity, label unchanged, aria-disabled, stays focusable |
| Loading + icon | Spinner replaces the icon slot (same side as iconPlacement) |
| Icon-only | size icon* — spinner replaces the glyph; wrap with Tooltip + aria-label |
| AT | Optional loadingText (default "Loading") in a polite live region; spinner and icon are aria-hidden |
asChild | loading and icon ignored — cannot inject into an arbitrary Slot child |
| Re-exports | buttonVariants for link-as-button styling |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/button.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/button.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/button.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/button.jsonWith a namespace: npx shadcn@latest add @f-ui/button.
registryDependencies: shadcn primitives button, spinner (resolved against the default shadcn registry — not this item).
Usage
import { DownloadIcon } from "lucide-react";
import { Button } from "@/components/f-ui/button/button";
<Button icon={<DownloadIcon />} loading={isPending} onClick={() => void save()}>
Download
</Button>;Examples
Variants
Solid primary, outline, ghost, and destructive — same ladder as Design emphasis. Pick one solid primary per action group.
"use client";
import { Button } from "@/components/f-ui/button/button";
export function ButtonDemo() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button>Save</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Preview</Button>
<Button variant="destructive">Delete</Button>
</div>
);
}With Icon
Pass the glyph as icon. Default placement is start (left of the label). iconPlacement="end" is for trailing arrows. While loading, the spinner replaces that slot — the third control shows that, with the Download label still visible.
"use client";
import { ArrowRightIcon, DownloadIcon } from "lucide-react";
import { Button } from "@/components/f-ui/button/button";
export function ButtonIconDemo() {
return (
<div className="flex flex-wrap items-center gap-2">
<Button icon={<DownloadIcon />}>Download</Button>
<Button icon={<ArrowRightIcon />} iconPlacement="end" variant="outline">
Next
</Button>
<Button loading icon={<DownloadIcon />}>
Download
</Button>
</div>
);
}Icon-Only
Square size="icon*" plus icon. Dense chrome always wraps Tooltip and sets aria-label. Do not ship a second IconButton product.
"use client";
import { RefreshCw } from "lucide-react";
import { Button } from "@/components/f-ui/button/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export function ButtonIconOnlyDemo() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
icon={<RefreshCw />}
size="icon"
variant="ghost"
aria-label="Refresh"
/>
</TooltipTrigger>
<TooltipContent>Refresh</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}Loading Vs Disabled
In-flight uses loading (soft spinner + label). Permission-blocked uses disabled with no spinner. Icon-only loading replaces the glyph. Click Run to see a short pending cycle.
"use client";
import { useState } from "react";
import { RefreshCw } from "lucide-react";
import { Button } from "@/components/f-ui/button/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export function ButtonLoadingDemo() {
const [pending, setPending] = useState(false);
return (
<TooltipProvider>
<div className="flex flex-wrap items-center gap-2">
<Button loading>Generate</Button>
<Button
loading={pending}
onClick={() => {
setPending(true);
window.setTimeout(() => setPending(false), 1500);
}}
>
Run
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
loading
size="icon"
aria-label="Refresh"
icon={<RefreshCw />}
/>
</TooltipTrigger>
<TooltipContent>Refresh</TooltipContent>
</Tooltip>
<Button disabled>Cannot start</Button>
</div>
</TooltipProvider>
);
}Composition
Button (f-ui)
├── Shadcn Button (ui)
│ ├── icon slot span? (data-icon, aria-hidden) OR Spinner replacing it
│ └── children (label; omitted when size is icon* and loading)
└── sr-only live region (loadingText while loading)API Reference
Props
Extends shadcn Button props.
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | Glyph beside the label. Decorative (aria-hidden). |
iconPlacement | "start" | "end" | "start" | Which side of the label the icon (or loading spinner replacing it) sits. |
loading | boolean | false | In-flight / job-running chrome. Blocks activate without native disabled. Replaces icon when set. |
loadingText | string | "Loading" | Assistive announcement only — not the visible label. |
disabled | boolean | — | Cannot start. While loading, native disabled is forced off (loading wins chrome). |
asChild | boolean | false | Slot merge. loading and icon are ignored when asChild. |
Also accepts shadcn variant, size, className, onClick, and other native button attributes.
See Also
- Button And Action Emphasis — which variant, icons, loading vs disabled vs region load
- Confirm — confirm primary uses this Button’s
loading - Form Page — submit uses
loading={submitting} - Spin — region / app lock indicator (not action chrome)
- Page And Region Status — skeleton triad
Domain Status Vs Derived View
Separate persisted lifecycle status from derived business views, UI chrome, and capability gates — so frontend labels may differ from backend enums without inventing a second source of truth.
Page
Ant-style page frame with a pinned header, scroll body, and in-flow footer row for docked actions.