20 CSS Neon Text Effects18 / 20

CSS + JSMIT licensed

Interactive Neon On/Off Sign Switch

A neon sign with a physical pull-switch: dark and dead until you flip it, then it strikes to life with a realistic warm-up stutter. The interactive 'turn the sign on' pattern gap that plain glow demos miss.

Published

Live Demo
Try it

The code

<div class="cnt-18 is-off" id="cnt-18">
  <div class="cnt-18__sign">
    <span class="cnt-18__tube">MOTEL</span>
    <span class="cnt-18__tube cnt-18__sub">no vacancy</span>
  </div>
  <button class="cnt-18__switch" type="button" id="cnt-18-switch" aria-pressed="false">
    <span class="cnt-18__knob"></span><span class="cnt-18__stext">flip the switch</span>
  </button>
</div>
.cnt-18,
.cnt-18 *,
.cnt-18 *::before,
.cnt-18 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cnt-18 ::selection {
  background: oklch(0.78 0.19 25);
  color: #fff;
}

.cnt-18 {
  --accent: oklch(0.78 0.2 25);
  --off: color-mix(in oklch,var(--accent) 16%,#241417);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  gap: 44px;
  align-items: center;
  justify-content: center;
  padding: 60px 20px;
  background: radial-gradient(90% 90% at 50% 40%,#130d0f,#060405);
}

.cnt-18__sign {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.cnt-18__tube {
  font-size: clamp(48px,12vw,110px);
  font-weight: 900;
  letter-spacing: .08em;
  line-height: 1;
  color: var(--off);
  text-shadow: none;
  transition: color .3s;
}

.cnt-18__sub {
  font-size: clamp(18px,4vw,32px);
  letter-spacing: .32em;
}

.cnt-18.is-on .cnt-18__tube {
  color: var(--accent);
  animation: cnt-18-strike 1.1s ease both;
}

.cnt-18.is-on .cnt-18__sub {
  animation-delay: .35s;
}

@keyframes cnt-18-strike {
  0% {
    color: var(--off);
    text-shadow: none;
  }

  12% {
    color: #fff;
    text-shadow: 0 0 6px #fff,0 0 16px var(--accent);
  }

  16% {
    color: var(--off);
    text-shadow: none;
  }

  28% {
    color: #fff;
    text-shadow: 0 0 6px #fff,0 0 18px var(--accent);
  }

  33% {
    color: var(--off);
    text-shadow: none;
  }

  45% {
    color: var(--accent);
    text-shadow: 0 0 6px #fff,0 0 18px var(--accent),0 0 40px var(--accent);
  }

  100% {
    color: var(--accent);
    text-shadow: 0 0 5px #fff,0 0 15px var(--accent),0 0 36px var(--accent),0 0 72px var(--accent);
  }
}

.cnt-18.is-on .cnt-18__tube {
  text-shadow: 0 0 5px #fff,0 0 15px var(--accent),0 0 36px var(--accent),0 0 72px var(--accent);
}

.cnt-18__switch {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  cursor: pointer;
  padding: 8px 18px 8px 8px;
  border-radius: 999px;
  border: 1px solid #2c1e21;
  background: #120c0e;
  font: 12px ui-monospace,Menlo,monospace;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: #9a7f84;
}

.cnt-18__knob {
  width: 44px;
  height: 24px;
  border-radius: 999px;
  background: #241417;
  position: relative;
  transition: background .25s;
}

.cnt-18__knob::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #6b5257;
  transition: transform .25s,background .25s;
}

.cnt-18.is-on .cnt-18__knob {
  background: color-mix(in oklch,var(--accent) 40%,#241417);
}

.cnt-18.is-on .cnt-18__knob::after {
  transform: translateX(20px);
  background: var(--accent);
  box-shadow: 0 0 12px var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .cnt-18.is-on .cnt-18__tube {
    animation: none;
  }
}
(function(){
  var root = document.getElementById('cnt-18');
  var btn = document.getElementById('cnt-18-switch');
  if(!root || !btn) return;
  btn.addEventListener('click', function(){
    var on = root.classList.toggle('is-on');
    root.classList.toggle('is-off', !on);
    btn.setAttribute('aria-pressed', String(on));
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Neon Text Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Interactive Neon On/Off Sign Switch
Source: https://codefronts.com/motion/css-neon-text-effect/interactive-neon-on-off-sign-switch/

A neon sign with a physical pull-switch: dark and dead until you flip it, then it strikes to life with a realistic warm-up stutter. The interactive 'turn the sign on' pattern gap that plain glow demos miss.
## HTML
```html
<div class="cnt-18 is-off" id="cnt-18">
  <div class="cnt-18__sign">
    <span class="cnt-18__tube">MOTEL</span>
    <span class="cnt-18__tube cnt-18__sub">no vacancy</span>
  </div>
  <button class="cnt-18__switch" type="button" id="cnt-18-switch" aria-pressed="false">
    <span class="cnt-18__knob"></span><span class="cnt-18__stext">flip the switch</span>
  </button>
</div>
```
## CSS
```css
.cnt-18,
.cnt-18 *,
.cnt-18 *::before,
.cnt-18 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cnt-18 ::selection {
  background: oklch(0.78 0.19 25);
  color: #fff;
}

.cnt-18 {
  --accent: oklch(0.78 0.2 25);
  --off: color-mix(in oklch,var(--accent) 16%,#241417);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  gap: 44px;
  align-items: center;
  justify-content: center;
  padding: 60px 20px;
  background: radial-gradient(90% 90% at 50% 40%,#130d0f,#060405);
}

.cnt-18__sign {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.cnt-18__tube {
  font-size: clamp(48px,12vw,110px);
  font-weight: 900;
  letter-spacing: .08em;
  line-height: 1;
  color: var(--off);
  text-shadow: none;
  transition: color .3s;
}

.cnt-18__sub {
  font-size: clamp(18px,4vw,32px);
  letter-spacing: .32em;
}

.cnt-18.is-on .cnt-18__tube {
  color: var(--accent);
  animation: cnt-18-strike 1.1s ease both;
}

.cnt-18.is-on .cnt-18__sub {
  animation-delay: .35s;
}

@keyframes cnt-18-strike {
  0% {
    color: var(--off);
    text-shadow: none;
  }

  12% {
    color: #fff;
    text-shadow: 0 0 6px #fff,0 0 16px var(--accent);
  }

  16% {
    color: var(--off);
    text-shadow: none;
  }

  28% {
    color: #fff;
    text-shadow: 0 0 6px #fff,0 0 18px var(--accent);
  }

  33% {
    color: var(--off);
    text-shadow: none;
  }

  45% {
    color: var(--accent);
    text-shadow: 0 0 6px #fff,0 0 18px var(--accent),0 0 40px var(--accent);
  }

  100% {
    color: var(--accent);
    text-shadow: 0 0 5px #fff,0 0 15px var(--accent),0 0 36px var(--accent),0 0 72px var(--accent);
  }
}

.cnt-18.is-on .cnt-18__tube {
  text-shadow: 0 0 5px #fff,0 0 15px var(--accent),0 0 36px var(--accent),0 0 72px var(--accent);
}

.cnt-18__switch {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  cursor: pointer;
  padding: 8px 18px 8px 8px;
  border-radius: 999px;
  border: 1px solid #2c1e21;
  background: #120c0e;
  font: 12px ui-monospace,Menlo,monospace;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: #9a7f84;
}

.cnt-18__knob {
  width: 44px;
  height: 24px;
  border-radius: 999px;
  background: #241417;
  position: relative;
  transition: background .25s;
}

.cnt-18__knob::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #6b5257;
  transition: transform .25s,background .25s;
}

.cnt-18.is-on .cnt-18__knob {
  background: color-mix(in oklch,var(--accent) 40%,#241417);
}

.cnt-18.is-on .cnt-18__knob::after {
  transform: translateX(20px);
  background: var(--accent);
  box-shadow: 0 0 12px var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .cnt-18.is-on .cnt-18__tube {
    animation: none;
  }
}
```

## JavaScript
```js
(function(){
  var root = document.getElementById('cnt-18');
  var btn = document.getElementById('cnt-18-switch');
  if(!root || !btn) return;
  btn.addEventListener('click', function(){
    var on = root.classList.toggle('is-on');
    root.classList.toggle('is-off', !on);
    btn.setAttribute('aria-pressed', String(on));
  });
})();
```

How this works

Two states live on one wrapper: .is-off renders the letters as a dim, unlit tube (dark stroke, no shadow); toggling to .is-on triggers a one-shot 'strike' @keyframes that stutters through a couple of failed ignitions before settling into a steady glow — the way a real tube warms up. The switch itself is a labelled button.

A single click handler flips the class; the entire visual transition — warm-up stutter, glow, and the switch's own toggle — is CSS. Default state is off, so nothing animates until the user chooses to light it.

Make it yours

  • Retint the tube with --accent; the off-state colour derives from it.
  • Author the warm-up personality in the strike @keyframes — more failed strikes = older sign.
  • Default it on (start with is-on) if you'd rather show the lit state first.
  • Wire the same class toggle to a real feature flag / theme switch for a themed control.

Gotchas — read before shipping

  • Provide a real accessible control — this uses a <button> with aria-pressed, not a bare clickable div.
  • Respect reduced-motion: the block skips the stutter and lights instantly.
  • Keep the off state visibly a sign (dim tube), not blank, so users know it's switchable.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

Floor set by oklch(), color-mix() as this demo is written. The core technique itself goes back further — Chrome 80+.

Class toggle + @keyframes; the one-line JS just flips a class. Fully degradable to a static lit sign.

Techniques used in this demo

Search CodeFronts

Loading…