Button
Primary click target with six visual variants, eight size presets (including icon-only), and an `asChild` escape hatch for polymorphic rendering.
Overview
Button is the cornerstone interactive primitive. A single CVA factory drives its visual surface (variant) and footprint (size), so any combination is a one-prop change. Use it for any direct user action: submit, confirm, navigate, dismiss, trigger.
When the action needs to render as a link, list item, or any non-button host, set asChild and the Radix Slot will forward all button styles and props onto the first child.
Preview
'use client';import { Button } from '@gremorie/rx-forms';export function ButtonPreview() { return ( <div className="flex flex-wrap items-center gap-3"> <Button>Default</Button> <Button variant="secondary">Secondary</Button> <Button variant="outline">Outline</Button> <Button variant="ghost">Ghost</Button> <Button variant="link">Link</Button> <Button variant="destructive">Destructive</Button> </div> );}Anatomy
Button single <button> (or asChild child) styled by buttonVariants; SVG children auto-sized to size-4Installation
bash npx gremorie@latest add rx-button bash pnpm dlx gremorie@latest add rx-button bash yarn dlx gremorie@latest add rx-button bash bunx --bun gremorie@latest add rx-button Usage
import { Button } from "@gremorie/rx-forms";
export function Example() {
return <Button onClick={handleClick}>Save changes</Button>;
}npx gremorie@latest add ng-buttonimport { Component } from '@angular/core';
import { Button } from '@gremorie/ng-forms';
@Component({
selector: 'app-example',
standalone: true,
imports: [Button],
template: `<ai-button (pressedChange)="handleClick()"
>Save changes</ai-button
>`,
})
export class ExampleComponent {
handleClick() {
// ...
}
}API
<Button>
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "default" | Visual treatment. |
size | "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | "default" | Footprint preset. Icon sizes are square (size-*) with no horizontal padding. |
asChild | boolean | false | When true, renders via Radix Slot.Root; the first child receives all button styles, props and events. |
disabled | boolean | false | Standard HTML attribute. Adds pointer-events-none and opacity-50. |
Extends all React.ComponentProps<"button">. Forwards a data-slot="button", data-variant, and data-size for downstream composition (e.g. ButtonGroup, InputGroup).
buttonVariants
Exported CVA factory so other primitives can reuse the button surface without rendering an actual <button>. Calendar nav arrows and InputGroupButton consume it directly.
import { buttonVariants } from '@gremorie/rx-forms';
<a className={buttonVariants({ variant: 'outline', size: 'sm' })} href="/docs">
Read more
</a>;Composition
<Button>is the leaf. It owns its visual surface and accessibility (disabled, focus ring,aria-invalid).- Icons inside are auto-sized via CSS (
[&_svg:not([class*='size-'])]:size-4) and givenpointer-events-noneso the button stays the click target. asChildlets you keep the visual treatment while rendering as<a>,<Link>,<NavLink>, or any other host element.
Variations
All variants
Showcase every variant for visual reference. default for primary actions, destructive for irreversible ones, outline and secondary for de-emphasized siblings, ghost for toolbar-style hosts, link for inline navigation that should look like prose.
<div className="flex flex-wrap gap-3">
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="destructive">Destructive</Button>
</div>Sizes
Four footprint presets - xs, sm, default, lg - keep dense toolbars and primary CTAs visually consistent. Icon-only square sizes (icon-xs through icon-lg) are shown below.
'use client';import { Button } from '@gremorie/rx-forms';export function ButtonSizesPreview() { return ( <div className="flex flex-wrap items-center gap-3"> <Button size="xs">Extra small</Button> <Button size="sm">Small</Button> <Button size="default">Default</Button> <Button size="lg">Large</Button> </div> );}Button with leading icon
Drop an icon as a child. CSS auto-sizes it and tightens the horizontal padding via has-[>svg]:px-3.
'use client';import { Button } from '@gremorie/rx-forms';import { Download } from 'lucide-react';export function ButtonIconPreview() { return ( <Button> <Download /> Download report </Button> );}Icon-only button
Use size="icon" (or icon-xs, icon-sm, icon-lg) for square buttons. Always pair with an aria-label so screen readers announce the action.
When an icon button sits next to another control, match height steps through the size variants (icon-sm with a size="sm" select or button, icon with the defaults) instead of forcing heights with classes. See Action rows and size pairing.
'use client';import { Button } from '@gremorie/rx-forms';import { Trash2 } from 'lucide-react';export function ButtonIconOnlyPreview() { return ( <div className="flex flex-wrap items-center gap-3"> <Button size="icon-xs" variant="ghost" aria-label="Delete row"> <Trash2 /> </Button> <Button size="icon-sm" variant="ghost" aria-label="Delete row"> <Trash2 /> </Button> <Button size="icon" variant="ghost" aria-label="Delete row"> <Trash2 /> </Button> <Button size="icon-lg" variant="ghost" aria-label="Delete row"> <Trash2 /> </Button> </div> );}Disabled
disabled removes the button from the tab order, drops opacity to 50%, and blocks pointer events. It applies across every variant.
'use client';import { Button } from '@gremorie/rx-forms';export function ButtonDisabledPreview() { return ( <div className="flex flex-wrap items-center gap-3"> <Button disabled>Default</Button> <Button variant="outline" disabled> Outline </Button> <Button variant="destructive" disabled> Destructive </Button> </div> );}asChild for navigation
Render the button styles onto a <Link> or <a> so the element semantics match the destination (router.push on click, working middle-click, SEO-friendly href).
import Link from 'next/link';
<Button asChild>
<Link href="/dashboard">Open dashboard</Link>
</Button>;Accessibility
- Keyboard: native
<button>acceptsEnterandSpace.asChildpreserves whatever semantics the host element provides. - Focus: 3px ring driven by
focus-visible:ring-ring/50, so it appears only for keyboard navigation, never on click. - Disabled:
disabledremoves the element from the tab order, drops opacity to 50%, and appliespointer-events-noneso the button can't be activated by mouse either. - Invalid:
aria-invalid="true"switches the focus ring to the destructive token. - Icon-only: always provide
aria-label. Without it, screen readers announce the button with no name.
Related
- Button Group - join several buttons into a single border-shared cluster
- Input Group - embed
InputGroupButtoninside an input - Toggle - two-state button (
aria-pressed) for stateful actions - Field - composes Button with
FormControlfor submit affordances