EmpireUI
Get Pro
← Blog8 min read#cyberpunk#card#react

Cyberpunk Card Component in React: Dark Grid, Neon Border, Scanlines

Build a cyberpunk card in React with dark grid backgrounds, glowing neon borders, and CSS scanline overlays. Full code walkthrough, no library lock-in.

dark neon glowing cyberpunk interface with grid lines and purple light

Why Cyberpunk UI Is Hard to Get Right

Most cyberpunk card tutorials online give you a purple box with a glowing border and call it done. That's not cyberpunk — that's just a dark card with bad shadows. The real aesthetic has texture: scanline overlays, grid perspective, clipped corners, color-bleed glows. Get those wrong and the whole thing reads as cheap Halloween decoration instead of a deliberate design choice.

Honestly, the difficulty isn't the CSS. It's knowing which details actually matter. A 1px grid at 40px intervals feels architectural. That same grid at 20px feels like graph paper. A glow with a 0 0 8px spread is a highlight. The same glow at 0 0 40px is an eyesore. The margin for error is genuinely slim.

That said, once you nail the visual recipe, it's surprisingly reusable. You can apply the same system to stats widgets, profile cards, notification toasts — any container surface. The cyberpunk style hub on Empire UI has ready-made components if you want to skip the build and grab production-ready pieces instead. But building it yourself once means you'll actually understand what you're tweaking.

The core ingredients: a dark base (not pure #000000 — use #0a0a0f or #080b14), a CSS grid background made with linear-gradient, a box-shadow neon glow, a scanline pseudo-element, and clipped or angled corners via clip-path. We'll build each layer separately so you can swap any piece out.

The Dark Grid Background

The grid is the foundation. Forget SVG patterns here — two overlapping linear-gradient declarations do the job with zero markup overhead and full CSS-only control.

.cyber-card {
  background-color: #080b14;
  background-image:
    linear-gradient(rgba(0, 255, 255, 0.04) 1px, transparent 1px),
    linear-gradient(90deg, rgba(0, 255, 255, 0.04) 1px, transparent 1px);
  background-size: 40px 40px;
}

The rgba(0, 255, 255, 0.04) opacity is critical. At 0.04 the grid reads as a subtle texture — present but not fighting your content. Push it to 0.1 and suddenly you're looking at a spreadsheet. Quick aside: the 40px grid size corresponds to roughly the same density you'd see in Blade Runner 2049 terminal screens — that movie from 2017 set a lot of the visual vocabulary we're still borrowing.

In practice, you might want the grid to feel like it recedes into the distance. You can fake perspective with a transform: perspective(500px) rotateX(3deg) on the grid layer, but that requires a separate pseudo-element so it doesn't tilt your card content. Worth noting: pseudo-elements can't have children, so any animated perspective grid needs to sit behind your content via position: absolute and z-index: 0.

Here's the extended version with a proper background layer: ``css .cyber-card { position: relative; background-color: #080b14; overflow: hidden; } .cyber-card::before { content: ''; position: absolute; inset: 0; background-image: linear-gradient(rgba(0, 255, 255, 0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 255, 255, 0.05) 1px, transparent 1px); background-size: 40px 40px; z-index: 0; pointer-events: none; } ``

Neon Border and Corner Clips

A plain border: 1px solid #0ff looks flat. You want the border to *glow* — which means layering box-shadow values. One inset glow, one outward blur, and optionally a second color-shifted glow for chromatic aberration. That last trick reads as expensive but it's one extra shadow value.

.cyber-card {
  border: 1px solid rgba(0, 255, 255, 0.6);
  box-shadow:
    0 0 8px rgba(0, 255, 255, 0.3),
    0 0 24px rgba(0, 255, 255, 0.12),
    inset 0 0 12px rgba(0, 255, 255, 0.05);
}

The clipped corner is what takes it from "dark card" to "cyberpunk card". clip-path handles this cleanly: ``css .cyber-card { clip-path: polygon( 0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px) ); } ``

