Button
A shadcn Base UI button with Depth primary, secondary, outline and destructive variants and squircle corners.
Primary, secondary, outline and destructive get a vertical gradient, a top highlight and a soft drop shadow. They also use squircle corners where the browser supports corner-shape.
import { Button } from "@/components/ui/button";
export default function ButtonVariantsExample() {
return (
<div className="flex flex-wrap items-center justify-center gap-3 p-6">
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
</div>
);
}
Installation
$ bunx --bun shadcn@latest add @notra/notra-buttonThis replaces components/ui/button.tsx. The gradient is built from your --primary, --foreground, --background, --muted and --destructive tokens, so it follows your theme.
Sizes
import { Button } from "@/components/ui/button";
export default function ButtonSizesExample() {
return (
<div className="flex flex-wrap items-center justify-center gap-3 p-6">
<Button size="xs">Extra small</Button>
<Button size="sm">Small</Button>
<Button>Default</Button>
<Button size="lg">Large</Button>
<Button disabled>Disabled</Button>
<Button disabled variant="secondary">
Disabled
</Button>
</div>
);
}
xs, sm, default and lg, plus icon, icon-xs, icon-sm and icon-lg for square buttons. Disabled buttons stay flat and don’t scale on press.
Loading
Pass loading while an action runs. The label blurs up and out, three dots rise in from below and bounce in turn, and the button keeps its width so nothing around it shifts. A loading button can’t be clicked again but stays focusable, and sets aria-busy.
The animated layers live in components/ui/button-content.tsx, a client component, so button.tsx and buttonVariants still work in server components.
"use client";
import { ArrowRightIcon, SendIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
const LOADING_DURATION_MS = 2000;
function LoadingButton(
props: Omit<React.ComponentProps<typeof Button>, "loading" | "onClick">
) {
const [loading, setLoading] = useState(false);
const handleClick = () => {
setLoading(true);
setTimeout(() => setLoading(false), LOADING_DURATION_MS);
};
return <Button {...props} loading={loading} onClick={handleClick} />;
}
export default function ButtonLoadingExample() {
return (
<div className="flex flex-wrap items-center justify-center gap-3 p-6">
<LoadingButton>Save changes</LoadingButton>
<LoadingButton variant="secondary">
Continue
<ArrowRightIcon data-icon="inline-end" />
</LoadingButton>
<LoadingButton variant="outline" size="sm">
Generate
</LoadingButton>
<LoadingButton variant="destructive">Delete</LoadingButton>
<LoadingButton aria-label="Send" size="icon">
<SendIcon />
</LoadingButton>
</div>
);
}
<Button loading={isSaving} onClick={save}>
Save changes
</Button>
Progress
For uploads and other work you can measure, pass progress from 0 to 100. The button fills from the left with a tint of its own text color, so it fits every variant. For work you can’t measure, like an import, use loading instead. Set it back to undefined when the work is done: the fill runs to the end and fades out. The fill is a Base UI Progress, and the button is busy and can’t be clicked while it shows.
"use client";
import { FileUpIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
const TICK_MS = 280;
function UploadButton() {
const [progress, setProgress] = useState<number>();
const uploading = progress !== undefined;
useEffect(() => {
if (!uploading) {
return;
}
const timer = setInterval(() => {
const step = 6 + Math.random() * 14;
setProgress((current) => {
if (current === undefined) {
return current;
}
const next = current + step;
return next < 100 ? next : undefined;
});
}, TICK_MS);
return () => clearInterval(timer);
}, [uploading]);
return (
<Button
onClick={() => setProgress(0)}
progress={progress}
variant="secondary"
>
<FileUpIcon data-icon="inline-start" />
Upload file
</Button>
);
}
export default function ButtonProgressExample() {
return (
<div className="flex flex-wrap items-center justify-center gap-3 p-6">
<UploadButton />
</div>
);
}
<Button progress={uploadProgress} onClick={upload} variant="secondary">
Upload file
</Button>