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

Cyberpunk Form Design: Glitch Inputs, Neon Labels, Scanline Fields

Build cyberpunk forms with glitch text effects, neon-glow labels, and CRT scanline inputs using pure CSS and React — no libraries required.

Neon green terminal form with glitch scanline effect on dark background

Why Forms Are the Hardest Part of Cyberpunk UI

Most cyberpunk tutorials stop at buttons and cards. They give you a neon glow, a dark background, maybe some scanlines on a hero section — and call it done. But forms? That's where the aesthetic either holds up or completely falls apart.

Forms are inherently functional. They're grids of boxes you type into. That utilitarian DNA fights against the dystopian, high-tension vibe of the cyberpunk style. Getting both to coexist takes deliberate decisions at every layer — the border treatment, the label animation, the error state, the focus ring.

Honestly, the best cyberpunk forms look like they were pulled from a 2077 terminal — monospace text, flickering cursor, a label that slides up and momentarily glitches when you tab into the field. That's not hard to build. It's just several small CSS and React decisions stacked on top of each other.

This article walks through all of them. By the end you'll have a fully themed form system — inputs, textareas, selects, error states — that fits right into the Empire UI cyberpunk component set.

The Base: Dark Background, Monospace, Hard Borders

Start with the structural decisions. Cyberpunk forms don't use rounded corners — or if they do, it's 2px max. You want hard geometry. border-radius: 0 is your friend here. The border itself should be thin (1px) and either neon cyan (#00ffff), electric magenta (#ff00ff), or acid green (#39ff14). Pick one per form, don't mix all three or it reads as random.

The background of each input field should be very dark — not pure black, which looks flat, but something like #0a0a0f or #050510. This gives the neon borders something to pop against. Think of it as the contrast ratio doing the work for you.

.cyber-input {
  background: #0a0a0f;
  border: 1px solid #00ffff;
  border-radius: 0;
  color: #e0e0ff;
  font-family: 'JetBrains Mono', 'Courier New', monospace;
  font-size: 14px;
  padding: 12px 16px;
  width: 100%;
  outline: none;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.cyber-input:focus {
  border-color: #ff00ff;
  box-shadow: 0 0 0 1px #ff00ff, 0 0 12px rgba(255, 0, 255, 0.3);
}

Worth noting: the double box-shadow on focus — one tight 1px outline and one diffused glow — is what makes the neon effect look genuine rather than like a regular browser focus ring with a filter slapped on it.

Font choice matters more than most people think. Monospace is non-negotiable. In 2024, JetBrains Mono became the go-to because the letter spacing at 14px is nearly perfect for terminal aesthetics without being unreadable. IBM Plex Mono is a solid alternative if you want something slightly narrower.

Floating Labels That Glitch on Focus

Static labels above inputs are fine. Floating labels — where the label sits inside the input and flies up when you focus — are much better for the aesthetic. They give you a built-in animation moment, and that moment is exactly where you inject the glitch effect.

The pattern here is a <div> wrapper with position: relative, the label set to position: absolute inside it, and a has-value or focused class toggled via React state. Nothing unusual so far. The cyberpunk twist is a brief @keyframes glitch that fires on the label's transform transition.

import { useState } from 'react';

function CyberInput({ label, ...props }: { label: string } & React.InputHTMLAttributes<HTMLInputElement>) {
  const [focused, setFocused] = useState(false);
  const [hasValue, setHasValue] = useState(false);

  return (
    <div className="cyber-field">
      <input
        {...props}
        className="cyber-input"
        onFocus={() => setFocused(true)}
        onBlur={(e) => {
          setFocused(false);
          setHasValue(e.target.value.length > 0);
        }}
        onChange={(e) => setHasValue(e.target.value.length > 0)}
      />
      <label className={`cyber-label ${focused || hasValue ? 'float' : ''} ${focused ? 'glitch' : ''}`}>
        {label}
      </label>
    </div>
  );
}
.cyber-field {
  position: relative;
  margin-bottom: 24px;
}

.cyber-label {
  position: absolute;
  left: 16px;
  top: 12px;
  color: #00ffff;
  font-family: 'JetBrains Mono', monospace;
  font-size: 14px;
  pointer-events: none;
  transition: transform 0.2s ease, font-size 0.2s ease, color 0.2s ease;
  transform-origin: left center;
}

.cyber-label.float {
  transform: translateY(-24px) scale(0.75);
  color: #ff00ff;
}

.cyber-label.glitch {
  animation: label-glitch 0.3s steps(1) forwards;
}

@keyframes label-glitch {
  0%   { clip-path: inset(0 0 90% 0); transform: translateY(-22px) scale(0.75) translateX(-2px); }
  20%  { clip-path: inset(80% 0 0 0); transform: translateY(-26px) scale(0.75) translateX(2px); }
  40%  { clip-path: inset(30% 0 50% 0); transform: translateY(-24px) scale(0.75) translateX(0); }
  60%  { clip-path: none; transform: translateY(-24px) scale(0.75) translateX(-1px); color: #00ffff; }
  100% { clip-path: none; transform: translateY(-24px) scale(0.75) translateX(0); color: #ff00ff; }
}

That clip-path trick is what separates a real glitch from a basic shake animation. You're literally cutting the label into horizontal strips and offsetting them independently, which is exactly how glitch effects work in video corruption. In practice, 0.3s at steps(1) is fast enough to feel reactive but slow enough that users actually register it happened.

CRT Scanline Overlay on Input Fields

The scanline effect is probably the single most recognizable cyberpunk CSS technique. It's a repeating linear gradient of semi-transparent black stripes overlaid on a surface. Most devs apply it to backgrounds. The interesting move is applying it directly to your input fields.

You can't put a pseudo-element on an <input> directly — browsers don't render ::before/::after on replaced elements. So wrap the input in a <div> and hang the scanline on that wrapper instead.

.cyber-field::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    transparent 0px,
    transparent 3px,
    rgba(0, 0, 0, 0.08) 3px,
    rgba(0, 0, 0, 0.08) 4px
  );
  z-index: 1;
}

Keep the scanline opacity low. 0.08 sounds negligible but at a 4px repeat it creates a clearly visible texture without making the text unreadable. Push it to 0.2 and suddenly users can't see what they're typing — which is a UX failure regardless of how good it looks.

One more thing — add a subtle vertical scan animation to sell the CRT illusion. A slow translateY keyframe on a gradient with a brighter stripe gives the effect of the electron beam sweeping downward. It's barely perceptible at 8 seconds but subconsciously reinforces the old-monitor feel.

Error States and Validation: Red Glitch, Not Red Text

Default browser validation styling is terrible for cyberpunk forms. Red border, maybe a tooltip — it looks completely out of place next to your neon cyan inputs. You need to own the error state entirely.

The pattern that works: swap the border and label color to #ff3131 (a harsh red, not the soft coral you'd use in a standard design system), add a horizontal shake animation on the input, and inject a glitch pass on the error message text itself. It communicates failure while staying in the aesthetic.

.cyber-input.error {
  border-color: #ff3131;
  box-shadow: 0 0 0 1px #ff3131, 0 0 16px rgba(255, 49, 49, 0.25);
  animation: input-shake 0.4s ease;
}

@keyframes input-shake {
  0%, 100% { transform: translateX(0); }
  20%       { transform: translateX(-4px); }
  40%       { transform: translateX(4px); }
  60%       { transform: translateX(-3px); }
  80%       { transform: translateX(2px); }
}

.cyber-error-msg {
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  color: #ff3131;
  margin-top: 4px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  animation: error-flicker 0.6s steps(2) forwards;
}

@keyframes error-flicker {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0; }
}

Look, resist the urge to go full-glitch on errors. A massive animated error state draws attention but it also makes users anxious rather than informed. The shake + flicker is enough — it registers immediately, then settles. The user can actually read the message.

If you're using React Hook Form, connect this to the formState.errors object and conditionally apply the .error class. The Empire UI cyberpunk components handle this wiring for you if you'd rather not roll it by hand, but it's straightforward either way.

Select Dropdowns and Textareas: Don't Let Them Break the Theme

Native <select> elements are the bane of custom UI systems. On macOS and Windows they look completely different, and neither looks remotely cyberpunk. You've got two options: appearance: none and heavy CSS, or a custom listbox built with <div> and role="listbox". For a form that needs to be accessible and relatively quick to ship, go with appearance: none.

.cyber-select {
  appearance: none;
  -webkit-appearance: none;
  background: #0a0a0f;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%2300ffff' stroke-width='1.5' fill='none'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  border: 1px solid #00ffff;
  border-radius: 0;
  color: #e0e0ff;
  font-family: 'JetBrains Mono', monospace;
  font-size: 14px;
  padding: 12px 40px 12px 16px;
  cursor: pointer;
}

.cyber-select:focus {
  border-color: #ff00ff;
  box-shadow: 0 0 0 1px #ff00ff, 0 0 12px rgba(255, 0, 255, 0.3);
  outline: none;
}

The SVG chevron inline as a data URI is the cleanest way to replace the native arrow without pulling in an icon library dependency. Style the stroke color to match your primary neon and you're done.

Textareas are easier — they take all the same CSS as your inputs. The one extra thing you want is resize: vertical only (not the default resize: both), and pin a minimum height around 120px. Horizontal resizing at arbitrary widths breaks the grid. Vertical is fine.

Quick aside: scrollbars inside textareas are worth styling too. Chromium browsers respect ::-webkit-scrollbar rules, so a 4px wide scrollbar with a #00ffff thumb against a #0a0a0f track takes 5 lines of CSS and makes the whole form feel more considered. Firefox users get the native scrollbar — that's an acceptable tradeoff.

Putting It Together: Full Form Component

Here's a complete form structure you can drop into any React project. It uses the CSS from earlier sections and wires up basic validation state. No external dependencies beyond React itself.

import { useState, FormEvent } from 'react';
import './cyber-form.css';

interface FormState {
  username: string;
  email: string;
  message: string;
}

export function CyberContactForm() {
  const [values, setValues] = useState<FormState>({ username: '', email: '', message: '' });
  const [errors, setErrors] = useState<Partial<FormState>>({});
  const [submitted, setSubmitted] = useState(false);

  function validate(): boolean {
    const next: Partial<FormState> = {};
    if (!values.username) next.username = 'HANDLE REQUIRED';
    if (!values.email.includes('@')) next.email = 'INVALID ADDRESS';
    if (values.message.length < 10) next.message = 'MESSAGE TOO SHORT';
    setErrors(next);
    return Object.keys(next).length === 0;
  }

  function handleSubmit(e: FormEvent) {
    e.preventDefault();
    if (validate()) setSubmitted(true);
  }

  if (submitted) {
    return <div className="cyber-success">// TRANSMISSION SENT</div>;
  }

  return (
    <form className="cyber-form" onSubmit={handleSubmit} noValidate>
      <CyberInput
        label="// HANDLE"
        value={values.username}
        onChange={(e) => setValues(v => ({ ...v, username: e.target.value }))}
        error={errors.username}
      />
      <CyberInput
        label="// EMAIL"
        type="email"
        value={values.email}
        onChange={(e) => setValues(v => ({ ...v, email: e.target.value }))}
        error={errors.email}
      />
      <CyberTextarea
        label="// MESSAGE"
        value={values.message}
        onChange={(e) => setValues(v => ({ ...v, message: e.target.value }))}
        error={errors.message}
      />
      <button type="submit" className="cyber-submit">SEND TRANSMISSION</button>
    </form>
  );
}

The // HANDLE label prefix is a small touch that pays off a lot. It immediately reads as terminal/CLI, and it's just a string change — no extra CSS. It's the kind of micro-decision that makes the difference between 'dark form' and actually feeling like cyberpunk.

For production use, you'd want to wire this to React Hook Form or Zod for validation, and pull the pre-built inputs from the Empire UI component library. But for a custom one-off form — a landing page contact form, a sign-up for a game, an in-app settings panel — this self-contained version is perfectly viable.

If you want to see how the scanline and glitch effects interact with other UI patterns, check out the gradient generator — it's built with a similar dark terminal aesthetic, and the source is a good reference for how to layer effects without tanking performance on lower-end hardware.

FAQ

Does the glitch animation affect performance on mobile?

The clip-path animation runs on the compositor thread in Chrome 112+ and Safari 17+, so it's fine on modern devices. Keep total animation duration under 400ms and use will-change: transform sparingly — only on elements that actually animate.

Can I use these styles with Tailwind instead of custom CSS?

Yes, but you'll hit Tailwind's limits fast. The clip-path glitch keyframes and the inline SVG chevron for selects need a tailwind.config.js extend.keyframes block or a plain CSS file alongside. Hybrid approach works well.

How do I make cyberpunk forms accessible?

Keep the label-to-input association via for/id or the wrapping pattern shown above. Ensure color contrast meets WCAG AA — #00ffff on #0a0a0f clears 4.5:1 easily. Don't rely on color alone for error states; include the text message.

What font pairs well with the cyberpunk form aesthetic?

JetBrains Mono for input text and labels, with IBM Plex Sans for headings if you need a sans-serif nearby. Both are free on Google Fonts. Avoid variable fonts here — the rendering at small sizes on dark backgrounds can look slightly blurry on non-retina screens.

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

Read next

Cyberpunk Button Design in CSS: Neon Glow, Glitch and Clip-PathCyberpunk UI Design: Neon, Grids and Dark Dystopia for the WebCyberpunk Design in Tailwind: Neon, Dark and Grid PatternsCSS Box Shadow: The Complete Guide With Live Examples