Button

A hand-drawn Base UI button styled with shadcn conventions. Every button draws its outline (and, for secondary, its fill) through RoughJS, so it needs a SketchProvider above it in the tree.

Button
Every hand-drawn button variant, size and state.
Variants
Sizes
Icon sizes
With icons
Disabled

Installation

npx shadcn@latest add https://sketchcn.chuwii.com/r/button.json

Styles

Button leans on these rules from sketch.css. The shadcn CLI installs the file with sketch-provider; copying the component by hand means copying them too.

[data-sketch-outline] path {
  stroke-dasharray: var(--sketch-dash, none);
  animation: var(--sketch-dash-animation, none);
}

@keyframes sketch-dash-boil {
  to {
    stroke-dashoffset: -10;
  }
}

@keyframes sketch-icon-wiggle {
  0%,
  100% {
    transform: rotate(-5deg);
  }

  50% {
    transform: rotate(5deg);
  }
}

[data-sketch-outline] path[fill]:not([fill="none"]) {
  fill: var(--sketch-fill);
  fill-opacity: var(--sketch-fill-opacity, 1);
  transition:
    fill 150ms ease,
    fill-opacity 150ms ease;
}

Read the whole file for the drawing variables, paper textures and reduced-motion rules.

Usage

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

export function SaveAction() {
  return <Button onClick={save}>Save sketch</Button>;
}

Variants

VariantWhen to use
defaultPrimary action. Filled sketch background.
outlineSecondary action on a plain surface.
secondaryMuted action, sketch-filled with the secondary color.
ghostLow-emphasis action. Outline boils on hover.
destructiveIrreversible actions such as delete.
linkInline text action. Draws a sketch underline instead of a box.

Sizes

Text sizes: xs, sm, default, lg.

Icon-only sizes: icon-xs, icon-sm, icon, icon-lg. Pass an aria-label when the button has no text.

<Button size="icon-sm" aria-label="Edit">
  <Pencil />
</Button>

Props

PropTypeDefaultDescription
variant"default" | "outline" | "secondary" | "ghost" | "destructive" | "link""default"Visual style.
size"xs" | "sm" | "default" | "lg" | "icon-xs" | "icon-sm" | "icon" | "icon-lg""default"Height, padding and icon scale.

Everything else, including render and disabled, is forwarded to the Base UI Button primitive and documented there.

The component also exports buttonVariants, BUTTON_VARIANTS, BUTTON_SIZES and ICON_BUTTON_SIZES for building your own variant lists.

Examples

Form actions

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

export function FormActions() {
  return (
    <div className="flex justify-end gap-2">
      <Button variant="ghost">Cancel</Button>
      <Button type="submit">
        <Plus />
        Save sketch
      </Button>
    </div>
  );
}
import { Link } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";

export function DocsLink() {
  return (
    <Button variant="outline" render={<Link to="/docs" />}>
      Read the docs
      <ArrowRight />
    </Button>
  );
}

Destructive row action

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

export function DeleteRow({ onDelete }: { onDelete: () => void }) {
  return (
    <Button variant="destructive" size="sm" onClick={onDelete}>
      <Trash2 />
      Delete
    </Button>
  );
}

Animation

Two keyframes from sketch.css, both applied as Tailwind arbitrary animations.

// Ghost variant: the dashed outline boils while hovered
"[--sketch-dash:6_4] hover:[--sketch-dash-animation:sketch-dash-boil_1s_steps(2)_infinite]"

// Every variant: the icon wiggles while the button is held
"active:[&>svg:not([data-sketch-outline]):not([data-sketch-bg])]:animate-[sketch-icon-wiggle_180ms_ease-in-out_infinite]"

Drop either by overriding the variable or the animation on the instance.

<Button variant="ghost" className="hover:[--sketch-dash-animation:none]">
  Still outline
</Button>