UIUpdated April 28, 2026

Button

Action button component used across PyColors UI, built on shadcn/ui. Includes variants, sizes, accessibility, and product usage rules.

UIButton

Actions users can trustLink to section

The Button component is the primary action primitive in PyColors UI.

Use it for:

  • submitting forms
  • triggering dialogs
  • opening sheets
  • starting checkout
  • saving changes
  • confirming destructive actions
  • navigating when an element should visually behave like a button

This implementation extends the shadcn/ui Button primitive and is aligned with the PyColors design system: semantic color tokens, radius, shadows, and focus rings.

Core idea

Buttons communicate intent. Choose the variant based on action priority, not decoration.

ImportLink to section

button-import.tsx
import { Button } from "@pycolors/ui";

Basic usageLink to section

basic-button.tsx
import { Button } from "@pycolors/ui";

export function BasicButton() {
  return <Button>Click me</Button>;
}

The default button uses primary semantic tokens and adapts automatically to light and dark mode.

Product action modelLink to section

Buttons are one of the highest-frequency interaction primitives inside a SaaS product.

They influence:

  • conversion clarity
  • workflow confidence
  • destructive action safety
  • onboarding friction
  • perceived product quality

Mental model

A button is not decoration. It is an interaction commitment.

When to use ButtonLink to section

Use Button when the user is taking an action.

Primary actions

Use for submit, save, continue, checkout, create, or upgrade actions.

Secondary actions

Use for cancel, view details, learn more, or alternative paths.

Risky actions

Use destructive styling only for dangerous or irreversible actions.

Decision modelLink to section

Primary actions

Primary buttons represent the highest-priority action inside a product surface.

UpgradeCheckoutContinue

VariantsLink to section

Buttons support variants for action priority and semantic meaning.

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

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

Available variantsLink to section

VariantPurpose
defaultPrimary action, highest emphasis
secondaryNeutral alternative action
outlineMedium-emphasis action
ghostLow-emphasis action in dense UI
destructiveDangerous or irreversible action
linkText-style inline action

Variant rule

A page should usually have one clear primary action. Use secondary, outline, or ghost for supporting actions.

SizesLink to section

Button supports standard sizes and icon-only sizes.

button-sizes.tsx
import { Button } from "@pycolors/ui";

function ClockIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 20 20" fill="currentColor" {...props}>
      <path d="M10 1a9 9 0 100 18 9 9 0 000-18zM9 5a1 1 0 012 0v5a1 1 0 01-.293.707l-3 3a1 1 0 11-1.414-1.414L9 9.586V5z" />
    </svg>
  );
}

function DotIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 20 20" fill="currentColor" {...props}>
      <path d="M10 18a8 8 0 100-16 8 8 0 000 16z" />
    </svg>
  );
}

export function ButtonSizes() {
  return (
    <div className="flex flex-wrap items-center gap-4">
      <Button size="sm">Small</Button>
      <Button size="default">Default</Button>
      <Button size="lg">Large</Button>

      <Button size="icon" aria-label="History">
        <ClockIcon aria-hidden="true" />
      </Button>

      <Button size="icon-sm" aria-label="Small icon button">
        <DotIcon aria-hidden="true" />
      </Button>

      <Button size="icon-lg" aria-label="Large icon button">
        <DotIcon aria-hidden="true" />
      </Button>
    </div>
  );
}

Available sizesLink to section

SizePurpose
smCompact buttons for dense product UIs
defaultStandard button size
lgStronger visual weight
iconSquare icon-only button
icon-smCompact icon-only button
icon-lgLarger icon-only button

Size rule

Keep button sizes consistent inside the same section. Use lg for major CTAs and sm for dense product actions.

Disabled stateLink to section

Use disabled state when an action is unavailable.

button-disabled.tsx
import { Button } from "@pycolors/ui";

export function ButtonDisabled() {
  return (
    <div className="flex flex-wrap gap-4">
      <Button disabled>Disabled</Button>
      <Button variant="secondary" disabled>Disabled</Button>
      <Button variant="outline" disabled>Disabled</Button>
    </div>
  );
}

Disabled buttons use reduced opacity and remove pointer interaction.

UX rule

When disabling a button, make sure the user can understand what is required to enable it.

With iconLink to section

Use icons when they reinforce the action.

The Button applies a default size-4 to nested SVG icons that do not already have a size-* class.

button-leading-icon.tsx
import { Button } from "@pycolors/ui";
import { Plus } from "lucide-react";

