Pure CSSMIT licensed

Neon Heading and Display Type Glow

Display type as a typographic problem rather than an effect: how stroke weight and letter-spacing govern how much glow a glyph can carry before its counters fill in and the whole line smears. Four specimens hold the same glow stack while weight and tracking change, so the failure point is visible instead of theoretical.

Live Demo
Try it

Untitled UI Pro: a 10,000+ component Figma UI kit · from $129 one-time

Untitled UI · Affiliate link

Get Untitled UI Pro

The code

<section class="nn-10" aria-labelledby="nn-10-heading">
  <div class="nn-10__stage">
    <div class="nn-10__theme">
      <input class="nn-10__tin" type="checkbox" id="nn-10-theme">
      <label class="nn-10__tlab" for="nn-10-theme">
        <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><circle cx="8" cy="8" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"></circle></svg>
        Flip light / dark
      </label>
    </div>
    <header class="nn-10__head">
      <p class="nn-10__kicker">Weight &middot; tracking &middot; aperture</p>
      <h2 class="nn-10__heading" id="nn-10-heading">How much glow can a glyph carry?</h2>
      <p class="nn-10__sub">Same stack, same hue, same size on all four lines. Only weight and letter-spacing change &mdash; and that decides whether the word survives.</p>
    </header>

    <ol class="nn-10__ladder">
      <li class="nn-10__step">
        <p class="nn-10__spec nn-10__spec--w300">Meridian</p>
        <div class="nn-10__meta">
          <p class="nn-10__grade nn-10__grade--warn">300 / tracking 0.02em</p>
          <p class="nn-10__note">Strokes are thinner than the core layer, so the glyph starts to dissolve into its own light.</p>
        </div>
      </li>
      <li class="nn-10__step">
        <p class="nn-10__spec nn-10__spec--w500">Meridian</p>
        <div class="nn-10__meta">
          <p class="nn-10__grade nn-10__grade--ok">500 / tracking 0.04em</p>
          <p class="nn-10__note">The working range. Stroke reads as solid, counters stay open, halo sits outside the letter.</p>
        </div>
      </li>
      <li class="nn-10__step">
        <p class="nn-10__spec nn-10__spec--w800">Meridian</p>
        <div class="nn-10__meta">
          <p class="nn-10__grade nn-10__grade--ok">800 / tracking 0.1em</p>
          <p class="nn-10__note">Heavy weights need the extra tracking; without it the halos of adjacent stems merge.</p>
        </div>
      </li>
      <li class="nn-10__step">
        <p class="nn-10__spec nn-10__spec--tight">Meridian</p>
        <div class="nn-10__meta">
          <p class="nn-10__grade nn-10__grade--bad">800 / tracking 0 &mdash; smears</p>
          <p class="nn-10__note">The counters fill and the gaps between letters glow brighter than the strokes. Same stack, no room.</p>
        </div>
      </li>
    </ol>

    <div class="nn-10__rule">
      <p class="nn-10__rtext">Every layer here is sized in <span class="nn-10__code">em</span>, so the light scales with the type: <span class="nn-10__code">0 0 .06em</span> core, <span class="nn-10__code">0 0 .3em</span> halo, <span class="nn-10__code">0 0 .8em</span> bloom.</p>
    </div>
  </div>
</section>
.nn-10 {
  --nn-10-bad: #f4a4a4;
  --nn-10-warn: #f0c88a;
  --nn-10-spec-neon: #ff6ec7;
}

@media (prefers-color-scheme: light) {
  .nn-10 {
    --nn-10-bad: #a12a20;
    --nn-10-warn: #78490a;
  }

  .nn-10:has(#nn-10-theme:checked) {
    --nn-10-bad: #f4a4a4;
    --nn-10-warn: #f0c88a;
  }
}

@media (prefers-color-scheme: dark) {
  .nn-10:has(#nn-10-theme:checked) {
    --nn-10-bad: #a12a20;
    --nn-10-warn: #78490a;
  }
}

.nn-10[data-theme="light"] {
  --nn-10-bad: #a12a20;
  --nn-10-warn: #78490a;
}

.nn-10 {
  --nn-10-spec-ink: #fdeef3;
  --nn-10-spec-dim: #bb8c9c;
}

.nn-10__theme {
  position: absolute;
  inset-block-start: clamp(10px, 2vw, 18px);
  inset-inline-end: clamp(10px, 2vw, 18px);
  z-index: 5;
  display: flex;
  justify-content: flex-end;
}