One more thing — clip-path clips the box-shadow too, so you lose the outward glow. The fix is wrapping the card in a parent div and applying the shadow there with a matching shape via filter: drop-shadow(). Drop shadows respect clip-path masks: ``css .cyber-card-wrapper { filter: drop-shadow(0 0 8px rgba(0, 255, 255, 0.4)) drop-shadow(0 0 20px rgba(0, 255, 255, 0.15)); } ``

Look, this two-div pattern feels redundant but it's genuinely the cleanest approach. Single-element solutions with ::after glow layers exist but they break whenever you add overflow: hidden to the card for the scanline effect — and you will add that.

Scanlines Overlay in CSS

Scanlines sell the CRT monitor nostalgia. They're a repeating horizontal stripe — dark lines every 2–4px — sitting above your content at low opacity. Done wrong they're just visual noise. Done right they add just enough analog texture to make the card feel like a terminal output rather than a React component.

.cyber-card::after {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    0deg,
    transparent,
    transparent 2px,
    rgba(0, 0, 0, 0.15) 2px,
    rgba(0, 0, 0, 0.15) 4px
  );
  z-index: 1;
  pointer-events: none;
}

The opacity 0.15 on the dark stripe is the sweet spot for most monitors. On OLED screens it's almost invisible, which is fine — the effect doesn't need to be heavy. What it needs is to be there, subtly. Worth noting: if you're already using ::before for the grid, you can stack both in ::after with a combined background or use a dedicated child div. The latter is cleaner for maintainability.

Want animated scanlines — a scrolling line drifting down the card? One rule: ``css @keyframes scan { 0% { background-position: 0 0; } 100% { background-position: 0 100%; } } .cyber-card::after { animation: scan 8s linear infinite; } `` The 8s duration keeps it slow enough to read as ambient rather than distracting. Anything under 4s and users will notice it while reading content, which breaks focus.

Putting It All Together: The React Component

Let's wire this into a proper React component with TypeScript props. The component accepts a glowColor prop so you can swap between cyan, magenta, and yellow variants — useful when you're building a dashboard where different card types need visual distinction.

import React from 'react';
import styles from './CyberCard.module.css';

interface CyberCardProps {
  children: React.ReactNode;
  glowColor?: string;
  className?: string;
}

export function CyberCard({
  children,
  glowColor = '0, 255, 255',
  className = '',
}: CyberCardProps) {
  return (
    <div
      className={`${styles.wrapper} ${className}`}
      style={{
        '--glow': glowColor,
      } as React.CSSProperties}
    >
      <div className={styles.card}>
        <div className={styles.content}>{children}</div>
      </div>
    </div>
  );
}

