20 CSS Text Gradient Effects 20 / 20

Dark Mode vs Light Mode CSS Text Gradient

A toggle-enabled card that switches between vivid neon gradients (dark) and rich jewel-tone gradients (light) via CSS variables and a JS class swap.

CSS + JS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="tg-20" id="tg-20-root">
  <div class="tg-20__card">
    <div class="tg-20__header">
      <div class="tg-20__brand">
        <span class="tg-20__icon">◈</span>
        <span class="tg-20__brand-name"><span class="tg-20__grad">Lumis</span></span>
      </div>
      <button class="tg-20__toggle" id="tg-20-toggle" aria-label="Toggle colour scheme">
        <span class="tg-20__toggle-icon tg-20__moon">☾</span>
        <span class="tg-20__toggle-icon tg-20__sun">☀</span>
      </button>
    </div>
    <h1 class="tg-20__title">Adaptive <span class="tg-20__grad">Colour</span><br>for Every Context</h1>
    <p class="tg-20__body">Gradient variables swap automatically between light and dark schemes via <code>prefers-color-scheme</code> and a JS toggle. Vivid saturated gradients work on dark backgrounds; softer, deeper tones on light.</p>
    <div class="tg-20__chips">
      <span class="tg-20__chip tg-20__grad-text">Dark: vivid neon</span>
      <span class="tg-20__chip tg-20__grad-text-alt">Light: rich jewel</span>
    </div>
  </div>
</div>
.tg-20, .tg-20 *, .tg-20 *::before, .tg-20 *::after { margin:0; padding:0; box-sizing:border-box; }

