EmpireUI
Get Pro
← Blog8 min read#cyberpunk#button#css

Cyberpunk Button Design in CSS: Neon Glow, Glitch and Clip-Path

Build cyberpunk buttons in pure CSS — neon glow with box-shadow, glitch animations with clip-path, and angled edges. Copy-paste examples included.

Neon glowing cyberpunk button with purple and cyan light on dark background

Why Cyberpunk Buttons Hit Different

The cyberpunk aesthetic is built on contradiction — decaying infrastructure lit by neon, cold steel warmed by magenta light. Buttons are the perfect canvas for it. They're interactive, they demand attention, and a well-crafted glow effect at 400% zoom still looks intentional rather than broken.

In practice, you don't need a design system overhaul to pull this off. Three CSS properties carry most of the weight: box-shadow for the neon glow, clip-path for angled edges, and @keyframes for the glitch. That said, the devil is in the stacking order — get that wrong and your glow bleeds into adjacent elements in ways you really don't want.

If you want to see fully-built examples before writing a line of code, the cyberpunk style hub on Empire UI has interactive component previews you can copy directly. Worth skimming those first so you know what you're aiming for.

One more thing — this guide assumes you're comfortable with CSS custom properties and basic animation syntax. If you're brand new to CSS transitions, go get comfortable with those first. Everything here builds on that foundation.

Building the Neon Glow with box-shadow

Neon glow is the most recognisable cyberpunk trait. The trick is layering multiple box-shadow values — one tight shadow for the immediate edge, one medium spread for the inner halo, and one large soft spread for the atmospheric bleed. Pick the wrong values and you get muddy light instead of the sharp, electric look you're after.

Here's the base pattern. Use --neon-color as a CSS custom property so you can theme variations without rewriting the whole ruleset: ``css :root { --neon-cyan: #00f5ff; --neon-magenta: #ff00c8; } .btn-neon { background: transparent; color: var(--neon-cyan); border: 2px solid var(--neon-cyan); padding: 12px 32px; font-family: 'Share Tech Mono', monospace; letter-spacing: 0.15em; text-transform: uppercase; cursor: pointer; box-shadow: 0 0 6px var(--neon-cyan), 0 0 20px var(--neon-cyan), 0 0 60px color-mix(in srgb, var(--neon-cyan) 40%, transparent); transition: box-shadow 0.2s ease; } .btn-neon:hover { box-shadow: 0 0 8px var(--neon-cyan), 0 0 30px var(--neon-cyan), 0 0 80px var(--neon-cyan), 0 0 120px color-mix(in srgb, var(--neon-cyan) 25%, transparent); } ``

The color-mix() call on the outer shadow is a 2024+ technique — it handles the atmospheric spread without you manually writing an rgba() value that drifts if you swap the color. Safari 16.2+, Chrome 111+, and Firefox 113+ all support it.

Quick aside: inset shadows work great for a "lit from within" variation. Add inset 0 0 15px color-mix(in srgb, var(--neon-cyan) 20%, transparent) to make the button surface glow rather than just the border. Feels more alive on dark backgrounds.

Honestly, the monospace font matters almost as much as the glow itself. "Share Tech Mono" or "Orbitron" from Google Fonts nail the terminal-display vibe. A sans-serif button with a neon border just looks like a broken UI kit, not intentional cyberpunk.

Angled Edges with clip-path

Rectangular buttons are boring. Cyberpunk UI almost always has that diagonal corner cut — bottom-right chopped off, or a parallelogram shape that implies speed. clip-path: polygon() is your friend here and it's cleaner than the old transform: skewX() hack that breaks text legibility.

.btn-angled {
  background: var(--neon-magenta);
  color: #0a0a0f;
  border: none;
  padding: 14px 40px 14px 24px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  cursor: pointer;
  /* cut bottom-right corner at 16px */
  clip-path: polygon(
    0 0,
    100% 0,
    100% calc(100% - 16px),
    calc(100% - 16px) 100%,
    0 100%
  );
  transition: clip-path 0.15s ease, background 0.15s ease;
}