And the CSS module: ``css /* CyberCard.module.css */ .wrapper { filter: drop-shadow(0 0 8px rgba(var(--glow), 0.45)) drop-shadow(0 0 24px rgba(var(--glow), 0.15)); } .card { position: relative; background-color: #080b14; border: 1px solid rgba(var(--glow), 0.55); clip-path: polygon( 0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 16px 100%, 0 calc(100% - 16px) ); overflow: hidden; } .card::before { content: ''; position: absolute; inset: 0; background-image: linear-gradient(rgba(var(--glow), 0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(var(--glow), 0.04) 1px, transparent 1px); background-size: 40px 40px; pointer-events: none; z-index: 0; } .card::after { content: ''; position: absolute; inset: 0; background: repeating-linear-gradient( 0deg, transparent, transparent 2px, rgba(0, 0, 0, 0.12) 2px, rgba(0, 0, 0, 0.12) 4px ); pointer-events: none; z-index: 2; } .content { position: relative; z-index: 1; padding: 24px; } ``

Usage is straightforward: ``tsx <CyberCard glowColor="255, 0, 255"> <h2>SYS_STATUS</h2> <p>All nodes nominal. Uptime 99.7%</p> </CyberCard> ``

The --glow CSS variable carrying the raw RGB tuple is a pattern worth stealing for any themed component system. You pass '255, 0, 255' instead of '#ff00ff' because CSS doesn't let you decompose hex inside rgba() at runtime without a preprocessor. This way the variable drops straight into rgba(var(--glow), 0.4) without any JS color parsing.

Hover States and Micro-interactions

A static card is fine. A card that reacts to hover is better — it signals interactivity and rewards attention without requiring a click. The cyberpunk aesthetic lends itself well to hover states because the glow intensity can shift dramatically without feeling jarring against an already-loud design.

.wrapper {
  transition: filter 0.2s ease;
}

.wrapper:hover {
  filter:
    drop-shadow(0 0 12px rgba(var(--glow), 0.7))
    drop-shadow(0 0 40px rgba(var(--glow), 0.25));
}

.card {
  transition: border-color 0.2s ease;
}

.wrapper:hover .card {
  border-color: rgba(var(--glow), 0.9);
}

One more thing — if you want a corner accent that animates in on hover, use a ::before on the .content div (not the card, since the card's ::before is already taken by the grid). A small 16px L-shaped highlight in the top-left corner using border-top and border-left reads well: ``css .content::before { content: ''; position: absolute; top: 8px; left: 8px; width: 16px; height: 16px; border-top: 2px solid rgba(var(--glow), 0.8); border-left: 2px solid rgba(var(--glow), 0.8); transition: opacity 0.2s ease; opacity: 0; } .wrapper:hover .content::before { opacity: 1; } ``

If your project already uses Framer Motion, you can replace the CSS transitions with motion.div variants and get spring physics on the glow intensity via animate={{ filter: '...' }}. That said, CSS transitions are genuinely sufficient here and don't add a dependency. Save Framer Motion for things CSS can't do — layout animations, drag interactions, route transitions.

When to Use This vs. Other Dark UI Styles

Cyberpunk cards aren't universally appropriate. If you're building a SaaS dashboard for accountants, this aesthetic will actively hurt conversions. The style reads as high-tech, gaming-adjacent, and slightly dangerous — which is exactly right for dev tools, game UIs, crypto dashboards, terminal emulators, and anything where the user self-identifies as technically sophisticated.

The closest neighbor in the dark-UI space is glassmorphism, which uses translucency and blur to create depth rather than glow and texture. Glassmorphism is more universally polished. Cyberpunk is more niche and intentional. They don't mix well in the same UI — pick one aesthetic system and stick to it. You can see the full contrast between these approaches in the glassmorphism vs neumorphism breakdown if you want to think through the tradeoffs more carefully.

For components you want to ship fast without rebuilding this from scratch, browse the Empire UI component library. The cyberpunk section has cards, stat widgets, and HUD-style containers that implement exactly this pattern — grid background, neon glow, scanlines — with theming built in. It's worth checking before you spend two hours tuning box-shadow values manually.

In practice, the maintenance cost of custom dark-UI components is low once they're built. The CSS is mostly self-contained, the React props surface is small, and the visual output doesn't depend on external services or third-party packages. That makes it a genuinely good candidate for your own component library rather than a one-off page component. Build it right once, drop it in everywhere.

FAQ

Can I use this cyberpunk card with Tailwind CSS instead of CSS modules?

Yes, but the repeating-linear-gradient scanline and the clip-path corner need arbitrary value syntax like [polygon(...)]. It works, it's just verbose. CSS modules or a plain stylesheet is cleaner for this specific pattern.

Why does my box-shadow glow disappear when I add clip-path?

clip-path clips the painted area including shadows. Wrap the card in a parent div and apply filter: drop-shadow() there instead — drop shadows respect the clipped shape.

How do I make the neon border color dynamic per card instance?

Pass the RGB tuple as a CSS custom property via inline style (e.g. style={{ '--glow': '255, 0, 255' }}), then reference it inside your stylesheet with rgba(var(--glow), 0.6). No JavaScript color parsing needed.

Does the scanline overlay affect text readability?

At 0.12–0.15 opacity it's practically invisible on most displays and doesn't hurt readability. If you're seeing contrast issues, check that your text z-index is above the scanline pseudo-element (z-index 2 or higher).

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

Read next

Cyberpunk Navbar in React: HUD-Style Navigation with Neon AccentsCyberpunk UI Design: Neon, Grids and Dark Dystopia for the WebCyberpunk Design in Tailwind: Neon, Dark and Grid PatternsUser Profile Card in React: Avatar, Stats, Follow Button, Bio