/* Dark defaults */
.tg-20 {
  /* Dark-mode gradient: vivid, high-saturation */
  --grad: linear-gradient(110deg, #f0abfc 0%, #818cf8 50%, #38bdf8 100%);
  --grad-alt: linear-gradient(110deg, #34d399 0%, #06b6d4 100%);
  --bg: #06030f;
  --surface: #10101a;
  --text: #f0e7ff;
  --muted: rgba(240,231,255,.5);
  --border: rgba(255,255,255,.08);
  --toggle-bg: rgba(255,255,255,.07);
  --moon-op: 1;
  --sun-op: 0;
  font-family: system-ui, -apple-system, sans-serif;
  background: var(--bg);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 44px 24px;
  transition: background .3s, color .3s;
}

/* Light-mode override via media query */
@media (prefers-color-scheme: light) {
  .tg-20:not(.tg-20--force-dark) {
    --grad: linear-gradient(110deg, #4f46e5 0%, #7c3aed 55%, #a21caf 100%);
    --grad-alt: linear-gradient(110deg, #0f766e 0%, #0284c7 100%);
    --bg: #f8fafc;
    --surface: #ffffff;
    --text: #0f172a;
    --muted: #64748b;
    --border: #e2e8f0;
    --toggle-bg: #e2e8f0;
    --moon-op: 0;
    --sun-op: 1;
  }
}

/* JS-toggled light class */
.tg-20--light {
  --grad: linear-gradient(110deg, #4f46e5 0%, #7c3aed 55%, #a21caf 100%);
  --grad-alt: linear-gradient(110deg, #0f766e 0%, #0284c7 100%);
  --bg: #f8fafc;
  --surface: #ffffff;
  --text: #0f172a;
  --muted: #64748b;
  --border: #e2e8f0;
  --toggle-bg: #e2e8f0;
  --moon-op: 0;
  --sun-op: 1;
}

.tg-20__card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 36px 32px;
  max-width: 520px;
  width: 100%;
  transition: background .3s, border-color .3s;
}

.tg-20__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 28px;
}

.tg-20__brand { display: flex; align-items: center; gap: 8px; }
.tg-20__icon {
  font-size: 1.3rem;
  background: var(--grad);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
.tg-20__brand-name { font-size: 1.25rem; font-weight: 800; }

/* Reusable gradient text — driven by --grad variable */
.tg-20__grad {
  background: var(--grad);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}
.tg-20__grad-text {
  background: var(--grad);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}
.tg-20__grad-text-alt {
  background: var(--grad-alt);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

.tg-20__toggle {
  background: var(--toggle-bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 12px;
  cursor: pointer;
  transition: background .2s;
  position: relative;
  width: 44px;
  height: 36px;
}
.tg-20__toggle-icon {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%,-50%);
  font-size: 1rem;
  transition: opacity .25s;
}
.tg-20__moon { opacity: var(--moon-op); }
.tg-20__sun  { opacity: var(--sun-op); }

.tg-20__title {
  font-size: clamp(1.6rem, 5vw, 2.5rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -.03em;
  color: var(--text);
  margin-bottom: 16px;
  transition: color .3s;
}

.tg-20__body {
  font-size: .875rem;
  color: var(--muted);
  line-height: 1.7;
  margin-bottom: 22px;
  transition: color .3s;
}
.tg-20__body code {
  font-size: .8em;
  background: var(--border);
  border-radius: 3px;
  padding: 1px 5px;
  color: inherit;
  filter: brightness(1.5);
}

.tg-20__chips { display: flex; gap: 10px; flex-wrap: wrap; }
.tg-20__chip {
  font-size: .8rem;
  font-weight: 700;
  padding: 5px 14px;
  border-radius: 99px;
  border: 1px solid var(--border);
}

@media (prefers-reduced-motion: reduce) {
  .tg-20, .tg-20__card, .tg-20__title, .tg-20__body, .tg-20__toggle { transition: none; }
}
(function(){
  var root = document.getElementById('tg-20-root');
  var btn  = document.getElementById('tg-20-toggle');
  var light = false;
  btn.addEventListener('click', function(){
    light = !light;
    root.classList.toggle('tg-20--light', light);
    root.classList.toggle('tg-20--force-dark', !light);
  });
})();

How this works

The light/dark swap is driven by two CSS custom property sets on the .tg-20 root element. The dark defaults are declared at the root; a @media (prefers-color-scheme: light) block overrides them for system-preference-light users. A JavaScript toggle adds/removes the .tg-20--light class, which contains a third identical property override block, allowing runtime switching independent of the OS setting.

Dark-mode gradients use vivid, high-lightness pastels (70–80% HSL lightness) that pop against deep backgrounds without needing high saturation. Light-mode gradients use darker, richer jewel tones (30–50% lightness) to maintain contrast against light surfaces. Using CSS custom properties on the wrapper element means all gradient consumers — headings, icons, chips — update simultaneously with a single class change on the root.

Customize

  • Add a third mode (e.g. 'contrast' or 'vibrant') by defining a fourth CSS variable block in a .tg-20--vibrant class and cycling through all modes in the toggle JS.
  • Use transition: --g-a .4s, --g-b .4s (requires @property registration) for smooth gradient variable interpolation between dark and light modes — requires Chrome 85+.
  • Extend the media query approach to swap fonts as well: define --font-weight: 800 in dark mode and --font-weight: 700 in light mode so text weight adapts alongside the gradient intensity.
  • Save the user's preference to localStorage and read it on page load so the chosen mode persists across sessions without re-clicking the toggle.
  • Test your actual OS setting by opening the demo in an incognito window — it will default to whatever prefers-color-scheme the OS reports, bypassing the JS toggle state.

Watch out for

  • CSS custom properties used in gradient definitions (e.g. var(--grad)) cannot be transitioned between values in most browsers without registering them via @property. The class-swap approach used here achieves an instant switch, not a smooth interpolation.
  • The --moon-op and --sun-op approach for toggling icons relies on setting opacity to 0 or 1 rather than using display: none, ensuring the toggle button size remains stable regardless of which icon is visible.
  • When persisting the mode to localStorage, be aware that prefers-color-scheme media queries still run at page load before JS executes. Add the class to document.documentElement immediately from a <script> in <head> to prevent a flash of the wrong mode.

Browser support

ChromeSafariFirefoxEdge
69+ 12.1+ 83+ 69+

CSS custom property variable-based gradient swapping is instant (no interpolation) in all browsers; smooth gradient transitions require @property registration available in Chrome 85+ only.

Search CodeFronts

Loading…