export function ButtonLeadingIcon() {
  return (
    <Button>
      <Plus className="mr-2 size-4" aria-hidden="true" />
      Add item
    </Button>
  );
}
button-trailing-icon.tsx
import { Button } from "@pycolors/ui";
import { ArrowRight } from "lucide-react";

export function ButtonTrailingIcon() {
  return (
    <Button>
      Continue
      <ArrowRight className="ml-2 size-4" aria-hidden="true" />
    </Button>
  );
}

Icon rule

Icons should support the label. For icon-only buttons, always provide an aria-label.

Validation stateLink to section

The Button supports invalid visual state through aria-invalid.

This can be useful for submit actions when the related form is invalid.

button-invalid.tsx
import { Button } from "@pycolors/ui";

export function ButtonInvalid() {
  return (
    <div className="flex flex-wrap gap-4">
      <Button aria-invalid="true">Fix errors</Button>
      <Button variant="outline" aria-invalid="true">
        Review
      </Button>
    </div>
  );
}

With iconLink to section

Use asChild to render the button as another element while preserving button styling.

This is commonly used for links and router components.

button-as-child.tsx
import { Button } from "@pycolors/ui";

export function ButtonAsChild() {
  return (
    <Button asChild>
      <a href="#docs">Go to docs</a>
    </Button>
  );
}

Semantics rule

If the action navigates, render an anchor or router link. If it submits or mutates state, render a button.

Usage patternsLink to section

Choose the action priority

Use default for the main action, then choose secondary, outline, or ghost for supporting actions.

Use semantic intent

Use destructive only when the action is dangerous or irreversible.

Keep labels action-oriented

Prefer clear verbs such as Save, Continue, Create project, Upgrade, Delete.

Pair loading with state

During async work, disable the button and update the label to show progress.

Keep icon-only buttons accessible

Always provide aria-label and mark decorative icons as aria-hidden.

Product decision guideLink to section

Mutation

Use Button

Buttons should trigger actions, mutations, submissions, confirmations, or workflow progression.
  • save data
  • submit forms
  • open overlays
  • trigger async actions
Navigation

Use Link

Links should navigate between pages, routes, or external resources.
  • page navigation
  • external docs
  • route changes
  • resource links
SituationUse
Submit a formButton
Trigger a mutationButton
Open a Dialog or SheetButton
Navigate to another URLLink styled with Button asChild
Show metadata or statusBadge
Inline text navigationLink
Compact row actionsDropdownMenu trigger button

Decision rule

Button is for intent. Link is for navigation. Badge is for metadata.

APILink to section

PropTypeDefaultDescription
variantButtonVariant"default"Visual style
sizeButtonSize"default"Button size
asChildbooleanfalseRender using Radix Slot
disabledbooleanfalseDisable interaction
classNamestringAdditional Tailwind classes

Button extends standard HTML button props.

button-props.ts
React.ComponentProps<"button">

TypeScript typesLink to section

button-types.ts
export type ButtonVariant =
  | "default"
  | "secondary"
  | "outline"
  | "ghost"
  | "destructive"
  | "link";

export type ButtonSize =
  | "default"
  | "sm"
  | "lg"
  | "icon"
  | "icon-sm"
  | "icon-lg";

AccessibilityLink to section

  • Use clear, descriptive labels.
  • Preserve keyboard focus styles.
  • Use icon-only buttons only with an accessible aria-label.
  • Mark decorative icons with aria-hidden="true".
  • Use disabled for unavailable actions.
  • Do not rely on color alone to communicate destructive or invalid state.
  • Use links for navigation and buttons for actions.

Accessibility rule

Every button must have an accessible name. Visible text is best; aria-label is required for icon-only buttons.

Prefer / avoidLink to section

Prefer

  • one clear primary action per section
  • short verb-first labels
  • consistent sizes in the same view
  • asChild for links that look like buttons
  • accessible labels for icon-only buttons

Avoid

  • multiple competing primary buttons
  • destructive styling for non-destructive actions
  • icon-only buttons without labels
  • buttons used as navigation without link semantics
  • vague labels like “OK” or “Submit” when context is unclear

Product copy guidelinesLink to section

Button labels should communicate intent clearly and reduce hesitation.

Good labels

Strong labels are explicit, action-oriented, and outcome-focused.

Save changesCreate projectUpgrade to ProInvite memberDelete project

Common questionsLink to section