FewTone UI
ComponentsDocs

Getting started

  • Introduction
  • Installation
  • Theming

Components4

  • ButtonNew
  • InputNew
  • SelectNew
  • SwitchNew
© 2026 FewTone · MIT licenceGitHub
  1. Components
  2. Button

ButtonNew

pnpm dlx shadcn@latest add …/r/button.json

An accessible React button with six variants, three sizes, icon support and a spring press, built on a native button.

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

export function Actions() {
  return <Button>Continue</Button>
}
default.tsx
import { Button } from "@/components/ui/button";

export default function ButtonDefault() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <Button>Continue</Button>
      <Button variant="outline">Cancel</Button>
    </div>
  );
}

Installation

  1. 1

    Install the dependencies.

    Package manager
    $ pnpm add class-variance-authority radix-ui
  2. 2

    Add the FewTone UI tokens to your global CSS file. Skip any variable your project already defines.

    app/globals.css
    :root {
      --size-control-sm: 2rem;
      --size-control-md: 2.25rem;
      --size-control-lg: 2.5rem;
      --size-icon-sm: 0.875rem;
      --size-icon-md: 1rem;
      --size-icon-lg: 1.25rem;
      --duration-instant: 80ms;
      --duration-fast: 150ms;
      --duration-base: 220ms;
      --duration-slow: 350ms;
      --duration-slower: 500ms;
      --z-popover: 1400;
      --focus-ring-width: 2px;
      --focus-ring-offset: 2px;
      --opacity-disabled: 0.5;
      --scale-press: 0.97;
      --ease-spring: linear(0, 0.157, 0.48, 0.77, 0.97, 1.07, 1.1, 1.09, 1.05, 1.02, 1, 0.995, 1);
      --destructive-foreground: oklch(1 0 0);
    }
    
    .dark {
      --destructive-foreground: oklch(1 0 0);
    }
    
    @theme inline {
      --animate-popover-in: popover-in var(--duration-fast) cubic-bezier(0.22, 1, 0.36, 1) both;
      --animate-popover-out: popover-out var(--duration-instant) cubic-bezier(0.55, 0, 1, 0.45) both;
      --color-destructive-foreground: var(--destructive-foreground);
      @keyframes popover-in {
        from {
          opacity: 0;
          transform: scale(0.96);
        }
        to {
          opacity: 1;
          transform: scale(1);
        }
      }
      @keyframes popover-out {
        from {
          opacity: 1;
          transform: scale(1);
        }
        to {
          opacity: 0;
          transform: scale(0.96);
        }
      }
    }
    
    @media (prefers-reduced-motion: reduce) {
      :root {
        --duration-instant: 1ms;
        --duration-fast: 1ms;
        --duration-base: 1ms;
        --duration-slow: 1ms;
        --duration-slower: 1ms;
      }
    }
  3. 3

    Copy the source into your project.

    components/ui/button.tsx
    import { cn } from "@/lib/utils";
    import { cva, type VariantProps } from "class-variance-authority";
    import { Slot } from "radix-ui";
    import type { ComponentProps } from "react";
    
    const buttonVariants = cva(
      [
        "inline-flex shrink-0 items-center justify-center gap-2 rounded-lg text-sm font-medium whitespace-nowrap",
        "transition duration-(--duration-fast) ease-out",
        // Press: a quick scale on a spring curve. Plain CSS, so it works on the server and with asChild.
        "active:scale-(--scale-press) active:ease-(--ease-spring)",
        "outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)",
        "disabled:pointer-events-none disabled:opacity-(--opacity-disabled)",
        "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--size-icon-md)",
      ],
      {
        variants: {
          variant: {
            default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
            secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
            outline: "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
            ghost: "hover:bg-accent hover:text-accent-foreground",
            destructive:
              "bg-destructive text-destructive-foreground shadow-xs outline-destructive hover:bg-destructive/90",
            link: "text-primary underline-offset-4 hover:underline",
          },
          size: {
            sm: "h-(--size-control-sm) gap-1.5 rounded-md px-3",
            md: "h-(--size-control-md) px-4",
            lg: "h-(--size-control-lg) px-5",
            icon: "size-(--size-control-md)",
            "icon-sm": "size-(--size-control-sm) rounded-md",
            "icon-lg": "size-(--size-control-lg)",
          },
        },
        defaultVariants: { variant: "default", size: "md" },
      },
    );
    
    export type ButtonProps = ComponentProps<"button"> &
      VariantProps<typeof buttonVariants> & {
        /** Render the single child instead of a button, for example a link, keeping the button styles */
        asChild?: boolean;
      };
    
    /**
     * A button for actions. It renders a native `button` (type "button" unless you set one), or with
     * `asChild` its single child, so a link can look like a button without nesting interactive elements.
     */
    function Button({ className, variant, size, asChild = false, type, ...props }: ButtonProps) {
      const Comp = asChild ? Slot.Root : "button";
      return (
        <Comp
          data-slot="button"
          data-variant={variant ?? "default"}
          data-size={size ?? "md"}
          type={asChild ? undefined : (type ?? "button")}
          className={cn(buttonVariants({ variant, size }), className)}
          {...props}
        />
      );
    }
    
    export { Button, buttonVariants };
  4. 4

    Update the import paths to match your project.