.btn-angled:hover {
  background: var(--neon-cyan);
  /* deeper cut on hover */
  clip-path: polygon(
    0 0,
    100% 0,
    100% calc(100% - 24px),
    calc(100% - 24px) 100%,
    0 100%
  );
}

That calc(100% - 16px) is the cut size — 16px is a good default for a medium-sized button. Go to 24px or higher for larger CTAs. Worth noting: clip-path hides box-shadow because the shadow is part of the element's painted area. You'll need a wrapper element to apply glow when using clip-path on the button itself.

<!-- Wrapper carries the shadow, button carries the clip -->
<div class="btn-glow-wrapper">
  <button class="btn-angled">LAUNCH</button>
</div>
```

```css
.btn-glow-wrapper {
  display: inline-block;
  filter: drop-shadow(0 0 10px var(--neon-magenta))
          drop-shadow(0 0 30px color-mix(in srgb, var(--neon-magenta) 50%, transparent));
  transition: filter 0.2s ease;
}

.btn-glow-wrapper:hover {
  filter: drop-shadow(0 0 16px var(--neon-magenta))
          drop-shadow(0 0 50px var(--neon-magenta));
}

filter: drop-shadow() respects the clipped shape, so your shadow follows the polygon outline instead of the element's bounding box. This is the trick that makes the whole thing look intentional. Use box-shadow on clip-path elements and you'll be confused for twenty minutes wondering why the glow is a rectangle.

The Glitch Animation

Glitch effects feel chaotic but the CSS is actually pretty mechanical. You're stacking pseudo-elements on top of the button, clipping them to different horizontal slices with clip-path, and animating their transform: translateX() independently. The randomness comes from offsetting keyframe timings, not from actual randomness.

.btn-glitch {
  position: relative;
  background: transparent;
  color: var(--neon-cyan);
  border: 2px solid var(--neon-cyan);
  padding: 12px 32px;
  font-family: 'Share Tech Mono', monospace;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  overflow: hidden;
  cursor: pointer;
}

.btn-glitch::before,
.btn-glitch::after {
  content: attr(data-text);
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: inherit;
  box-sizing: border-box;
}

.btn-glitch::before {
  color: var(--neon-magenta);
  clip-path: polygon(0 20%, 100% 20%, 100% 40%, 0 40%);
  animation: glitch-1 3s infinite;
}

.btn-glitch::after {
  color: var(--neon-cyan);
  clip-path: polygon(0 60%, 100% 60%, 100% 80%, 0 80%);
  animation: glitch-2 3s infinite 0.4s;
}

@keyframes glitch-1 {
  0%, 90%, 100% { transform: translateX(0); }
  92%            { transform: translateX(-4px); }
  94%            { transform: translateX(4px); }
  96%            { transform: translateX(-2px); }
  98%            { transform: translateX(3px); }
}

@keyframes glitch-2 {
  0%, 88%, 100% { transform: translateX(0); }
  90%            { transform: translateX(3px); }
  92%            { transform: translateX(-3px); }
  95%            { transform: translateX(5px); }
  97%            { transform: translateX(-1px); }
}

The data-text attribute on the HTML element mirrors the button label so the pseudo-elements display the same text. It's a well-known trick but easy to forget: <button class="btn-glitch" data-text="CONNECT">CONNECT</button>.

Look, if you want the glitch to fire only on hover rather than on a loop, wrap the animation declarations inside a :hover selector and set animation-play-state: paused by default. Constant glitch on a production UI gets tiring fast — use it sparingly, or gate it behind user interaction.

For accessibility, always include prefers-reduced-motion. The glitch is a rapid motion effect and can genuinely cause discomfort: ``css @media (prefers-reduced-motion: reduce) { .btn-glitch::before, .btn-glitch::after { animation: none; } } ``

Combining All Three: The Full Cyberpunk Button

Now you've got the three parts, here's how they compose. The strategy is: clip-path polygon on the button for the angled shape, drop-shadow on a wrapper div for the neon glow, and glitch pseudo-elements inside the button. Each concern stays in its own layer.

