Sparkles Text — Free HTML CSS JS Twinkle Effect Snippet

Sparkles Text · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Bounds-aware spawning
Sparkles appear within the measured headline box.
Randomized properties
Per-sparkle size, color, and lifetime.
Pop-and-vanish keyframe
Scale from zero, rotate, then fade out.
Inline SVG stars
Crisp four-point shapes, no images.
Self-cleaning DOM
Each sparkle removes itself after its life.
Load burst
Six sparkles spawn immediately on load.
Reduced-motion safe
Skips animation when the user prefers it.
Gradient headline
Three-stop background-clip text underneath.

About this UI Snippet

Sparkles Text — Twinkling Four-Point Stars Around a Headline

Screenshot of the Sparkles Text snippet rendered live

Sparkles text is the delightful headline treatment where tiny four-point stars continuously appear, twinkle, and fade around a gradient title — adding a sense of magic or premium polish to a hero. This snippet builds it in plain HTML, CSS, and vanilla JavaScript, spawning lightweight SVG sparkles at random positions within the headline's bounds and cleaning them up automatically.

Spawning within the headline's box

Each sparkle is positioned relative to the live headline. spark() reads the title's getBoundingClientRect and the stage's rect, then picks a random point inside the title's area padded outward by 14px, so sparkles cluster on and just around the letters rather than scattering across the whole page. Because the position is measured each spawn, the effect stays correctly placed even if the headline reflows or the viewport changes — there are no hard-coded coordinates.

Randomized per-sparkle properties

Variety is what sells a twinkle. Every sparkle gets a random size (8–22px), a random color from a small palette, and a random lifetime (0.7–1.5s) passed into CSS as a --life custom property. The randomness means no two sparkles look or behave identically, so the field feels organic rather than like a repeating pattern. The four-point star shape is a single inline SVG path, so it's crisp at any size and weighs almost nothing.

The twinkle animation

Each sparkle runs the skTwinkle keyframe once: it scales up from 0 while rotating to 90°, holds briefly at full size and opacity, then scales back down to 0 while continuing to rotate to 180° and fading out. The scale-from-zero-and-back is what makes it "pop and vanish" like a real sparkle, and the rotation adds life. The keyframe duration is the per-sparkle --life, so different sparkles twinkle at different speeds.

Self-cleaning DOM

Sparkles are created on a 220ms interval, which would pile up thousands of nodes over time. To prevent that, each sparkle removes itself with a setTimeout matched to its own lifetime (plus a small buffer), so the DOM only ever holds the handful currently visible. A short burst of six sparkles is also spawned on load so the effect is alive immediately rather than building up gradually.

Respecting reduced motion

Continuous motion can be uncomfortable or distracting for some users, so the script checks prefers-reduced-motion via matchMedia and skips the interval and the burst entirely when the user has requested reduced motion. The headline still renders with its gradient; it just doesn't twinkle. This is the accessible default for any decorative animation.

The gradient headline

The title itself uses a three-stop background-clip: text gradient (fuchsia to indigo to cyan) so it already shimmers in color, and the sparkles layer on top at a higher z-index to enhance it. The two work together: colorful type plus animated highlights.

Customizing it

Change the spawn interval for a denser or sparser field, adjust the size and lifetime ranges, swap the palette to match your brand, or widen the padding to throw sparkles further from the text. Replace the star path with a different glyph for a snow or confetti variant. Pair it with a text generate reveal or a shimmer button for a magical hero.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA gradient headline renders with sparkles twinkling around it.
  2. 2
    Watch the twinkleStars pop in, rotate, and fade at random sizes and colors.
  3. 3
    Note the placementSparkles cluster on and just around the letters.
  4. 4
    Edit the paletteSwap the COLORS array to match your brand.
  5. 5
    Tune the densityChange the spawn interval and lifetime ranges.
  6. 6
    Check reduced motionEnable the OS setting and the twinkle turns off.

Real-world uses

Common Use Cases

Magical hero headlines
Pair with a shimmer button CTA.
AI product titles
Layer over a text generate reveal.
Seasonal landing pages
Swap stars for snow above a hero section.
Reward moments
Echo the sparkle near a confetti button.
Premium upsells
Highlight a feature on a pricing card.
Particle effect demos
A reference for self-cleaning spawned elements.

Got questions?

Frequently Asked Questions

Each spawn reads the headline's getBoundingClientRect and the stage's rect, then picks a random point inside the title's area padded outward by 14px. Because the box is measured every time, sparkles stay correctly clustered on and around the letters even if the headline reflows or the viewport resizes — no coordinates are hard-coded.

