Skip to main content
Gremorie

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-4

Installation

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-button
import { 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>

PropTypeDefaultDescription
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.
asChildbooleanfalseWhen true, renders via Radix Slot.Root; the first child receives all button styles, props and events.
disabledbooleanfalseStandard 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

  1. <Button> is the leaf. It owns its visual surface and accessibility (disabled, focus ring, aria-invalid).
  2. Icons inside are auto-sized via CSS ([&_svg:not([class*='size-'])]:size-4) and given pointer-events-none so the button stays the click target.
  3. asChild lets 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> accepts Enter and Space. asChild preserves 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: disabled removes the element from the tab order, drops opacity to 50%, and applies pointer-events-none so 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.
  • Button Group - join several buttons into a single border-shared cluster
  • Input Group - embed InputGroupButton inside an input
  • Toggle - two-state button (aria-pressed) for stateful actions
  • Field - composes Button with FormControl for submit affordances

On this page