Button
Action button component used across PyColors UI, built on shadcn/ui. Includes variants, sizes, accessibility, and product usage rules.
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
import { Button } from "@pycolors/ui";Basic usageLink to section
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
Secondary actions
Risky actions
Decision modelLink to section
Primary actions
Primary buttons represent the highest-priority action inside a product surface.
VariantsLink to section
Buttons support variants for action priority and semantic meaning.
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
| Variant | Purpose |
|---|---|
default | Primary action, highest emphasis |
secondary | Neutral alternative action |
outline | Medium-emphasis action |
ghost | Low-emphasis action in dense UI |
destructive | Dangerous or irreversible action |
link | Text-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.
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
| Size | Purpose |
|---|---|
sm | Compact buttons for dense product UIs |
default | Standard button size |
lg | Stronger visual weight |
icon | Square icon-only button |
icon-sm | Compact icon-only button |
icon-lg | Larger 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.
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.
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>
);
}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.
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.
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
Use Button
- save data
- submit forms
- open overlays
- trigger async actions
Use Link
- page navigation
- external docs
- route changes
- resource links
Button vs link vs badgeLink to section
| Situation | Use |
|---|---|
| Submit a form | Button |
| Trigger a mutation | Button |
| Open a Dialog or Sheet | Button |
| Navigate to another URL | Link styled with Button asChild |
| Show metadata or status | Badge |
| Inline text navigation | Link |
| Compact row actions | DropdownMenu trigger button |
Decision rule
Button is for intent. Link is for navigation. Badge is for metadata.
APILink to section
| Prop | Type | Default | Description |
|---|---|---|---|
variant | ButtonVariant | "default" | Visual style |
size | ButtonSize | "default" | Button size |
asChild | boolean | false | Render using Radix Slot |
disabled | boolean | false | Disable interaction |
className | string | — | Additional Tailwind classes |
Button extends standard HTML button props.
React.ComponentProps<"button">TypeScript typesLink to section
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
disabledfor 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
asChildfor 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.