<div class="cyber-btn-wrap">
  <button class="cyber-btn" data-text="INITIATE">
    INITIATE
  </button>
</div>
```

```css
:root {
  --cyber-primary: #00f5ff;
  --cyber-accent: #ff00c8;
  --cyber-bg: #0a0a0f;
}

.cyber-btn-wrap {
  display: inline-block;
  filter: drop-shadow(0 0 8px var(--cyber-primary))
          drop-shadow(0 0 24px color-mix(in srgb, var(--cyber-primary) 40%, transparent));
  transition: filter 0.2s ease;
}

.cyber-btn-wrap:hover {
  filter: drop-shadow(0 0 14px var(--cyber-primary))
          drop-shadow(0 0 48px var(--cyber-primary))
          drop-shadow(0 0 80px color-mix(in srgb, var(--cyber-primary) 30%, transparent));
}

.cyber-btn {
  position: relative;
  background: var(--cyber-bg);
  color: var(--cyber-primary);
  border: 2px solid var(--cyber-primary);
  padding: 14px 48px 14px 32px;
  font-family: 'Share Tech Mono', monospace;
  font-size: 0.875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  overflow: hidden;
  clip-path: polygon(
    0 0, 100% 0,
    100% calc(100% - 14px),
    calc(100% - 14px) 100%,
    0 100%
  );
  transition: color 0.2s ease, background 0.2s ease;
}

.cyber-btn:hover {
  background: var(--cyber-primary);
  color: var(--cyber-bg);
}

.cyber-btn::before {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px 48px 14px 32px;
  box-sizing: border-box;
  color: var(--cyber-accent);
  clip-path: polygon(0 25%, 100% 25%, 100% 45%, 0 45%);
  animation: glitch-a 4s infinite;
}

.cyber-btn::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px 48px 14px 32px;
  box-sizing: border-box;
  color: var(--cyber-primary);
  clip-path: polygon(0 60%, 100% 60%, 100% 78%, 0 78%);
  animation: glitch-b 4s infinite 0.6s;
}

@keyframes glitch-a {
  0%, 88%, 100% { transform: translateX(0); opacity: 0; }
  89% { transform: translateX(-5px); opacity: 1; }
  91% { transform: translateX(4px); opacity: 1; }
  93% { transform: translateX(-2px); opacity: 1; }
  95% { transform: translateX(0); opacity: 0; }
}