.nn-10__tin {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.nn-10__tlab {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  min-height: 44px;
  padding: 0 .8rem;
  border: 1px solid currentColor;
  font-size: .7rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: .7;
}

.nn-10__tlab svg {
  width: .9rem;
  height: .9rem;
  flex: none;
}

.nn-10__tlab:hover {
  opacity: 1;
}

.nn-10__tin:focus-visible + .nn-10__tlab {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

@media (prefers-color-scheme: light) {
  .nn-10:has(#nn-10-theme:checked) {
    --nn-10-bg: #150409;
    --nn-10-well: #1e070d;
    --nn-10-line: #3a1520;
    --nn-10-ink: #fdeef3;
    --nn-10-muted: #bb8c9c;
    --nn-10-neon: #ff6ec7;
  }
}

@media (prefers-color-scheme: dark) {
  .nn-10:has(#nn-10-theme:checked) {
    --nn-10-bg: #f1e9ec;
    --nn-10-well: #26101a;
    --nn-10-line: #d0bfc6;
    --nn-10-ink: #1a0a10;
    --nn-10-muted: #5e4650;
    --nn-10-neon: #a31462;
  }
}

.nn-10 {
  --nn-10-hue: 340;
  --nn-10-bg: #150409;
  --nn-10-well: #1e070d;
  --nn-10-line: #3a1520;
  --nn-10-ink: #fdeef3;
  --nn-10-muted: #bb8c9c;
  --nn-10-neon: #ff6ec7;
  --nn-10-core-a: .9;
  --nn-10-halo-a: .45;
  --nn-10-bloom-a: .22;
  --nn-10-core: rgba(255, 110, 199, .9);
  --nn-10-halo: rgba(255, 110, 199, .45);
  --nn-10-bloom: rgba(255, 110, 199, .22);
  --nn-10-font-display: "Avenir Next Condensed", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: block;
  box-sizing: border-box;
  background: var(--nn-10-bg);
  color: var(--nn-10-ink);
  font-family: system-ui, sans-serif;
}

.nn-10 *,
.nn-10 *::before,
.nn-10 *::after {
  box-sizing: border-box;
}

@supports (color: oklch(50% 0.1 200)) {
  .nn-10 {
    --nn-10-bg: oklch(15% 0.045 12);
    --nn-10-well: oklch(19% 0.055 12);
    --nn-10-line: oklch(30% 0.07 8);
    --nn-10-ink: oklch(96% 0.018 350);
    --nn-10-muted: oklch(74% 0.045 350);
    --nn-10-neon: oklch(78% 0.2 var(--nn-10-hue));
    --nn-10-core: oklch(80% 0.2 var(--nn-10-hue) / var(--nn-10-core-a));
    --nn-10-halo: oklch(76% 0.19 var(--nn-10-hue) / var(--nn-10-halo-a));
    --nn-10-bloom: oklch(72% 0.18 var(--nn-10-hue) / var(--nn-10-bloom-a));
  }
}

@media (prefers-color-scheme: light) {
  .nn-10 {
    --nn-10-bg: #f1e9ec;
    --nn-10-well: #26101a;
    --nn-10-line: #d0bfc6;
    --nn-10-ink: #1a0a10;
    --nn-10-muted: #5e4650;
    --nn-10-neon: #a31462;
  }
}

.nn-10[data-theme="light"] {
  --nn-10-bg: #f1e9ec;
  --nn-10-well: #26101a;
  --nn-10-line: #d0bfc6;
  --nn-10-ink: #1a0a10;
  --nn-10-muted: #5e4650;
  --nn-10-neon: #a31462;
}

.nn-10[data-theme="dark"] {
  --nn-10-bad: #f4a4a4;
  --nn-10-warn: #f0c88a;
  --nn-10-bg: #150409;
  --nn-10-well: #1e070d;
  --nn-10-line: #3a1520;
  --nn-10-ink: #fdeef3;
  --nn-10-muted: #bb8c9c;
  --nn-10-neon: #ff6ec7;
}

.nn-10__stage {
  max-width: 68rem;
  margin: 0 auto;
  padding: clamp(1.75rem, 5vw, 2.75rem) clamp(1.25rem, 5vw, 2.5rem) clamp(2.25rem, 6vw, 3rem);
  display: grid;
  gap: clamp(1.75rem, 4.5vw, 2.5rem);
}

.nn-10__kicker {
  margin: 0 0 .5rem;
  font-family: var(--nn-10-font-display);
  font-size: .8rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--nn-10-neon);
}

.nn-10__heading {
  margin: 0;
  font-family: var(--nn-10-font-display);
  font-size: clamp(1.6rem, 6vw, 2.6rem);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .02em;
  text-wrap: balance;
}

.nn-10__sub {
  margin: .6rem 0 0;
  max-width: 42rem;
  font-size: .92rem;
  line-height: 1.65;
  color: var(--nn-10-muted);
  text-wrap: pretty;
}

.nn-10__ladder {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0;
  border-top: 1px solid var(--nn-10-line);
}

.nn-10__step {
  display: grid;
  gap: 1rem;
  align-items: center;
  padding: clamp(1.5rem, 4vw, 2.25rem) 0;
  border-bottom: 1px solid var(--nn-10-line);
  grid-template-columns: 1fr;
}

@media (min-width: 54rem) {
  .nn-10__step {
    grid-template-columns: minmax(0, 1fr) minmax(0, 18rem);
    gap: 2rem;
  }
}

.nn-10__spec {
  margin: 0;
  font-family: var(--nn-10-font-display);
  font-size: clamp(2.4rem, 12vw, 4.5rem);
  line-height: 1;
  color: var(--nn-10-ink);
  text-transform: uppercase;
  min-inline-size: 0;
  text-shadow: 0 0 .06em var(--nn-10-core), 0 0 .3em var(--nn-10-halo), 0 0 .8em var(--nn-10-bloom);
}

.nn-10__spec--w300 {
  font-weight: 300;
  letter-spacing: .02em;
}

.nn-10__spec--w500 {
  font-weight: 500;
  letter-spacing: .04em;
}

.nn-10__spec--w800 {
  font-weight: 800;
  letter-spacing: .1em;
}

.nn-10__spec--tight {
  font-weight: 800;
  letter-spacing: 0;
}

.nn-10__meta {
  display: grid;
  gap: .4rem;
  min-inline-size: 0;
}

.nn-10__grade {
  margin: 0;
  font-family: ui-monospace, Menlo, monospace;
  font-size: .74rem;
  letter-spacing: .04em;
}

.nn-10__grade--ok {
  color: var(--nn-10-neon);
}

.nn-10__grade--warn {
  color: var(--nn-10-warn);
}

.nn-10__grade--bad {
  color: var(--nn-10-bad);
}

.nn-10__note {
  margin: 0;
  font-size: .82rem;
  line-height: 1.6;
  color: var(--nn-10-muted);
  text-wrap: pretty;
}

.nn-10__rule {
  padding: 1.1rem 1.25rem;
  border: 1px solid var(--nn-10-line);
}

.nn-10__rtext {
  margin: 0;
  font-size: .88rem;
  line-height: 1.7;
  color: var(--nn-10-ink);
  text-wrap: pretty;
}

.nn-10__code {
  font-family: ui-monospace, Menlo, monospace;
  font-size: .82em;
  color: var(--nn-10-neon);
}
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 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: Neon Heading and Display Type Glow
Source: https://codefronts.com/design-styles/css-neon/neon-typography/

Display type as a typographic problem rather than an effect: how stroke weight and letter-spacing govern how much glow a glyph can carry before its counters fill in and the whole line smears. Four specimens hold the same glow stack while weight and tracking change, so the failure point is visible instead of theoretical.
## HTML
```html
<section class="nn-10" aria-labelledby="nn-10-heading">
  <div class="nn-10__stage">
    <div class="nn-10__theme">
      <input class="nn-10__tin" type="checkbox" id="nn-10-theme">
      <label class="nn-10__tlab" for="nn-10-theme">
        <svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M12.6 3.4l-1.4 1.4M4.8 11.2l-1.4 1.4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path><circle cx="8" cy="8" r="2.6" fill="none" stroke="currentColor" stroke-width="1.5"></circle></svg>
        Flip light / dark
      </label>
    </div>
    <header class="nn-10__head">
      <p class="nn-10__kicker">Weight &middot; tracking &middot; aperture</p>
      <h2 class="nn-10__heading" id="nn-10-heading">How much glow can a glyph carry?</h2>
      <p class="nn-10__sub">Same stack, same hue, same size on all four lines. Only weight and letter-spacing change &mdash; and that decides whether the word survives.</p>
    </header>

    <ol class="nn-10__ladder">
      <li class="nn-10__step">
        <p class="nn-10__spec nn-10__spec--w300">Meridian</p>
        <div class="nn-10__meta">
          <p class="nn-10__grade nn-10__grade--warn">300 / tracking 0.02em</p>
          <p class="nn-10__note">Strokes are thinner than the core layer, so the glyph starts to dissolve into its own light.</p>
        </div>
      </li>
      <li class="nn-10__step">
        <p class="nn-10__spec nn-10__spec--w500">Meridian</p>
        <div class="nn-10__meta">
          <p class="nn-10__grade nn-10__grade--ok">500 / tracking 0.04em</p>
          <p class="nn-10__note">The working range. Stroke reads as solid, counters stay open, halo sits outside the letter.</p>
        </div>
      </li>
      <li class="nn-10__step">
        <p class="nn-10__spec nn-10__spec--w800">Meridian</p>
        <div class="nn-10__meta">
          <p class="nn-10__grade nn-10__grade--ok">800 / tracking 0.1em</p>
          <p class="nn-10__note">Heavy weights need the extra tracking; without it the halos of adjacent stems merge.</p>
        </div>
      </li>
      <li class="nn-10__step">
        <p class="nn-10__spec nn-10__spec--tight">Meridian</p>
        <div class="nn-10__meta">
          <p class="nn-10__grade nn-10__grade--bad">800 / tracking 0 &mdash; smears</p>
          <p class="nn-10__note">The counters fill and the gaps between letters glow brighter than the strokes. Same stack, no room.</p>
        </div>
      </li>
    </ol>

    <div class="nn-10__rule">
      <p class="nn-10__rtext">Every layer here is sized in <span class="nn-10__code">em</span>, so the light scales with the type: <span class="nn-10__code">0 0 .06em</span> core, <span class="nn-10__code">0 0 .3em</span> halo, <span class="nn-10__code">0 0 .8em</span> bloom.</p>
    </div>
  </div>
</section>
```
## CSS
```css
.nn-10 {
  --nn-10-bad: #f4a4a4;
  --nn-10-warn: #f0c88a;
  --nn-10-spec-neon: #ff6ec7;
}

@media (prefers-color-scheme: light) {
  .nn-10 {
    --nn-10-bad: #a12a20;
    --nn-10-warn: #78490a;
  }

  .nn-10:has(#nn-10-theme:checked) {
    --nn-10-bad: #f4a4a4;
    --nn-10-warn: #f0c88a;
  }
}

@media (prefers-color-scheme: dark) {
  .nn-10:has(#nn-10-theme:checked) {
    --nn-10-bad: #a12a20;
    --nn-10-warn: #78490a;
  }
}

.nn-10[data-theme="light"] {
  --nn-10-bad: #a12a20;
  --nn-10-warn: #78490a;
}

.nn-10 {
  --nn-10-spec-ink: #fdeef3;
  --nn-10-spec-dim: #bb8c9c;
}

.nn-10__theme {
  position: absolute;
  inset-block-start: clamp(10px, 2vw, 18px);
  inset-inline-end: clamp(10px, 2vw, 18px);
  z-index: 5;
  display: flex;
  justify-content: flex-end;
}

.nn-10__tin {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.nn-10__tlab {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  min-height: 44px;
  padding: 0 .8rem;
  border: 1px solid currentColor;
  font-size: .7rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: .7;
}

.nn-10__tlab svg {
  width: .9rem;
  height: .9rem;
  flex: none;
}

.nn-10__tlab:hover {
  opacity: 1;
}

.nn-10__tin:focus-visible + .nn-10__tlab {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

@media (prefers-color-scheme: light) {
  .nn-10:has(#nn-10-theme:checked) {
    --nn-10-bg: #150409;
    --nn-10-well: #1e070d;
    --nn-10-line: #3a1520;
    --nn-10-ink: #fdeef3;
    --nn-10-muted: #bb8c9c;
    --nn-10-neon: #ff6ec7;
  }
}

@media (prefers-color-scheme: dark) {
  .nn-10:has(#nn-10-theme:checked) {
    --nn-10-bg: #f1e9ec;
    --nn-10-well: #26101a;
    --nn-10-line: #d0bfc6;
    --nn-10-ink: #1a0a10;
    --nn-10-muted: #5e4650;
    --nn-10-neon: #a31462;
  }
}

.nn-10 {
  --nn-10-hue: 340;
  --nn-10-bg: #150409;
  --nn-10-well: #1e070d;
  --nn-10-line: #3a1520;
  --nn-10-ink: #fdeef3;
  --nn-10-muted: #bb8c9c;
  --nn-10-neon: #ff6ec7;
  --nn-10-core-a: .9;
  --nn-10-halo-a: .45;
  --nn-10-bloom-a: .22;
  --nn-10-core: rgba(255, 110, 199, .9);
  --nn-10-halo: rgba(255, 110, 199, .45);
  --nn-10-bloom: rgba(255, 110, 199, .22);
  --nn-10-font-display: "Avenir Next Condensed", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: block;
  box-sizing: border-box;
  background: var(--nn-10-bg);
  color: var(--nn-10-ink);
  font-family: system-ui, sans-serif;
}

.nn-10 *,
.nn-10 *::before,
.nn-10 *::after {
  box-sizing: border-box;
}

@supports (color: oklch(50% 0.1 200)) {
  .nn-10 {
    --nn-10-bg: oklch(15% 0.045 12);
    --nn-10-well: oklch(19% 0.055 12);
    --nn-10-line: oklch(30% 0.07 8);
    --nn-10-ink: oklch(96% 0.018 350);
    --nn-10-muted: oklch(74% 0.045 350);
    --nn-10-neon: oklch(78% 0.2 var(--nn-10-hue));
    --nn-10-core: oklch(80% 0.2 var(--nn-10-hue) / var(--nn-10-core-a));
    --nn-10-halo: oklch(76% 0.19 var(--nn-10-hue) / var(--nn-10-halo-a));
    --nn-10-bloom: oklch(72% 0.18 var(--nn-10-hue) / var(--nn-10-bloom-a));
  }
}

@media (prefers-color-scheme: light) {
  .nn-10 {
    --nn-10-bg: #f1e9ec;
    --nn-10-well: #26101a;
    --nn-10-line: #d0bfc6;
    --nn-10-ink: #1a0a10;
    --nn-10-muted: #5e4650;
    --nn-10-neon: #a31462;
  }
}

.nn-10[data-theme="light"] {
  --nn-10-bg: #f1e9ec;
  --nn-10-well: #26101a;
  --nn-10-line: #d0bfc6;
  --nn-10-ink: #1a0a10;
  --nn-10-muted: #5e4650;
  --nn-10-neon: #a31462;
}

.nn-10[data-theme="dark"] {
  --nn-10-bad: #f4a4a4;
  --nn-10-warn: #f0c88a;
  --nn-10-bg: #150409;
  --nn-10-well: #1e070d;
  --nn-10-line: #3a1520;
  --nn-10-ink: #fdeef3;
  --nn-10-muted: #bb8c9c;
  --nn-10-neon: #ff6ec7;
}

.nn-10__stage {
  max-width: 68rem;
  margin: 0 auto;
  padding: clamp(1.75rem, 5vw, 2.75rem) clamp(1.25rem, 5vw, 2.5rem) clamp(2.25rem, 6vw, 3rem);
  display: grid;
  gap: clamp(1.75rem, 4.5vw, 2.5rem);
}

.nn-10__kicker {
  margin: 0 0 .5rem;
  font-family: var(--nn-10-font-display);
  font-size: .8rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--nn-10-neon);
}

.nn-10__heading {
  margin: 0;
  font-family: var(--nn-10-font-display);
  font-size: clamp(1.6rem, 6vw, 2.6rem);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .02em;
  text-wrap: balance;
}

.nn-10__sub {
  margin: .6rem 0 0;
  max-width: 42rem;
  font-size: .92rem;
  line-height: 1.65;
  color: var(--nn-10-muted);
  text-wrap: pretty;
}

.nn-10__ladder {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 0;
  border-top: 1px solid var(--nn-10-line);
}

.nn-10__step {
  display: grid;
  gap: 1rem;
  align-items: center;
  padding: clamp(1.5rem, 4vw, 2.25rem) 0;
  border-bottom: 1px solid var(--nn-10-line);
  grid-template-columns: 1fr;
}

@media (min-width: 54rem) {
  .nn-10__step {
    grid-template-columns: minmax(0, 1fr) minmax(0, 18rem);
    gap: 2rem;
  }
}

.nn-10__spec {
  margin: 0;
  font-family: var(--nn-10-font-display);
  font-size: clamp(2.4rem, 12vw, 4.5rem);
  line-height: 1;
  color: var(--nn-10-ink);
  text-transform: uppercase;
  min-inline-size: 0;
  text-shadow: 0 0 .06em var(--nn-10-core), 0 0 .3em var(--nn-10-halo), 0 0 .8em var(--nn-10-bloom);
}

.nn-10__spec--w300 {
  font-weight: 300;
  letter-spacing: .02em;
}

.nn-10__spec--w500 {
  font-weight: 500;
  letter-spacing: .04em;
}

.nn-10__spec--w800 {
  font-weight: 800;
  letter-spacing: .1em;
}

.nn-10__spec--tight {
  font-weight: 800;
  letter-spacing: 0;
}

.nn-10__meta {
  display: grid;
  gap: .4rem;
  min-inline-size: 0;
}

.nn-10__grade {
  margin: 0;
  font-family: ui-monospace, Menlo, monospace;
  font-size: .74rem;
  letter-spacing: .04em;
}

.nn-10__grade--ok {
  color: var(--nn-10-neon);
}

.nn-10__grade--warn {
  color: var(--nn-10-warn);
}

.nn-10__grade--bad {
  color: var(--nn-10-bad);
}

.nn-10__note {
  margin: 0;
  font-size: .82rem;
  line-height: 1.6;
  color: var(--nn-10-muted);
  text-wrap: pretty;
}

.nn-10__rule {
  padding: 1.1rem 1.25rem;
  border: 1px solid var(--nn-10-line);
}

.nn-10__rtext {
  margin: 0;
  font-size: .88rem;
  line-height: 1.7;
  color: var(--nn-10-ink);
  text-wrap: pretty;
}

.nn-10__code {
  font-family: ui-monospace, Menlo, monospace;
  font-size: .82em;
  color: var(--nn-10-neon);
}
```

How this works

Glow eats apertures. A blur spreads outward from every edge of a glyph, including the inside edges of its counters — the enclosed space in an e, a or o. On a light weight there is plenty of aperture and the halo has room. On a heavy weight the counters are already narrow, so the same bloom closes them and the word turns into a bright brick. The fix is not less glow; it is more space.

Tracking is the pressure valve. Two adjacent glyphs each carrying a 0.3em halo overlap in the gap between them, and overlapping halos add up, so the space between letters becomes brighter than the letters. Opening letter-spacing as weight or glow increases keeps each glyph's light its own. Condensed faces need this most, which is why the condensed specimen here runs the widest tracking.

Size the glow in em, not px. A 20px halo on a 48px heading and the same 20px halo on a 96px heading are not the same effect; the second looks anaemic. Expressing every layer in em ties the light to the type, so one heading scale change does not require retuning three shadow layers. Combine that with clamp() on the font size and the whole system stays proportional down to 320px.

The trap is faux bold. If the display face has no real bold, the browser synthesises one by smearing the outline — and a synthesised weight has ragged edges that the glow amplifies into fuzz. Check that the weight you are asking for actually exists in the face; if it does not, use a heavier face rather than a heavier number.

Make it yours

  • Retint every specimen by editing --nn-10-hue; the stack is derived from it.
  • Change the glow layers from em to px and resize the heading to see the proportion break.
  • Take the 800-weight specimen's tracking back to 0 to watch adjacent halos merge into a bar.
  • Raise --nn-10-bloom-a from .22 to .45 and note which weight fails first.
  • Swap the display face for a slab or a grotesk; the tracking values will need reopening, which is the lesson.
  • Add a fifth specimen at 200 weight to find the floor where the glow overwhelms the stroke entirely.

Gotchas — read before shipping

  • Heavy weights fail first: the bloom closes their counters and the word reads as a lit block rather than letters.
  • Zero tracking on a glowing condensed face makes the gaps between letters brighter than the letters themselves.
  • Glow radii in px break the moment the heading scale changes; em keeps the light proportional to the type.
  • A synthesised bold weight has ragged outlines, and a blur amplifies that raggedness into visible fuzz.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 79+.

clamp() on the type scale sets the floor; text-shadow with em-based radii is Baseline since Chrome 10 / Safari 5.1 / Firefox 4. oklch() lives inside an @supports gate with hex declared first, so the fallback path renders the same four specimens with the hue written out per layer. The script-free theme toggle uses :has() (Chrome 105 / Safari 15.4 / Firefox 121) after the @media rules, so without it the demo follows the system colour scheme and the control does nothing.

Techniques used in this demo

Search CodeFronts

Loading…