defuss-shadcn / Actions / button
ButtonATM
Triggers an action or event. Six variants, four sizes. Uses <button> or <a>. Zero JavaScript required.
On this page (16)
§Variants
Six styles via data-variant. Default has a solid primary fill.
§Sizes
The full five-step scale via data-size - the default equals md - plus icon-only variants.
§Icon
Square button for icon-only actions. Requires aria-label.
§With Icon
Icons auto-size to 1rem. Place before or after the label.
§Loading
aria-busy + a .spinner child marks a pending action. An enabled busy button spins; a disabled one freezes the spinner (the dimming carries the pending signal) - button.css pauses .spinner under :disabled.
§Disabled
50% opacity, no pointer events.
§As Link
Use <a> instead of <button> for navigation.
§Rounded
Add style='border-radius:9999px' for pill-shaped buttons.
§Soft and dashed
Two more styles: soft is a tint of the color with the text in it; dashed reads as 'add something here'.
§Colors
data-tone paints any style in success, warning, info or destructive - solid fills with the color, the other styles write in it (mixed toward the text color, so amber stays readable).
§Custom colors
data-tone='custom' takes your own color from --btn-color (and the text on a solid fill from --btn-color-fg) - every style follows it.
§Emojis
An emoji is text - it sits in the label like an icon. For an emoji-only button, hide the glyph and name the button with aria-label.
§Frames
The frame-* classes from shapes.css replace the button's border - double, offset, corners, sketch, gradient, stitched.
§Aura
Wrap a button in an .aura (shapes.css) for an animated light around it - set --shape-round to the button's radius and display:inline-grid so it hugs the button.
§Left-to-right and right-to-left
Buttons are logical: an icon before the label leads in either direction. Mark directional icons (arrows, chevrons) data-rtl-flip and they mirror under dir='rtl'; symmetric icons stay put.
§CSS view file
/* -- Button component ------------------------------------------ */@layer components { .btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; white-space: nowrap; font-size: 0.875rem; font-weight: 500; font-family: var(--font-sans); line-height: 1; border-radius: var(--radius-md); border: 1px solid transparent; cursor: pointer; text-decoration: none; transition: all 150ms ease; outline: none; flex-shrink: 0; height: 2.25rem; padding: 0 1rem; background-color: var(--primary); color: var(--primary-foreground); & svg { pointer-events: none; flex-shrink: 0; &:not([class*="size-"]) { width: 1rem; height: 1rem; } } /* a loading .spinner inside a button takes the button's own text color — spinner.css defaults it to --muted-foreground, invisible on a primary */ & .spinner { color: inherit; } &:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; } &:disabled, &[aria-disabled="true"] { pointer-events: none; opacity: 0.5; } /* Loading state (aria-busy + a .spinner child): a disabled button must freeze its spinner - "pending" is signaled by the dimming, a frozen wheel is the accepted pattern; only an ENABLED busy button spins. */ &:disabled .spinner, &[aria-disabled="true"] .spinner { animation-play-state: paused; } /* -- Variants -------------------------------------------- */ &[data-variant="default"] { background-color: var(--primary); color: var(--primary-foreground); &:hover { opacity: 0.9; } } &[data-variant="secondary"] { background-color: var(--secondary); color: var(--secondary-foreground); /* --secondary is near-white in light themes, so an opacity fade would wash the button further into the page background (invisible hover). Mixing in the foreground color instead darkens light themes and lightens dark ones - always moving away from the page surface. */ &:hover { background-color: color-mix(in oklch, var(--secondary) 85%, var(--secondary-foreground)); } } &[data-variant="outline"] { background-color: var(--background); color: var(--foreground); border: 1px solid var(--border); box-shadow: var(--shadow-xs); &:hover { background-color: var(--accent); color: var(--accent-foreground); } } &[data-variant="ghost"] { background-color: transparent; color: var(--foreground); border-color: transparent; &:hover { background-color: var(--accent); color: var(--accent-foreground); } } &[data-variant="destructive"] { background-color: var(--destructive); color: var(--destructive-foreground); &:hover { opacity: 0.9; } } &[data-variant="link"] { background-color: transparent; color: var(--primary); height: auto; padding: 0; text-underline-offset: 4px; &:hover { text-decoration: underline; } } /* soft: a tint of the color, text in the color */ &[data-variant="soft"] { background-color: color-mix(in oklch, var(--primary) 12%, transparent); color: color-mix(in oklch, var(--primary) 80%, var(--foreground)); &:hover { background-color: color-mix(in oklch, var(--primary) 20%, transparent); } } /* dashed: a drop-zone / "add" look */ &[data-variant="dashed"] { background-color: transparent; color: var(--foreground); border: 1px dashed color-mix(in oklch, var(--foreground) 35%, transparent); &:hover { background-color: var(--accent); color: var(--accent-foreground); border-style: solid; } } /* -- Tones: any variant in another color ------------------- data-tone="success" / "warning" / "info" / "destructive" - or data-tone="custom" with style="--btn-color: …; --btn-color-fg: …" (the text on a solid fill, default white). Solid buttons fill with the tone; outline / ghost / soft / dashed / link write in it, mixed toward the foreground so a light tone (amber) stays readable. */ &[data-tone] { --_tone: var(--btn-color, var(--primary)); --_tone-fg: var(--btn-color-fg, #fff); --_tone-ink: color-mix(in oklch, var(--_tone) 78%, var(--foreground)); } &[data-tone="success"] { --_tone: oklch(0.6 0.15 150); } &[data-tone="warning"] { --_tone: oklch(0.8 0.16 80); --_tone-fg: oklch(0.28 0.06 60); } &[data-tone="info"] { --_tone: oklch(0.6 0.16 250); } &[data-tone="destructive"] { --_tone: var(--destructive); --_tone-fg: var(--destructive-foreground, #fff); } &[data-tone]:is(:not([data-variant]), [data-variant="default"]) { background-color: var(--_tone); color: var(--_tone-fg); &:hover { opacity: 0.9; } } &[data-tone][data-variant="outline"] { background-color: transparent; color: var(--_tone-ink); border-color: var(--_tone); &:hover { background-color: color-mix(in oklch, var(--_tone) 12%, transparent); color: var(--_tone-ink); } } &[data-tone]:is([data-variant="ghost"], [data-variant="dashed"]) { color: var(--_tone-ink); &:hover { background-color: color-mix(in oklch, var(--_tone) 14%, transparent); color: var(--_tone-ink); } } &[data-tone][data-variant="dashed"] { border-color: var(--_tone); } &[data-tone][data-variant="soft"] { background-color: color-mix(in oklch, var(--_tone) 16%, transparent); color: var(--_tone-ink); &:hover { background-color: color-mix(in oklch, var(--_tone) 26%, transparent); } } &[data-tone][data-variant="link"] { color: var(--_tone-ink); } /* -- RTL: arrows and other directional icons mirror ---------- mark them data-rtl-flip; symmetric icons stay as they are */ &:dir(rtl) [data-rtl-flip] { scale: -1 1; } /* -- Sizes ----------------------------------------------- */ &[data-size="xs"] { height: 1.75rem; padding: 0 0.5rem; font-size: 0.75rem; border-radius: var(--radius-sm); } &[data-size="sm"] { height: 2rem; padding: 0 0.75rem; font-size: 0.8125rem; } &[data-size="md"] { height: 2.25rem; padding: 0 1rem; font-size: 0.875rem; } &[data-size="lg"] { height: 2.75rem; padding: 0 2rem; font-size: 1rem; } &[data-size="xl"] { height: 3.25rem; padding: 0 2.5rem; font-size: 1.125rem; } &[data-size="icon"] { height: 2.25rem; width: 2.25rem; padding: 0; } &[data-size="icon-xs"] { height: 1.75rem; width: 1.75rem; padding: 0; border-radius: var(--radius-sm); } &[data-size="icon-sm"] { height: 2rem; width: 2rem; padding: 0; } &[data-size="icon-lg"] { height: 2.75rem; width: 2.75rem; padding: 0; } &[data-size="icon-xl"] { height: 3.25rem; width: 3.25rem; padding: 0; } } /* -- Accessibility ------------------------------------------ */ @media (prefers-reduced-motion: reduce) { .btn { transition: none; } } @media (prefers-contrast: more) { .btn { border: 2px solid transparent; &[data-variant="default"], &[data-variant="secondary"], &[data-variant="destructive"] { border-color: currentColor; } &[data-variant="outline"], &[data-variant="ghost"] { border-color: var(--foreground); } } } @media (forced-colors: active) { .btn { border: 1px solid ButtonText; &:disabled, &[aria-disabled="true"] { border-color: GrayText; color: GrayText; } } }}Comments, ideas or improvements? Edit this page's source on GitHub