@keyframes glitch-b {
  0%, 85%, 100% { transform: translateX(0); opacity: 0; }
  86% { transform: translateX(4px); opacity: 1; }
  88% { transform: translateX(-4px); opacity: 1; }
  91% { transform: translateX(2px); opacity: 1; }
  93% { transform: translateX(0); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .cyber-btn::before,
  .cyber-btn::after { animation: none; }
}

The opacity: 0 default on the pseudo-elements means glitch layers are invisible at rest — they only flash in during the keyframe window. That keeps the base button clean and the glitch feels like a malfunction rather than a persistent overlay.

That said, if you're building this in React or a component framework, extract the CSS custom properties to a theme file and pass color variants as props. Hardcoding --cyber-primary everywhere gets painful when your design team decides cyan is "too 2023" and wants lime instead.

For a broader view of where cyberpunk sits alongside other dark UI trends, the glassmorphism vs neumorphism breakdown is worth reading — it contextualises how different aesthetics handle depth and surface, which helps you decide when cyberpunk actually fits your project.

Tailwind Adaptation

Most of the box-shadow and clip-path values you need aren't in Tailwind's default config. You'll extend theme.extend in tailwind.config.ts. Tailwind v4 (released early 2025) makes this cleaner since you configure via CSS variables in @theme, but the example below works for both v3 and v4.

// tailwind.config.ts (v3 syntax)
module.exports = {
  theme: {
    extend: {
      boxShadow: {
        'neon-cyan': '0 0 6px #00f5ff, 0 0 20px #00f5ff, 0 0 60px rgba(0,245,255,0.3)',
        'neon-cyan-lg': '0 0 10px #00f5ff, 0 0 40px #00f5ff, 0 0 100px rgba(0,245,255,0.25)',
        'neon-magenta': '0 0 6px #ff00c8, 0 0 20px #ff00c8, 0 0 60px rgba(255,0,200,0.3)',
      },
      fontFamily: {
        cyber: ['Share Tech Mono', 'monospace'],
      },
    },
  },
};
```

Then your JSX becomes much more readable:

```jsx
<button
  data-text="INITIATE"
  className={
    'font-cyber uppercase tracking-widest text-cyber-cyan ' +
    'border-2 border-cyber-cyan bg-transparent px-8 py-3 ' +
    'shadow-neon-cyan hover:shadow-neon-cyan-lg ' +
    'transition-shadow duration-200 btn-glitch'
  }
>
  INITIATE
</button>

The glitch pseudo-elements still need a stylesheet or a CSS Module — Tailwind can't generate ::before { content: attr(data-text) } from utility classes. That's fine. Mix utilities for layout and color with a small CSS file for the animation specifics. Don't try to do everything in one place.

Honestly, if you want pre-built cyberpunk components rather than assembling this from scratch every time, the Empire UI style hub has buttons, cards, and inputs already wired up. Copy the component, adjust the color tokens, done.

Performance and Common Pitfalls

A few things that'll bite you if you're not watching. First: filter: drop-shadow() on the wrapper triggers a compositing layer. That's actually good for animation performance — the browser can GPU-accelerate it. But if you stack too many drop-shadow calls (more than 3) and animate them, you'll see frame drops on lower-end Android devices. Profile in DevTools before shipping.

Second: clip-path animations are cheap when you're transitioning between two polygon values with the same vertex count. Change the number of vertices between keyframes and you get a hard cut instead of a morph. Keep polygon point counts identical if you want smooth transitions.

The overflow: hidden on the button is required for the pseudo-element glitch to stay contained. Without it, the sliced text layers will bleed outside the button bounds at large viewport sizes. Easy to miss in a small viewport, very obvious on a 1440p monitor.

Worth noting: if you're using these buttons inside a position: fixed or will-change: transform ancestor, backdrop-filter and filter both create new stacking contexts, which can cause z-index headaches. Test inside your actual layout, not in an isolated CodePen.

Finally, test your color contrast. Neon on dark is usually fine for large text, but fine-print labels inside a cyan-glowing button can fail WCAG AA at the 4.5:1 ratio. Run it through a contrast checker — the visual vibes don't automatically mean accessible. You can also pair cyberpunk buttons with more subtle components; browsing the gradient generator is a fast way to find background values that keep contrast ratios in check.

FAQ

Why does my neon box-shadow disappear when I use clip-path?

clip-path masks the painted area of the element, including its shadows. Wrap the button in a parent element and apply filter: drop-shadow() there instead — it traces the clipped shape correctly.

Can I use cyberpunk button styles in a React component library?

Absolutely. Use CSS custom properties for colors so consumers can override them, CSS Modules or a single stylesheet for the glitch keyframes, and Tailwind utilities for layout and spacing. The pseudo-element trick requires actual CSS, not just class utilities.

How do I stop the glitch animation from being distracting?

Set animation-play-state: paused by default and switch to running on :hover or :focus. Always include a prefers-reduced-motion media query that disables the animation entirely.

What fonts work best for cyberpunk buttons?

"Share Tech Mono" and "Orbitron" are the go-to picks and both are free on Google Fonts. Monospace with wide letter-spacing (letter-spacing: 0.15em) is the defining typographic move.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Cyberpunk Form Design: Glitch Inputs, Neon Labels, Scanline FieldsNeon Button in CSS: Glowing Border, Pulsing Light, Hover BloomLiquid Fill Button Animation in CSS: SVG and clip-path MorphCSS text-shadow: Glow Effects, Neon Text and Layered Shadows