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.

EffectCore propertiesMain riskVerdict
Hover fill and slide::before, transformLabel contrast mid-fillBuild first
Gradientlinear-gradient, background-positionSnaps on transitionBuild
3D pressbox-shadow, transformShadow repaintBest for main buttons
Glowbox-shadow, text-shadowVanishes on light surfacesUse sparingly
Animated gradient border@property, conic-gradientNeeds @propertyAdd a static fallback
Glassmorphismbackdrop-filter, rgba()Contrast shifts with the backdropControlled backgrounds only
NeumorphismTwo box-shadow layersShadow-only boundarySkip 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.

What is shaping UX design today?

Uncover the newest UX design statistics: user behavior, design trends, ROI data, and insights driving better digital experiences.

Check Them Out →

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.

  • background sets a solid color or a linear-gradient() fill
  • border-radius runs from 8px for a soft rectangle to 999px for a pill, or 50% for a round icon button
  • box-shadow handles elevation, glow and 3D edges, all built from comma-separated layers
  • transform gives you scale(1.05) for a grow and translateY(-2px) for a lift
  • transition decides 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.

  1. Write the default state with a min-height, a border, and transition on named properties only.
  2. Add :hover for a color or elevation change.
  3. Add :focus-visible with an outline, for example outline: 3px solid #1d4ed8; outline-offset: 3px.
  4. Add :active after :hover, so the press look wins while the pointer is down.
  5. Add :disabled last, with reduced emphasis and cursor: 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.

  • :hover shifts the color, lifts with translateY(-2px), or raises the shadow
  • :active sinks with translateY(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.

AspectTransitionKeyframes
TriggerState change such as :hoverPage load or a class toggle
StopsStart and endAny number
Typical button effectHover fill, pressPulse, 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:

  1. Confirm that :hover, :focus-visible, :active and :disabled all exist.
  2. Check each vendor-prefixed line against the browsers you target, and delete the ones that no longer apply.
  3. Test label contrast in each state.
  4. Replace transition: all with 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.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)