Examples

Variants

Default for the main action, secondary and outline for the rest, destructive for actions that remove things.

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

export default function ButtonVariants() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <Button>Default</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="destructive">Delete</Button>
      <Button variant="link">Link</Button>
    </div>
  );
}

Sizes

Three heights, plus square icon buttons. Icon-only buttons need an aria-label.

sizes.tsx
import { Button } from "@/components/ui/button";
import { PlusIcon } from "lucide-react";

export default function ButtonSizes() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <Button size="sm">Small</Button>
      <Button>Medium</Button>
      <Button size="lg">Large</Button>
      <Button size="icon-sm" variant="outline" aria-label="Add item">
        <PlusIcon />
      </Button>
      <Button size="icon" variant="outline" aria-label="Add item">
        <PlusIcon />
      </Button>
      <Button size="icon-lg" variant="outline" aria-label="Add item">
        <PlusIcon />
      </Button>
    </div>
  );
}

With an icon

Icons sit before or after the label and size themselves.

with-icon.tsx
import { Button } from "@/components/ui/button";
import { ArrowRightIcon, DownloadIcon } from "lucide-react";

export default function ButtonWithIcon() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <Button variant="outline">
        <DownloadIcon aria-hidden />
        Download
      </Button>
      <Button>
        Next step
        <ArrowRightIcon aria-hidden />
      </Button>
    </div>
  );
}

As a link

With asChild, the button styles go onto its child, so a link stays a real link.

View on GitHub
as-link.tsx
import { Button } from "@/components/ui/button";
import { ArrowUpRightIcon } from "lucide-react";

export default function ButtonAsLink() {
  return (
    <Button asChild variant="outline">
      <a href="https://github.com/FewTone/ui" target="_blank" rel="noreferrer">
        View on GitHub
        <ArrowUpRightIcon aria-hidden />
      </a>
    </Button>
  );
}

API reference

Also accepts every native button prop. Exports buttonVariants for styling other elements.

PropTypeDefaultDescription
variant?"default" | "secondary" | "outline" | "ghost" | "destructive" | "link""default"The visual style.
size?"sm" | "md" | "lg" | "icon" | "icon-sm" | "icon-lg""md"Height and padding. Icon sizes are square.
asChild?booleanfalseRender the single child (for example a link) with the button styles instead of a button.
type?"button" | "submit" | "reset""button"The native button type. Defaults to button so it never submits a form by accident.

Related components

  • InputNew

    An accessible React text input with three sizes and clear invalid, disabled and file states, built on a native input.

  • SelectNew

    An accessible React select with a gliding highlight, groups, labels and a scrollable list, built on Radix Select.

  • SwitchNew

    An accessible React switch for on/off settings, with a spring-animated thumb, built on Radix Switch.

Last updated 13 Oct 2026Edit on GitHub