03 / 51
All 51 in 51 CSS Buttons
- 01CSS Submit Button (Form Primary)
- 02CSS Primary and Secondary Button Pair
- 03CSS Ghost / Outline Button
- 04CSS Icon-Only Button with Accessible Tooltip
- 05CSS Gradient CTA Button
- 06CSS Loading Spinner Button (State Transition)
- 07CSS Disabled Button State (Full State Reference)
- 08CSS Button Group Component (Segmented Radio-Like)
- 09CSS Add to Cart Button
- 10CSS Download Progress Button
- 11CSS Like Heart Button
- 12CSS Confirm Delete Button
- 13CSS Play Pause Button
- 14CSS Subscribe Confetti Button
- 15CSS Magnetic Mercury Ripple Button
- 16CSS Brutalist Glitch Button
- 17CSS Neon Plasma Burst Button
- 18CSS Clay Press Button
- 19CSS Editorial Ink Spread Button
- 20CSS Particle Shard Burst Button
- 21CSS Brushed Brass Plate Button
- 22CSS Washi Paper Crane Button
- 23CSS Frosted Ice Cube Button
- 24CSS Vintage Typewriter Key Button
- 25CSS Lacquered Walnut Wood Button
- 26CSS Pharma Gel Capsule Button
- 27CSS Aged Copper Coin Button
- 28CSS Origami Fortune Cookie Button
- 29CSS Holographic Foil Sticker Button
- 30CSS Brutalist Concrete Slab Button
- 31CSS Mercury Bead Morph Button
- 32CSS Soft Pill Modern SaaS Button
- 33CSS Masking Tape Label Button
- 34CSS Obsidian Glass Shard Button
- 35CSS Wine Velvet Ribbon Button
- 36CSS LED Segment Display Button
- 37CSS Cassette Mixtape Label Button
- 38CSS Chalkboard Menu Button
- 39CSS Vinyl Record Sleeve Ripple Button
- 40CSS Polaroid Snapshot Button
- 41CSS Airline Boarding Pass Button
- 42CSS Aged Brass Subway Token Button
- 43CSS Vintage Rx Pharmacy Label Button
- 44CSS Bakelite Radio Dial Toggle Button
- 45CSS Regal Postage Stamp Button
- 46CSS Victorian Cigar Band Foil Button
- 47CSS Vintage Calculator Key Button
- 48CSS Toolbox Latch Spotlight Button
- 49CSS Receipt Paper Total Count-Up Button
- 50CSS IBM Punch Card Magnetic Follow Button
- 51CSS Brass Periscope Lens Button
CSS Ghost / Outline Button
A ghost button that keeps its 1px silhouette at rest and fills from the bottom edge on hover, with the label colour inverting in the same beat. The fill is one background-size transition on a gradient — no pseudo-element, no overflow clipping, and the border is drawn in currentColor so a single colour change re-skins everything.
Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS
ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link
The code
<div class="cb-03">
<div class="cb-03__stage">
<button class="cb-03__btn" type="button">
<span>Read the case study</span>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17 17 7"/><path d="M8.5 7H17v8.5"/></svg>
</button>
</div>
</div><div class="cb-03">
<div class="cb-03__stage">
<button class="cb-03__btn" type="button">
<span>Read the case study</span>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17 17 7"/><path d="M8.5 7H17v8.5"/></svg>
</button>
</div>
</div>@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
.cb-03 {
--bg: #faf9f6;
--accent: #111827;
--on-accent: #f9fafb;
--border-w: 1px;
--radius: 999px;
--sans: "Space Grotesk",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--bg);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
@supports (color: oklch(50% 0 0)) {
.cb-03 {
--bg: oklch(97.8% .006 95);
--accent: oklch(22% .02 265);
--on-accent: oklch(98% .003 265);
}
}
@media (prefers-color-scheme: dark) {
.cb-03:not([data-theme="light"]) {
--bg: #0b0b0c;
--accent: #f4f4f0;
--on-accent: #101012;
}
}
.cb-03[data-theme="dark"] {
--bg: #0b0b0c;
--accent: #f4f4f0;
--on-accent: #101012;
}
.cb-03,
.cb-03 *,
.cb-03 *::before,
.cb-03 *::after {
box-sizing: border-box;
}
.cb-03__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.5rem,6vw,4rem);
}
.cb-03__btn {
cursor: pointer;
display: grid;
grid-auto-flow: column;
align-items: center;
gap: .65rem;
min-height: 3.25rem;
padding: 0 1.75rem;
border: var(--border-w) solid currentColor;
border-radius: var(--radius);
font: inherit;
font-size: .9375rem;
font-weight: 500;
letter-spacing: .01em;
color: var(--accent);
background-color: transparent;
background-image: linear-gradient(var(--accent),var(--accent));
background-repeat: no-repeat;
background-position: 50% 100%;
background-size: 100% 0%;
transition: background-size .42s cubic-bezier(.16,1,.3,1), color .3s ease .06s, letter-spacing .42s cubic-bezier(.16,1,.3,1);
}
.cb-03__btn svg {
width: 1.05rem;
height: 1.05rem;
transition: rotate .42s cubic-bezier(.16,1,.3,1);
}
.cb-03__btn:is(:hover,:focus-visible) {
background-size: 100% 100%;
color: var(--on-accent);
letter-spacing: .03em;
}
.cb-03__btn:is(:hover,:focus-visible) svg {
rotate: 45deg;
}
.cb-03__btn:active {
scale: .985;
}
.cb-03__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 4px;
}
.cb-03__btn:disabled {
cursor: not-allowed;
opacity: .4;
background-size: 100% 0%;
}
@media (prefers-reduced-motion: reduce) {
.cb-03__btn,
.cb-03__btn svg {
transition: color .1s linear, background-size .1s linear;
}
}
@media (forced-colors: active) {
.cb-03__btn {
background: none;
border-color: ButtonText;
color: ButtonText;
}
.cb-03__btn:hover {
background: Highlight;
color: HighlightText;
}
}@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
.cb-03 {
--bg: #faf9f6;
--accent: #111827;
--on-accent: #f9fafb;
--border-w: 1px;
--radius: 999px;
--sans: "Space Grotesk",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--bg);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
@supports (color: oklch(50% 0 0)) {
.cb-03 {
--bg: oklch(97.8% .006 95);
--accent: oklch(22% .02 265);
--on-accent: oklch(98% .003 265);
}
}
@media (prefers-color-scheme: dark) {
.cb-03:not([data-theme="light"]) {
--bg: #0b0b0c;
--accent: #f4f4f0;
--on-accent: #101012;
}
}
.cb-03[data-theme="dark"] {
--bg: #0b0b0c;
--accent: #f4f4f0;
--on-accent: #101012;
}
.cb-03,
.cb-03 *,
.cb-03 *::before,
.cb-03 *::after {
box-sizing: border-box;
}
.cb-03__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.5rem,6vw,4rem);
}
.cb-03__btn {
cursor: pointer;
display: grid;
grid-auto-flow: column;
align-items: center;
gap: .65rem;
min-height: 3.25rem;
padding: 0 1.75rem;
border: var(--border-w) solid currentColor;
border-radius: var(--radius);
font: inherit;
font-size: .9375rem;
font-weight: 500;
letter-spacing: .01em;
color: var(--accent);
background-color: transparent;
background-image: linear-gradient(var(--accent),var(--accent));
background-repeat: no-repeat;
background-position: 50% 100%;
background-size: 100% 0%;
transition: background-size .42s cubic-bezier(.16,1,.3,1), color .3s ease .06s, letter-spacing .42s cubic-bezier(.16,1,.3,1);
}
.cb-03__btn svg {
width: 1.05rem;
height: 1.05rem;
transition: rotate .42s cubic-bezier(.16,1,.3,1);
}
.cb-03__btn:is(:hover,:focus-visible) {
background-size: 100% 100%;
color: var(--on-accent);
letter-spacing: .03em;
}
.cb-03__btn:is(:hover,:focus-visible) svg {
rotate: 45deg;
}
.cb-03__btn:active {
scale: .985;
}
.cb-03__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 4px;
}
.cb-03__btn:disabled {
cursor: not-allowed;
opacity: .4;
background-size: 100% 0%;
}
@media (prefers-reduced-motion: reduce) {
.cb-03__btn,
.cb-03__btn svg {
transition: color .1s linear, background-size .1s linear;
}
}
@media (forced-colors: active) {
.cb-03__btn {
background: none;
border-color: ButtonText;
color: ButtonText;
}
.cb-03__btn:hover {
background: Highlight;
color: HighlightText;
}
}Here's a working CSS Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Ghost / Outline Button
Source: https://codefronts.com/components/css-buttons/css-ghost-outline-button/
A ghost button that keeps its 1px silhouette at rest and fills from the bottom edge on hover, with the label colour inverting in the same beat. The fill is one background-size transition on a gradient — no pseudo-element, no overflow clipping, and the border is drawn in currentColor so a single colour change re-skins everything.
## HTML
```html
<div class="cb-03">
<div class="cb-03__stage">
<button class="cb-03__btn" type="button">
<span>Read the case study</span>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17 17 7"/><path d="M8.5 7H17v8.5"/></svg>
</button>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
.cb-03 {
--bg: #faf9f6;
--accent: #111827;
--on-accent: #f9fafb;
--border-w: 1px;
--radius: 999px;
--sans: "Space Grotesk",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--bg);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
@supports (color: oklch(50% 0 0)) {
.cb-03 {
--bg: oklch(97.8% .006 95);
--accent: oklch(22% .02 265);
--on-accent: oklch(98% .003 265);
}
}
@media (prefers-color-scheme: dark) {
.cb-03:not([data-theme="light"]) {
--bg: #0b0b0c;
--accent: #f4f4f0;
--on-accent: #101012;
}
}
.cb-03[data-theme="dark"] {
--bg: #0b0b0c;
--accent: #f4f4f0;
--on-accent: #101012;
}
.cb-03,
.cb-03 *,
.cb-03 *::before,
.cb-03 *::after {
box-sizing: border-box;
}
.cb-03__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.5rem,6vw,4rem);
}
.cb-03__btn {
cursor: pointer;
display: grid;
grid-auto-flow: column;
align-items: center;
gap: .65rem;
min-height: 3.25rem;
padding: 0 1.75rem;
border: var(--border-w) solid currentColor;
border-radius: var(--radius);
font: inherit;
font-size: .9375rem;
font-weight: 500;
letter-spacing: .01em;
color: var(--accent);
background-color: transparent;
background-image: linear-gradient(var(--accent),var(--accent));
background-repeat: no-repeat;
background-position: 50% 100%;
background-size: 100% 0%;
transition: background-size .42s cubic-bezier(.16,1,.3,1), color .3s ease .06s, letter-spacing .42s cubic-bezier(.16,1,.3,1);
}
.cb-03__btn svg {
width: 1.05rem;
height: 1.05rem;
transition: rotate .42s cubic-bezier(.16,1,.3,1);
}
.cb-03__btn:is(:hover,:focus-visible) {
background-size: 100% 100%;
color: var(--on-accent);
letter-spacing: .03em;
}
.cb-03__btn:is(:hover,:focus-visible) svg {
rotate: 45deg;
}
.cb-03__btn:active {
scale: .985;
}
.cb-03__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 4px;
}
.cb-03__btn:disabled {
cursor: not-allowed;
opacity: .4;
background-size: 100% 0%;
}
@media (prefers-reduced-motion: reduce) {
.cb-03__btn,
.cb-03__btn svg {
transition: color .1s linear, background-size .1s linear;
}
}
@media (forced-colors: active) {
.cb-03__btn {
background: none;
border-color: ButtonText;
color: ButtonText;
}
.cb-03__btn:hover {
background: Highlight;
color: HighlightText;
}
}
```Here's a working CSS Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Ghost / Outline Button
Source: https://codefronts.com/components/css-buttons/css-ghost-outline-button/
A ghost button that keeps its 1px silhouette at rest and fills from the bottom edge on hover, with the label colour inverting in the same beat. The fill is one background-size transition on a gradient — no pseudo-element, no overflow clipping, and the border is drawn in currentColor so a single colour change re-skins everything.
## HTML
```html
<div class="cb-03">
<div class="cb-03__stage">
<button class="cb-03__btn" type="button">
<span>Read the case study</span>
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17 17 7"/><path d="M8.5 7H17v8.5"/></svg>
</button>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
.cb-03 {
--bg: #faf9f6;
--accent: #111827;
--on-accent: #f9fafb;
--border-w: 1px;
--radius: 999px;
--sans: "Space Grotesk",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--bg);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
}
@supports (color: oklch(50% 0 0)) {
.cb-03 {
--bg: oklch(97.8% .006 95);
--accent: oklch(22% .02 265);
--on-accent: oklch(98% .003 265);
}
}
@media (prefers-color-scheme: dark) {
.cb-03:not([data-theme="light"]) {
--bg: #0b0b0c;
--accent: #f4f4f0;
--on-accent: #101012;
}
}
.cb-03[data-theme="dark"] {
--bg: #0b0b0c;
--accent: #f4f4f0;
--on-accent: #101012;
}
.cb-03,
.cb-03 *,
.cb-03 *::before,
.cb-03 *::after {
box-sizing: border-box;
}
.cb-03__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.5rem,6vw,4rem);
}
.cb-03__btn {
cursor: pointer;
display: grid;
grid-auto-flow: column;
align-items: center;
gap: .65rem;
min-height: 3.25rem;
padding: 0 1.75rem;
border: var(--border-w) solid currentColor;
border-radius: var(--radius);
font: inherit;
font-size: .9375rem;
font-weight: 500;
letter-spacing: .01em;
color: var(--accent);
background-color: transparent;
background-image: linear-gradient(var(--accent),var(--accent));
background-repeat: no-repeat;
background-position: 50% 100%;
background-size: 100% 0%;
transition: background-size .42s cubic-bezier(.16,1,.3,1), color .3s ease .06s, letter-spacing .42s cubic-bezier(.16,1,.3,1);
}
.cb-03__btn svg {
width: 1.05rem;
height: 1.05rem;
transition: rotate .42s cubic-bezier(.16,1,.3,1);
}
.cb-03__btn:is(:hover,:focus-visible) {
background-size: 100% 100%;
color: var(--on-accent);
letter-spacing: .03em;
}
.cb-03__btn:is(:hover,:focus-visible) svg {
rotate: 45deg;
}
.cb-03__btn:active {
scale: .985;
}
.cb-03__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 4px;
}
.cb-03__btn:disabled {
cursor: not-allowed;
opacity: .4;
background-size: 100% 0%;
}
@media (prefers-reduced-motion: reduce) {
.cb-03__btn,
.cb-03__btn svg {
transition: color .1s linear, background-size .1s linear;
}
}
@media (forced-colors: active) {
.cb-03__btn {
background: none;
border-color: ButtonText;
color: ButtonText;
}
.cb-03__btn:hover {
background: Highlight;
color: HighlightText;
}
}
```How this works
The trick is animating background-size on a fixed-position gradient rather than moving a layer, which keeps the fill perfectly clipped to the border radius for free:
.cb-03__btn{
color:var(--accent);
border:1px solid currentColor;
background-image:linear-gradient(var(--accent),var(--accent));
background-repeat:no-repeat;
background-position:50% 100%;
background-size:100% 0%;
transition:background-size .42s cubic-bezier(.16,1,.3,1), color .3s ease .06s;
}
.cb-03__btn:is(:hover,:focus-visible){
background-size:100% 100%;
color:var(--on-accent);
}The color transition is delayed 60ms so the label flips after the fill has passed its midpoint — without that delay the text momentarily sits at the same lightness as the rising fill and disappears. Because the border is currentColor, it inverts along with the label and needs no second declaration.
Make it yours
- Change the fill direction by moving
background-position:0% 50%wipes left-to-right,50% 0%drops from the top. - For a diagonal wipe, swap in a
linear-gradient(120deg, …)and animatebackground-size:200% 200%with a shifting position. - Set
--border-w:2pxfor a bolder editorial ghost; keeppaddinginremso the height doesn't shift. - A dashed variant is one line:
border-style:dashed— useful for "add item" placeholders. - Reduce the easing duration to
.24sfor dense UI; the long 420ms curve suits marketing pages.
Gotchas — read before shipping
- Ghost buttons fail contrast easily: the label needs 4.5:1 against the page, and the 1px border needs 3:1 as a non-text element. A light grey outline on white passes neither.
- Percentage
background-sizeon a zero dimension needs the paired axis to be explicit — write100% 0%, not0%, or the transition will not interpolate in Safari. - Users read ghost buttons as disabled when they sit next to a filled primary with no label difference. Pair with clear verbs.
- If you swap to a pseudo-element fill instead, remember
overflow:hiddenwill clip the focus outline; keep the outline outside viaoutline-offset. :is(:hover,:focus-visible)collapses two selectors, but an unsupported pseudo inside:is()is forgiving — it will not invalidate the whole rule, which is the behaviour you want here.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 113+ | 111+ |
Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome 88+.
:is() needs Chrome 88 / Safari 14 / Firefox 78; split the selector for older engines. background-size transitions and currentColor borders are supported everywhere relevant.