Every sparkle gets a random size from 8 to 22px, a random color from a small palette, and a random lifetime from 0.7 to 1.5s passed to CSS as a --life custom property used as the animation duration. That per-sparkle variation makes the field feel organic instead of a repeating, uniform pattern.

No. Sparkles spawn on a 220ms interval, but each one removes itself with a setTimeout matched to its own lifetime plus a small buffer. So the DOM only ever contains the few sparkles currently visible, not the thousands that would accumulate if they were never cleaned up.

It checks prefers-reduced-motion with matchMedia and skips the spawning interval and the load burst entirely when the user has requested reduced motion, so the headline renders statically with its gradient and no twinkle. Honoring that media query is the accessible default for purely decorative motion.

Wrap the headline in a relatively-positioned container ref and spawn sparkles in a mount effect that sets up the interval and clears it on unmount. Append sparkle elements to the ref (or render a managed array in state and prune finished ones). Read prefers-reduced-motion before starting. The CSS keyframe ports directly; in Tailwind define skTwinkle in the config and drive the duration with an inline --life style.

Sparkles Text — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Sparkles Text</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#0a0312;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;text-align:center}
    
    .sk-stage{position:relative;padding:30px}
    .sk-text{position:relative;display:inline-block;font-size:clamp(34px,8vw,72px);font-weight:900;letter-spacing:-.03em;background:linear-gradient(120deg,#f0abfc,#818cf8,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent;z-index:1}
    .sk-sub{margin-top:18px;color:#9a8bb5;font-size:15px}
    
    .sk-spark{position:absolute;pointer-events:none;z-index:2;animation:skTwinkle var(--life) ease-in-out forwards}
    .sk-spark svg{display:block;width:100%;height:100%}
    @keyframes skTwinkle{
      0%{opacity:0;transform:scale(0) rotate(0deg)}
      35%{opacity:1;transform:scale(1) rotate(90deg)}
      100%{opacity:0;transform:scale(0) rotate(180deg)}
    }
  </style>
</head>
<body>
  <div class="sk-stage">
    <h1 class="sk-text" id="skText">Magic, on demand</h1>
    <p class="sk-sub">Sparkles spawn and twinkle around the headline, then fade.</p>
  </div>
  <script>
    var text = document.getElementById('skText');
    var stage = document.querySelector('.sk-stage');
    var COLORS = ['#f0abfc', '#818cf8', '#22d3ee', '#fde047', '#fff'];
    
    function spark() {
      var rect = text.getBoundingClientRect();
      var host = stage.getBoundingClientRect();
      // Spawn within the headline's bounding box, padded outward a little.
      var pad = 14;
      var x = rect.left - host.left - pad + Math.random() * (rect.width + pad * 2);
      var y = rect.top - host.top - pad + Math.random() * (rect.height + pad * 2);
      var size = 8 + Math.random() * 14;
      var color = COLORS[Math.floor(Math.random() * COLORS.length)];
      var life = (0.7 + Math.random() * 0.8).toFixed(2);
    
      var s = document.createElement('span');
      s.className = 'sk-spark';
      s.style.cssText = 'left:' + x + 'px;top:' + y + 'px;width:' + size + 'px;height:' + size + 'px;--life:' + life + 's';
      s.innerHTML = '<svg viewBox="0 0 24 24" fill="' + color + '"><path d="M12 0c.7 5.5 5.8 10.6 12 12-6.2 1.4-11.3 6.5-12 12-.7-5.5-5.8-10.6-12-12C6.2 10.6 11.3 5.5 12 0z"/></svg>';
      stage.appendChild(s);
      // Remove after its life so nodes never accumulate.
      setTimeout(function () { s.remove(); }, life * 1000 + 50);
    }
    
    // Respect users who prefer reduced motion.
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (!reduce) {
      setInterval(spark, 220);
      for (var k = 0; k < 6; k++) setTimeout(spark, k * 90);
    }
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Sparkles Text</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    @keyframes skTwinkle{
      0%{opacity:0;transform:scale(0) rotate(0deg)}
      35%{opacity:1;transform:scale(1) rotate(90deg)}
      100%{opacity:0;transform:scale(0) rotate(180deg)}
    }

    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#0a0312;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;text-align:center
    }

    .sk-spark {
      position:absolute;pointer-events:none;z-index:2;animation:skTwinkle var(--life) ease-in-out forwards
    }

    .sk-spark svg {
      display:block;width:100%;height:100%
    }
  </style>
</head>
<body>
  <div class="sk-stage relative p-[30px]">
    <h1 class="relative inline-block text-[clamp(34px,8vw,72px)] font-black tracking-[-.03em] [background:linear-gradient(120deg,#f0abfc,#818cf8,#22d3ee)] [-webkit-background-clip:text] bg-clip-text text-transparent z-[1]" id="skText">Magic, on demand</h1>
    <p class="mt-[18px] text-[#9a8bb5] text-[15px]">Sparkles spawn and twinkle around the headline, then fade.</p>
  </div>
  <script>
    var text = document.getElementById('skText');
    var stage = document.querySelector('.sk-stage');
    var COLORS = ['#f0abfc', '#818cf8', '#22d3ee', '#fde047', '#fff'];
    
    function spark() {
      var rect = text.getBoundingClientRect();
      var host = stage.getBoundingClientRect();
      // Spawn within the headline's bounding box, padded outward a little.
      var pad = 14;
      var x = rect.left - host.left - pad + Math.random() * (rect.width + pad * 2);
      var y = rect.top - host.top - pad + Math.random() * (rect.height + pad * 2);
      var size = 8 + Math.random() * 14;
      var color = COLORS[Math.floor(Math.random() * COLORS.length)];
      var life = (0.7 + Math.random() * 0.8).toFixed(2);
    
      var s = document.createElement('span');
      s.className = 'sk-spark';
      s.style.cssText = 'left:' + x + 'px;top:' + y + 'px;width:' + size + 'px;height:' + size + 'px;--life:' + life + 's';
      s.innerHTML = '<svg viewBox="0 0 24 24" fill="' + color + '"><path d="M12 0c.7 5.5 5.8 10.6 12 12-6.2 1.4-11.3 6.5-12 12-.7-5.5-5.8-10.6-12-12C6.2 10.6 11.3 5.5 12 0z"/></svg>';
      stage.appendChild(s);
      // Remove after its life so nodes never accumulate.
      setTimeout(function () { s.remove(); }, life * 1000 + 50);
    }
    
    // Respect users who prefer reduced motion.
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (!reduce) {
      setInterval(spark, 220);
      for (var k = 0; k < 6; k++) setTimeout(spark, k * 90);
    }
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to SparklesText.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0312;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;text-align:center}

.sk-stage{position:relative;padding:30px}
.sk-text{position:relative;display:inline-block;font-size:clamp(34px,8vw,72px);font-weight:900;letter-spacing:-.03em;background:linear-gradient(120deg,#f0abfc,#818cf8,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent;z-index:1}
.sk-sub{margin-top:18px;color:#9a8bb5;font-size:15px}

.sk-spark{position:absolute;pointer-events:none;z-index:2;animation:skTwinkle var(--life) ease-in-out forwards}
.sk-spark svg{display:block;width:100%;height:100%}
@keyframes skTwinkle{
  0%{opacity:0;transform:scale(0) rotate(0deg)}
  35%{opacity:1;transform:scale(1) rotate(90deg)}
  100%{opacity:0;transform:scale(0) rotate(180deg)}
}
`;

export default function SparklesText() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var text = document.getElementById('skText');
    var stage = document.querySelector('.sk-stage');
    var COLORS = ['#f0abfc', '#818cf8', '#22d3ee', '#fde047', '#fff'];
    
    function spark() {
      var rect = text.getBoundingClientRect();
      var host = stage.getBoundingClientRect();
      // Spawn within the headline's bounding box, padded outward a little.
      var pad = 14;
      var x = rect.left - host.left - pad + Math.random() * (rect.width + pad * 2);
      var y = rect.top - host.top - pad + Math.random() * (rect.height + pad * 2);
      var size = 8 + Math.random() * 14;
      var color = COLORS[Math.floor(Math.random() * COLORS.length)];
      var life = (0.7 + Math.random() * 0.8).toFixed(2);
    
      var s = document.createElement('span');
      s.className = 'sk-spark';
      s.style.cssText = 'left:' + x + 'px;top:' + y + 'px;width:' + size + 'px;height:' + size + 'px;--life:' + life + 's';
      s.innerHTML = '<svg viewBox="0 0 24 24" fill="' + color + '"><path d="M12 0c.7 5.5 5.8 10.6 12 12-6.2 1.4-11.3 6.5-12 12-.7-5.5-5.8-10.6-12-12C6.2 10.6 11.3 5.5 12 0z"/></svg>';
      stage.appendChild(s);
      // Remove after its life so nodes never accumulate.
      setTimeout(function () { s.remove(); }, life * 1000 + 50);
    }
    
    // Respect users who prefer reduced motion.
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (!reduce) {
      setInterval(spark, 220);
      for (var k = 0; k < 6; k++) setTimeout(spark, k * 90);
    }
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="sk-stage">
        <h1 className="sk-text" id="skText">Magic, on demand</h1>
        <p className="sk-sub">Sparkles spawn and twinkle around the headline, then fade.</p>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function SparklesText() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var text = document.getElementById('skText');
    var stage = document.querySelector('.sk-stage');
    var COLORS = ['#f0abfc', '#818cf8', '#22d3ee', '#fde047', '#fff'];
    
    function spark() {
      var rect = text.getBoundingClientRect();
      var host = stage.getBoundingClientRect();
      // Spawn within the headline's bounding box, padded outward a little.
      var pad = 14;
      var x = rect.left - host.left - pad + Math.random() * (rect.width + pad * 2);
      var y = rect.top - host.top - pad + Math.random() * (rect.height + pad * 2);
      var size = 8 + Math.random() * 14;
      var color = COLORS[Math.floor(Math.random() * COLORS.length)];
      var life = (0.7 + Math.random() * 0.8).toFixed(2);
    
      var s = document.createElement('span');
      s.className = 'sk-spark';
      s.style.cssText = 'left:' + x + 'px;top:' + y + 'px;width:' + size + 'px;height:' + size + 'px;--life:' + life + 's';
      s.innerHTML = '<svg viewBox="0 0 24 24" fill="' + color + '"><path d="M12 0c.7 5.5 5.8 10.6 12 12-6.2 1.4-11.3 6.5-12 12-.7-5.5-5.8-10.6-12-12C6.2 10.6 11.3 5.5 12 0z"/></svg>';
      stage.appendChild(s);
      // Remove after its life so nodes never accumulate.
      setTimeout(function () { s.remove(); }, life * 1000 + 50);
    }
    
    // Respect users who prefer reduced motion.
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (!reduce) {
      setInterval(spark, 220);
      for (var k = 0; k < 6; k++) setTimeout(spark, k * 90);
    }
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{`
@keyframes skTwinkle{
  0%{opacity:0;transform:scale(0) rotate(0deg)}
  35%{opacity:1;transform:scale(1) rotate(90deg)}
  100%{opacity:0;transform:scale(0) rotate(180deg)}
}

* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#0a0312;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;text-align:center
}

.sk-spark {
  position:absolute;pointer-events:none;z-index:2;animation:skTwinkle var(--life) ease-in-out forwards
}

.sk-spark svg {
  display:block;width:100%;height:100%
}
      `}</style>
      <div className="sk-stage relative p-[30px]">
        <h1 className="relative inline-block text-[clamp(34px,8vw,72px)] font-black tracking-[-.03em] [background:linear-gradient(120deg,#f0abfc,#818cf8,#22d3ee)] [-webkit-background-clip:text] bg-clip-text text-transparent z-[1]" id="skText">Magic, on demand</h1>
        <p className="mt-[18px] text-[#9a8bb5] text-[15px]">Sparkles spawn and twinkle around the headline, then fade.</p>
      </div>
    </>
  );
}
Vue
<template>
  <div class="sk-stage">
    <h1 class="sk-text" id="skText">Magic, on demand</h1>
    <p class="sk-sub">Sparkles spawn and twinkle around the headline, then fade.</p>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

let text;
let stage;
var COLORS = ['#f0abfc', '#818cf8', '#22d3ee', '#fde047', '#fff'];
function spark() {
  var rect = text.getBoundingClientRect();
  var host = stage.getBoundingClientRect();
  // Spawn within the headline's bounding box, padded outward a little.
  var pad = 14;
  var x = rect.left - host.left - pad + Math.random() * (rect.width + pad * 2);
  var y = rect.top - host.top - pad + Math.random() * (rect.height + pad * 2);
  var size = 8 + Math.random() * 14;
  var color = COLORS[Math.floor(Math.random() * COLORS.length)];
  var life = (0.7 + Math.random() * 0.8).toFixed(2);

  var s = document.createElement('span');
  s.className = 'sk-spark';
  s.style.cssText = 'left:' + x + 'px;top:' + y + 'px;width:' + size + 'px;height:' + size + 'px;--life:' + life + 's';
  s.innerHTML = '<svg viewBox="0 0 24 24" fill="' + color + '"><path d="M12 0c.7 5.5 5.8 10.6 12 12-6.2 1.4-11.3 6.5-12 12-.7-5.5-5.8-10.6-12-12C6.2 10.6 11.3 5.5 12 0z"/></svg>';
  stage.appendChild(s);
  // Remove after its life so nodes never accumulate.
  setTimeout(function () { s.remove(); }, life * 1000 + 50);
}
// Respect users who prefer reduced motion.
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

onMounted(() => {
  text = document.getElementById('skText');
  stage = document.querySelector('.sk-stage');
  if (!reduce) {
    setInterval(spark, 220);
    for (var k = 0; k < 6; k++) setTimeout(spark, k * 90);
  }
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0312;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;text-align:center}

.sk-stage{position:relative;padding:30px}
.sk-text{position:relative;display:inline-block;font-size:clamp(34px,8vw,72px);font-weight:900;letter-spacing:-.03em;background:linear-gradient(120deg,#f0abfc,#818cf8,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent;z-index:1}
.sk-sub{margin-top:18px;color:#9a8bb5;font-size:15px}

.sk-spark{position:absolute;pointer-events:none;z-index:2;animation:skTwinkle var(--life) ease-in-out forwards}
.sk-spark svg{display:block;width:100%;height:100%}
@keyframes skTwinkle{
  0%{opacity:0;transform:scale(0) rotate(0deg)}
  35%{opacity:1;transform:scale(1) rotate(90deg)}
  100%{opacity:0;transform:scale(0) rotate(180deg)}
}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-sparkles-text',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="sk-stage">
      <h1 class="sk-text" id="skText">Magic, on demand</h1>
      <p class="sk-sub">Sparkles spawn and twinkle around the headline, then fade.</p>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#0a0312;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;text-align:center}
    
    .sk-stage{position:relative;padding:30px}
    .sk-text{position:relative;display:inline-block;font-size:clamp(34px,8vw,72px);font-weight:900;letter-spacing:-.03em;background:linear-gradient(120deg,#f0abfc,#818cf8,#22d3ee);-webkit-background-clip:text;background-clip:text;color:transparent;z-index:1}
    .sk-sub{margin-top:18px;color:#9a8bb5;font-size:15px}
    
    .sk-spark{position:absolute;pointer-events:none;z-index:2;animation:skTwinkle var(--life) ease-in-out forwards}
    .sk-spark svg{display:block;width:100%;height:100%}
    @keyframes skTwinkle{
      0%{opacity:0;transform:scale(0) rotate(0deg)}
      35%{opacity:1;transform:scale(1) rotate(90deg)}
      100%{opacity:0;transform:scale(0) rotate(180deg)}
    }
  `]
})
export class SparklesTextComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var text = document.getElementById('skText');
    var stage = document.querySelector('.sk-stage');
    var COLORS = ['#f0abfc', '#818cf8', '#22d3ee', '#fde047', '#fff'];
    
    function spark() {
      var rect = text.getBoundingClientRect();
      var host = stage.getBoundingClientRect();
      // Spawn within the headline's bounding box, padded outward a little.
      var pad = 14;
      var x = rect.left - host.left - pad + Math.random() * (rect.width + pad * 2);
      var y = rect.top - host.top - pad + Math.random() * (rect.height + pad * 2);
      var size = 8 + Math.random() * 14;
      var color = COLORS[Math.floor(Math.random() * COLORS.length)];
      var life = (0.7 + Math.random() * 0.8).toFixed(2);
    
      var s = document.createElement('span');
      s.className = 'sk-spark';
      s.style.cssText = 'left:' + x + 'px;top:' + y + 'px;width:' + size + 'px;height:' + size + 'px;--life:' + life + 's';
      s.innerHTML = '<svg viewBox="0 0 24 24" fill="' + color + '"><path d="M12 0c.7 5.5 5.8 10.6 12 12-6.2 1.4-11.3 6.5-12 12-.7-5.5-5.8-10.6-12-12C6.2 10.6 11.3 5.5 12 0z"/></svg>';
      stage.appendChild(s);
      // Remove after its life so nodes never accumulate.
      setTimeout(function () { s.remove(); }, life * 1000 + 50);
    }
    
    // Respect users who prefer reduced motion.
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (!reduce) {
      setInterval(spark, 220);
      for (var k = 0; k < 6; k++) setTimeout(spark, k * 90);
    }

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { spark });
  }
}