You can make a button feel finished with CSS alone. No images, no JavaScript, just hover, focus and press states that do something visible. Hover fill, gradient, 3D press, glow, animated gradient border, glassmorphism and neumorphism cover most of what people build.
They don’t carry the same risk. Scored on build effort, accessibility exposure and browser support, the range runs from low for the hover fill to high for neumorphism.
Front-end developers mostly put these on call-to-action buttons, and that’s where an effect has to survive keyboard focus, touch screens and older browsers. Motion is the one area with no support excuse left. The prefers-reduced-motion media feature has been Baseline widely available since July 2022 (Web Platform DX), so any animated effect can ship with a reduced-motion variant.
Which cool CSS button effects are worth building?
See the Pen
Modern Animated Buttons Collection by Bogdan Sandu (@bogdansandu)
on CodePen.
Hover fill, gradient and 3D press buttons are the safest to build, and neumorphism is the riskiest. The table ranks them from lowest risk to highest.
| Effect | Core properties | Main risk | Verdict |
|---|---|---|---|
| Hover fill and slide | ::before, transform | Label contrast mid-fill | Build first |
| Gradient | linear-gradient, background-position | Snaps on transition | Build |
| 3D press | box-shadow, transform | Shadow repaint | Best for main buttons |
| Glow | box-shadow, text-shadow | Vanishes on light surfaces | Use sparingly |
| Animated gradient border | @property, conic-gradient | Needs @property | Add a static fallback |
| Glassmorphism | backdrop-filter, rgba() | Contrast shifts with the backdrop | Controlled backgrounds only |
| Neumorphism | Two box-shadow layers | Shadow-only boundary | Skip for icon-only buttons |
For a main button, a gradient or a 3D press does the job. A secondary button is fine with a hover fill. Icon buttons can take a hover fill too, or a color-matched glow.
Hover fill and slide buttons
A pseudo-element sits behind the label, starts outside the button’s edge, and slides in on hover. It takes the least code of any effect here and forgives the most mistakes.
The core set is position: relative and overflow: hidden on the button, transform: translateX(-100%) on the ::before layer, and translateX(0) on :hover.
Only transform moves, so the animation stays cheap. The catch is the label. It has to meet WCAG 1.4.3 text contrast against both the resting color and the fill color, and keyboard focus needs to trigger the same fill.
Gradient buttons
The base is background: linear-gradient(135deg, #3b82f6, #8b5cf6), with two or three stops.
Where it goes wrong is the transition between two gradients. Browsers do not interpolate gradient images, so the color change snaps instead of fading. Enlarge the fill with background-size: 200% 100% and move background-position on hover, or fade a gradient layer in with opacity. Check the label contrast at the lightest stop too, because the text crosses the whole range.
One gradient button per view is plenty. Put three on the screen and they fight over the same click.
3D press buttons
The depth comes from a hard shadow and the press from a transform.
A solid shadow such as box-shadow: 0 6px 0 #1e3a8a draws the edge. On :active, transform: translateY(4px) with the shadow cut to 0 2px 0 makes the button sink into its base.
Variations that add perspective and layered edges build on the same base, as shown in these CSS 3D button examples.
It works well on main call-to-action buttons and on touch screens, where the press is easy to see. It falls short in flat or minimalist layouts, and on pages with lots of buttons, since the shadow repaints on each state change.
Glowing buttons
A glow is a blurred, color-matched box-shadow with no offset, such as 0 0 16px rgba(80, 140, 255, 0.6). A matching text-shadow carries it onto the label.
On dark surfaces it reads well. On white ones it disappears.
It’s one declaration with no extra markup, which is the nice part. Animating the blur radius repaints the button, though, so fade a separate glow layer with opacity instead.
Animated gradient border buttons

MDN lists @property as Baseline 2024, newly available since July 2024. That date is the support floor for this effect.
The registration needs a syntax of "<angle>", an inherits value (false here) and an initial-value, or the whole rule is ignored. A conic-gradient border reads the registered --angle, and @keyframes turns it from 0deg to 360deg.
Without registration the browser treats --angle as plain text, so the angle jumps instead of turning.
Where @property is missing, fall back to a static gradient border. And stop the loop under prefers-reduced-motion.
Glassmorphism buttons
The recipe pairs backdrop-filter: blur(10px) with a translucent fill such as rgba(255, 255, 255, 0.15). The blur only shows when the element or its background is partly transparent.
Keep a -webkit-backdrop-filter line for older Safari builds. The wider technique is covered in this glassmorphism CSS guide.
Text contrast depends on whatever sits behind the button, so test the label against the lightest and darkest background it can meet.
Web Platform DX data lists backdrop-filter as Baseline newly available since 16 September 2024, the day Safari 18 shipped it.
Neumorphism buttons
Neumorphism ranks last on purpose.
The button matches its background and takes its shape from two shadows, one light and one dark, as in box-shadow: 6px 6px 12px #bebebe, -6px -6px 12px #ffffff.
W3C’s Understanding document for WCAG 1.4.11 asks for a 3:1 contrast ratio on visual information that identifies controls and their states. A button with a visible text label rests on its text contrast, but an icon-only neumorphic button depends on the shadow alone, so the shadow has to clear 3:1 against the adjacent color.
I’d skip it for icon-only buttons and for the main action. On labeled secondary controls it’s tolerable, since the text carries the contrast.
Which CSS properties do most button effects rely on?
Most button effects come down to background, border-radius, box-shadow, transform and transition. Text-shadow, pseudo-elements and custom properties cover the rest.
backgroundsets a solid color or alinear-gradient()fillborder-radiusruns from8pxfor a soft rectangle to999pxfor a pill, or50%for a round icon buttonbox-shadowhandles elevation, glow and 3D edges, all built from comma-separated layerstransformgives youscale(1.05)for a grow andtranslateY(-2px)for a lifttransitiondecides which properties change, how long it takes, and what easing applies
padding and min-height sit alongside these and set the hit area, so a short label never collapses the button.
Layered shadows are easier to tune by eye, and a visual box shadow generator shows each layer as it changes.
Gradient stops behave the same way. A CSS gradient generator saves trial and error before the values reach the stylesheet.
The ::before and ::after pseudo-elements add overlay layers for slide fills, shine sweeps and glow without extra HTML. Each needs content: "" and position: absolute inside a button set to position: relative.
web.dev’s compositing guidance says to stick to transform and opacity for animation, because changes to them avoid layout and paint.
Syntax and accepted values for every property above are documented on MDN Web Docs.
Color variants with CSS variables
Custom properties let one rule serve every color variant. Define the colors on the base class, then override them per modifier.
.btn { background: var(--btn-bg); color: var(--btn-fg); } plus .btn--danger { --btn-bg: #c62828; --btn-fg: #ffffff; } produces a new variant with no duplicated declarations.
CSS variables resolve in the browser, so a theme switch or a media query can change them at runtime. Sass variables are resolved at build time instead, which suits loops that generate variant classes from a color map.
Projects already on Sass can use both. Maps generate the classes, and custom properties hold the values that change in the browser.
How do you style hover, focus, active and disabled states?
Write the states in cascade order: default, :hover, :focus-visible, :active, then :disabled. Rules with equal specificity are resolved by order, so the later state wins when two apply at once.
- Write the default state with a
min-height, a border, andtransitionon named properties only. - Add
:hoverfor a color or elevation change. - Add
:focus-visiblewith an outline, for exampleoutline: 3px solid #1d4ed8; outline-offset: 3px. - Add
:activeafter:hover, so the press look wins while the pointer is down. - Add
:disabledlast, with reduced emphasis andcursor: not-allowed.
More hover treatments, from fills to underline wipes, are collected in these CSS button hover effects.
Hover and active states
Hover tells people the button is clickable, and active confirms the press. Both should change something visible without moving the surrounding layout.
:hovershifts the color, lifts withtranslateY(-2px), or raises the shadow:activesinks withtranslateY(1px)or drops the shadow
Hover is an enhancement. Touch screens have no hover, so no state may carry meaning that only hover reveals.
Why :focus-visible replaces :focus
MDN classes :focus-visible as Baseline widely available. It styles the focused element only when the browser decides the user needs to see where focus is, so mouse clicks skip the ring while keyboard tabbing shows it.
Never ship outline: none without a replacement. MDN warns that a removed outline has to be replaced with one that passes WCAG 1.4.11 non-text contrast.
Disabled state
The disabled attribute blocks clicks and removes the button from the tab order. Styling alone with opacity or pointer-events: none leaves keyboard activation intact, which is the usual mistake.
For the look, lower the contrast and use a not-allowed cursor. WCAG 1.4.11 exempts inactive components from its contrast requirement, according to W3C’s Understanding document.
One cascade trap to know about: :hover still matches a disabled button. Put the :disabled rule last, or add :not(:disabled) to the hover rule.
Should a button animation use a transition or keyframes?
Use a transition for state changes and @keyframes for loops or multi-step effects. A transition needs a trigger and two states, while keyframes can run without one.
| Aspect | Transition | Keyframes |
|---|---|---|
| Trigger | State change such as :hover | Page load or a class toggle |
| Stops | Start and end | Any number |
| Typical button effect | Hover fill, press | Pulse, shine sweep, rotating border |
Transitions for state changes
Name the properties instead of writing transition: all. The all keyword animates every property that changes, including ones that trigger layout or paint.
A typical declaration reads transition: transform 100ms ease-out, background-color 100ms ease-out.
Timing values are easier to judge in a CSS transition generator that previews each curve.
Keyframes for loops and multi-step effects
A pulse loops transform: scale() between two values, and a shine sweep translates a pseudo-element across the button. The rotating border from earlier turns the registered --angle property from 0deg to 360deg.
A fuller look at CSS keyframes covers percentages, direction and fill modes.
Infinite loops hold attention whether the user wants it held or not, so pair them with prefers-reduced-motion.
Duration and easing values
NN/g puts most UI animations between 100 and 500 ms, scaled to complexity and travel distance. Simple feedback sits near 100 ms, and at 500 ms animations start to feel like a drag.
For buttons, that puts a press at the low end and a slide fill somewhat higher, still well short of 500 ms. NN/g also flags fully linear motion as unnatural.
ease-out starts fast and lands softly, which suits hover-in. ease-in-out works better for loops that return to their starting point.
The Animations panel in Chrome DevTools replays a transition at reduced speed, which makes easing curves easier to judge.
How do you keep cool CSS buttons accessible?
Every state of the button needs its text contrast, hit area, focus indicator and motion checked. An effect that fails any of them locks some users out.
The numbers below come from the W3C’s WCAG 2.2 documents.
- The minimum target size (SC 2.5.8, Level AA) is 24 by 24 CSS pixels
- The enhanced target size (SC 2.5.5, Level AAA) is 44 by 44 CSS pixels
- Text contrast (SC 1.4.3, Level AA) needs 4.5:1 for regular text and 3:1 for large text
- Under SC 2.2.2 (Level A), anything moving for more than five seconds beside other content needs a pause, stop or hide mechanism
Contrast in every state
According to W3C’s Understanding document, SC 1.4.3 covers text shown on hover and on keyboard focus. A label that passes at rest can still fail on the hover fill.
Test the label in default, hover, focus and active states. A disabled button is exempt as an inactive component.
W3C defines large text as 18 point, or 14 point bold. That works out to about 24 CSS pixels at regular weight, or about 18.5 CSS pixels in bold, so a bold 16px button label still needs the full 4.5:1.
Gradient and glass buttons multiply the work, since a color contrast checker compares two colors at a time and each gradient stop is a new pair.
Hit area and target size
SC 2.5.8 measures the clickable area, not the visible glyph. Padding and min-height grow the hit area without touching the icon.
A 16px icon button with 4px of padding on each side reaches 24 by 24.
The spacing exception covers undersized buttons. According to the axe-core rule, a 24px circle centered on each undersized target must not overlap another target or another undersized target’s circle, so a toolbar of tiny buttons with no gap fails.
A focus indicator that survives the effect
Glow, gradient and glass backgrounds can swallow a thin focus ring.
Use outline with outline-offset, which sits outside the button and keeps its own contrast. A box-shadow ring is a bad bet when an ancestor has overflow: hidden, because the ancestor clips it. And pick a ring color that contrasts with the page behind the button, not only with the button itself.
Reduced motion and looping animation
MDN describes prefers-reduced-motion as a request to remove, reduce or replace motion-based animation. Replacing keeps the feedback: swap the scale for a color change instead of deleting the effect.
@media (prefers-reduced-motion: reduce) { .btn { transition: background-color 100ms ease-out; } .btn:hover { transform: none; } .btn--pulse { animation: none; } }
The media query only reaches users who changed an operating system setting, and SC 2.2.2 sets its own floor. A looping pulse needs a finite animation-iteration-count that ends inside five seconds, or a pause control.
Native semantics before ARIA
W3C’s Using ARIA note puts one rule first: use a native element when it already carries the semantics and behavior needed. A <button> brings keyboard focus and Enter and Space activation by default.
A styled div with role="button" has to rebuild all of that by hand. Keep ARIA attributes for the gaps, such as an aria-label on an icon-only button with no visible text.
When do cool CSS button effects break or backfire?
Effects break when an animated property forces repaints or when a browser lacks the feature. Input devices that can’t hover cause a third kind of trouble. All of them have a guard that costs a few lines of CSS.
Repaints and layer memory
Promoting every button to its own compositor layer trades smoothness for memory.
MDN warns that excessive will-change use means excessive memory use and more complex rendering. It also recommends setting the property from script around the interaction instead of leaving it in the stylesheet.
The Rendering panel in Chrome DevTools makes the cost visible. Paint flashing turns repainted areas green, and Layer borders outline the compositor layers.
A button animated with transform alone should not flash. If it does, a paint-heavy property such as a shadow blur is animating somewhere.
Support gaps and fallbacks
Write the baseline outside @supports and the enhancement inside, so an unsupported browser still gets a usable button. MDN’s feature query guide describes the same split.
.glass { background: rgba(20, 20, 30, 0.85); } @supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { .glass { background: rgba(255, 255, 255, 0.15); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); } }
The query lists both forms because Safari 17 and earlier only understand the prefixed property, and a test for the unprefixed one alone would send them to the dark fallback.
The fallback fill is darker on purpose. Without the blur, a 15 percent white fill leaves the label sitting directly on whatever is behind it.
A feature query tests whether the browser parses a property and value, not whether the effect renders well. The contrast check still runs on both paths.
Per-version tables live on Can I use, and the wider discipline is covered under cross-browser compatibility.
Hover on touch screens
Touch browsers emulate hover on tap, so a hover style can stay stuck after the finger lifts.
Wrap hover-only effects in @media (hover: hover). This media query tests the primary input device.
(hover: hover) follows the primary input. A tablet with a trackpad attached still counts as touch, so hover effects stay off.
Then there’s (any-hover: hover), which looks at every attached input. Cybozu’s frontend team notes that the iPad with a trackpad passes this test, which brings hover effects back along with the stuck-hover risk on taps.
For buttons, default to (hover: hover). A missing hover effect on a hybrid device costs less than a stuck one on a phone.
Should you write cool CSS buttons yourself or copy them from a library?
Custom CSS suits buttons that are part of the brand, and a framework suits a project that already ships one. A copied snippet is fine for a one-off. What decides it is who maintains the buttons six months from now.
Custom CSS with CSS variables
You get full control over every state, no extra dependency to update, and effects that match the design exactly. The price is that every accessibility check is yours to run, and the first button takes more hours to ship.
Copy-paste libraries and demos
See the Pen
Modern Button Collection With Interactive CSS Effects by Bogdan Sandu (@bogdansandu)
on CodePen.
Uiverse publishes its elements under the MIT License, in CSS or Tailwind versions, and CodePen demos fill the same role. Licensing is rarely the problem. Code age is.
CSS Portal’s gradient button tutorial, for example, still lists -moz-linear-gradient and -o-linear-gradient lines that current browsers no longer need.
Audit any copied snippet before it reaches production:
- Confirm that
:hover,:focus-visible,:activeand:disabledall exist. - Check each vendor-prefixed line against the browsers you target, and delete the ones that no longer apply.
- Test label contrast in each state.
- Replace
transition: allwith named properties.
Fast and free, but the states are often missing and the code is often old.
Tailwind CSS
Tailwind writes each state into the class list. The hover:, focus-visible:, disabled: and motion-reduce: variants cover the four states and reduced motion without a separate stylesheet, per Tailwind’s docs.
Ready-made Tailwind button patterns show how those variants stack on a single element.
Focus and reduced-motion handling sit one prefix away, which is the best thing about it. The downside is long class lists that repeat across buttons until they move into a component.
Bootstrap
Bootstrap is the safe baseline rather than the cool one.
Its docs describe a base .btn class, color variants, .btn-outline-* modifiers, and sizing that can be adjusted through CSS variables or Sass variables. The Bootstrap button classes work on a <button> element, and the docs allow them on anchors and inputs too.
States and sizes ship built in, and variants stay consistent across a team. Glow, glassmorphism and animated borders are another story, since they mean overriding defaults, and each override adds specificity to manage.
The two approaches combine well. Keep .btn for structure and add one modifier class that carries the effect.
Cool CSS Buttons Examples To Check Out
For the Bloggers: Stylish Animated CSS
See the Pen
Stylish Animated CSS Buttons For Blogger. by Prio-Soft™ (@priosoft)
on CodePen.
Twitter’s Aqua Button Doppelgänger

The Showstopper: Button with a Ring Indicator

Splash of Colors: The CSS Button Edition

Taste the Rainbow… with Buttons

Flip, Reveal, Surprise: The 3D Button

Context Kings: Material Design Buttons

Subtle Glow: Button Aura

Arrowed Down: Stylish Button Options

Heartbeat of the Web: Pulsing Button

Into the Future with Glitchy Cyberpunk

Unfold the Mystery: FlipCover Buttons

Slide into Twitter: A Door-Open Concept

Pastel Dreams: Animated Beauties

Paint Me with Animation: CSS + SVG Magic

Save Space & Style: Save Button

Dot the i’s and Cross the t’s: Pure CSS Button

Shake and Bake: Animated CSS Button

Game On: Valorant Button

Blobby Fun: Blobs Button

Little Things Matter: Progress Button Microinteractions

Into the Future: Futuristic Button Styles

Chasing Colors: 100 Days CSS Button N 045

Fluid Dreams: Flush Button

Tip and Reveal: Auto Width CSS Button Flip

Lift Off: Rocket Launch Button

Cool CSS Buttons FAQ
Can a ripple effect work without JavaScript?
Partly. CSS can fake a ripple from the button’s center using :active and a scaling pseudo-element, but it cannot read where the pointer landed. A ripple that starts at the exact click point needs a few lines of JavaScript to pass coordinates into CSS variables.
A fuller breakdown sits in this guide to the CSS ripple effect.
Should a styled button be a button element or an anchor element?
Use a button element for actions such as submitting a form or opening a dialog, and an anchor with href for navigation to another URL. Both take identical CSS, so the visual result does not decide the choice.
Anchors have no disabled attribute, which is one more reason to keep actions on buttons.
How do you add an icon to a CSS button and align it with the label?
Place an inline SVG next to the label and set the button to display: inline-flex with align-items: center and a gap. Size the icon in em so it scales with the text.
Mark a decorative icon with aria-hidden="true" when a visible label already names the action.
Does a button need cursor: pointer?
No. Browsers show the default arrow on a button element, and cursor: pointer is a style choice that signals clickability, borrowed from links.
If the project uses it, pair it with cursor: not-allowed on the :disabled state so the two states read differently.
How to Test a Button Before It Ships
Test cool CSS buttons keyboard first, then touch, then reduced motion. That order follows severity. A missing focus ring blocks keyboard users outright, while a stuck hover only degrades the page.
- Tab to the button and confirm a visible focus ring
- Tap it on a phone and watch for stuck hover
- Enable reduce motion and check that feedback remains
The reduced-motion variant strips part of the effect, so the button looks plainer for the users who request it. That’s the cost, and it’s worth paying.
Web Platform DX projects backdrop-filter to reach Baseline widely available on 16 March 2027 (position verified October 2026). After that date, a glassmorphism button can drop its @supports fallback on projects that target widely available features.
A broader pass over the rest of the page belongs in a web accessibility checklist.
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026








