implement
Button

Button

The button, and the variant table half the registry borrows.

import { Div } from "@implementjs/core";
import { PlusIcon } from "@implementjs/lucide";
import { Button } from "@/lib/components/ui/button";

export default function ButtonDemo() {
	return Div(
		{ class: "flex flex-wrap items-center justify-center gap-2" },
		Button("Default"),
		Button({ variant: "secondary" }, "Secondary"),
		Button({ variant: "outline" }, "Outline"),
		Button({ variant: "ghost" }, "Ghost"),
		Button({ variant: "destructive" }, "Destructive"),
		Button({ variant: "link" }, "Link"),
		Button({ variant: "outline", size: "sm" }, PlusIcon({ "aria-hidden": true }), "New"),
		Button({ size: "icon", "aria-label": "Add" }, PlusIcon({ "aria-hidden": true })),
		Button({ disabled: true }, "Disabled"),
	);
}

Installation

npx jsrepo add @implementjs/ui/button

It installs tailwind-variants at the same time.

Usage

buttonVariants is the part that travels. A dialog trigger, a popover close, the calendar's arrows, a toast action — none of them are Button, but all of them render through this table, which is why one edit here restyles the whole registry.

import { Button, buttonVariants } from "@/lib/components/ui/button";

Button({ variant: "outline", size: "sm" }, "Save");

// the same styles on something that is not a button
A({ href: "/docs", class: buttonVariants({ variant: "link" }) }, "Read the docs");

Icons

An icon in a button is sized and made non-interactive by the base styles, so it needs no classes of its own. Give an icon-only button an aria-label — there is no text to name it:

Button({ size: "icon", "aria-label": "Add" }, PlusIcon({ "aria-hidden": true }));

has-[>svg] trims the horizontal padding when a button holds both an icon and a label, so the pair stays optically centered.

Overriding

class is merged with the variant table, not appended to it, so a utility you pass wins over the one the variant baked in:

Button({ size: "icon", class: "size-20" }); // 20, not 9
Button({ variant: "outline", class: "border-destructive" });

That holds for every component in the registry — see Merging classes.

Variants elsewhere

ButtonVariant and ButtonSize are exported as types. Components that put a button somewhere accept them by those names — DialogTrigger({ variant: "destructive" }) reaches the same table.

API Reference

Button

The button. `buttonVariants` is exported alongside it, which is how the parts of other components that render as buttons — a dialog trigger, a calendar's arrows — borrow the same styles. Renders a Button; extra props are forwarded onto it.

PropTypeDefaultDescription
variant"default" | "destructive" | "outline" | "secondary" | "ghost" | "link""default"Which button style to render. Also set as data-variant.
size"default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg""default"Height and padding. The icon sizes are square and drop the horizontal padding. Also set as data-size.
Data attributeValue
[data-slot]"button"
[data-variant]"default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
[data-size]"default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg"