Meteor Card — Free HTML CSS JS Shooting Star Snippet

Meteor Card · Cards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Dot-plus-tail meteors
A head and a gradient ::before trail, no images.
Diagonal fall keyframe
Meteors travel and fade across the card.
Randomized timing
Per-meteor start, delay, and duration.
Clipped to the card
overflow hidden frames the streaks.
Off-screen pause
IntersectionObserver stops the shower when hidden.
Content stays readable
Meteors sit behind the body layer.
Cosmic styling
Radial background and indigo glow shadow.
Tunable density
One COUNT constant controls the shower.

About this UI Snippet

Meteor Card — Shooting Stars Streaking Across a Card

Screenshot of the Meteor Card snippet rendered live

The meteor card is the atmospheric effect where thin shooting stars streak diagonally across a dark card, each with a glowing trailing tail — turning a plain pricing or feature card into a tiny night sky. This snippet builds it with plain HTML, CSS, and a small vanilla JavaScript generator, with randomized timing so the shower looks natural and an IntersectionObserver to pause it when off-screen.

A meteor is a dot with a tail

Each meteor is a tiny 2px dot, and its glowing trail is a ::before pseudo-element: a 60px-wide horizontal gradient that fades from light indigo to transparent, attached to the dot's trailing side. The whole thing is rotated -45° so it travels and points diagonally. This two-part construction — a head plus a gradient tail — is what reads as a shooting star rather than a moving dot, and it's pure CSS with no images.

The falling animation

The mcFall keyframe translates each meteor from the top toward the bottom-left by translate(-340px, 340px) while keeping the -45° rotation, and fades it out near the end of its travel. Because the card has overflow: hidden, meteors are clipped to the card's rounded bounds, so they appear and disappear at the edges like real streaks crossing a window. The animation is linear infinite, so each meteor loops forever.

Randomization is the whole trick

If every meteor shared the same start position, delay, and speed, you'd see an obvious grid of synchronized lines. The JavaScript generates 14 meteors, each with a random horizontal start (some beginning beyond the right edge), a random animation-delay up to 6 seconds, and a random animation-duration between 3 and 7 seconds. Those three randomized values mean meteors enter at different times, places, and speeds, so the shower looks scattered and organic — the single most important detail for selling the effect.

Pausing off-screen

A continuously animating shower wastes CPU and battery when nobody's looking at it, so an IntersectionObserver watches the card and toggles each meteor's animationPlayState between running and paused as the card enters and leaves the viewport. With threshold: 0 it switches the moment any part of the card is visible. This is a lightweight, scroll-listener-free way to keep decorative animations efficient.

Layering over content

The meteors live in an absolutely positioned layer behind the card body (which sits at a higher z-index), so the streaks pass behind the icon, heading, and button without obscuring them. The card's radial-gradient background and indigo glow shadow complete the cosmic look, and the content remains fully readable.

Customizing it

Change COUNT for a denser or sparser shower, widen the random duration range for more speed variety, lengthen the tail by editing the ::before width, or recolor the trail gradient to match your theme. Adjust the travel distance in mcFall if you resize the card. Pair it with a pin card or a focus cards grid for a striking set of cards, or use it as a premium pricing card tier.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA dark card renders with meteors streaking diagonally across it.
  2. 2
    Watch the showerThin shooting stars cross the card at scattered times and speeds.
  3. 3
    Note the clippingMeteors appear and vanish at the card's rounded edges.
  4. 4
    Scroll it off-screenThe shower pauses automatically to save cycles.
  5. 5
    Change the densityEdit COUNT for more or fewer meteors.
  6. 6
    Recolor the trailsAdjust the tail gradient and length to your theme.

Real-world uses

Common Use Cases

Premium pricing tiers
A flashy upgrade to a pricing card.
Feature highlights
Pair with a pin card in a grid.
Launch announcements
Set a cosmic mood near a lamp header.
Reward screens
Echo the sky with a confetti button.
Space-themed sites
Combine with a starfield background.
CSS effect demos
A reference for randomized meteor showers.

Got questions?

Frequently Asked Questions

Each meteor is a tiny 2px dot with a ::before pseudo-element that is a 60px horizontal gradient fading to transparent — the glowing tail. The whole element is rotated -45 degrees so it travels and points diagonally. The head-plus-tail construction is what makes it read as a shooting star, and it's entirely CSS with no image assets.

The JavaScript gives each of the 14 meteors a random horizontal start, a random animation-delay up to 6 seconds, and a random duration between 3 and 7 seconds. Because they enter at different times, positions, and speeds, the shower looks scattered and organic rather than a grid of identical lines firing together.

An IntersectionObserver with threshold 0 watches the card and toggles each meteor's animationPlayState between running and paused as the card enters or leaves the viewport. That stops the continuous animation work whenever the card isn't visible, without needing a scroll listener.

The meteors live in an absolutely positioned layer, and the card body sits above them at a higher z-index. So the streaks pass behind the icon, heading, and button. The card's overflow: hidden also clips them to its rounded bounds so they enter and exit cleanly at the edges.

Generate the meteor elements from an array in render, applying the random left, delay, and duration as inline styles computed once. Keep the IntersectionObserver in a mount effect with cleanup on unmount, using a ref to the card. The CSS keyframe and tail port directly; in Tailwind, define mcFall in the config and render the meteors with arbitrary inline animation styles.

Meteor Card — 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>Meteor Card</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#05050d;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
    
    .mc-stage{padding:30px}
    .mc-card{position:relative;width:300px;border-radius:20px;padding:26px;background:radial-gradient(circle at 50% 0,#1a1a33,#0a0a18);border:1px solid #23233f;overflow:hidden;box-shadow:0 30px 70px -30px rgba(99,102,241,.5)}
    
    .mc-meteors{position:absolute;inset:0;pointer-events:none}
    .mc-meteor{position:absolute;top:-10%;width:2px;height:2px;border-radius:50%;background:#fff;box-shadow:0 0 0 1px rgba(255,255,255,.1)}
    .mc-meteor::before{content:'';position:absolute;top:50%;right:1px;width:60px;height:1px;background:linear-gradient(90deg,#a5b4fc,transparent);transform:translateY(-50%)}
    .mc-meteor{animation:mcFall linear infinite}
    @keyframes mcFall{
      from{transform:translate(0,0) rotate(-45deg);opacity:1}
      70%{opacity:1}
      to{transform:translate(-340px,340px) rotate(-45deg);opacity:0}
    }
    
    .mc-body{position:relative;z-index:1}
    .mc-icon{width:46px;height:46px;border-radius:12px;background:rgba(99,102,241,.18);border:1px solid rgba(99,102,241,.4);display:flex;align-items:center;justify-content:center;font-size:22px;color:#c7d2fe;margin-bottom:16px}
    .mc-card h3{font-size:21px;font-weight:800;margin-bottom:9px}
    .mc-card p{font-size:13.5px;color:#a3a3c2;line-height:1.6;margin-bottom:20px}
    .mc-btn{width:100%;background:#6366f1;color:#fff;border:none;border-radius:11px;padding:12px;font-family:inherit;font-size:14px;font-weight:700;cursor:pointer;transition:background .2s,transform .15s}
    .mc-btn:hover{background:#4f46e5;transform:translateY(-1px)}
  </style>
</head>
<body>
  <div class="mc-stage">
    <article class="mc-card" id="mcCard">
      <div class="mc-meteors" id="mcMeteors" aria-hidden="true"></div>
      <div class="mc-body">
        <div class="mc-icon">✦</div>
        <h3>Cosmic plan</h3>
        <p>Unlimited projects, priority support, and a sky full of shooting stars streaking across the card.</p>
        <button type="button" class="mc-btn">Launch</button>
      </div>
    </article>
  </div>
  <script>
    var host = document.getElementById('mcMeteors');
    var COUNT = 14;
    
    // Generate meteors with staggered start times and varied speed/position so the
    // shower never looks like a synchronized grid of identical streaks.
    for (var i = 0; i < COUNT; i++) {
      var m = document.createElement('span');
      m.className = 'mc-meteor';
      var left = Math.random() * 130;          // start beyond the right edge sometimes
      var delay = (Math.random() * 6).toFixed(2);
      var dur = (3 + Math.random() * 4).toFixed(2);
      m.style.cssText = 'left:' + left + '%;animation-delay:' + delay +
        's;animation-duration:' + dur + 's';
      host.appendChild(m);
    }
    
    // Pause the shower when the card scrolls out of view to save cycles.
    var card = document.getElementById('mcCard');
    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (e) {
        host.style.animationPlayState = '';
        host.querySelectorAll('.mc-meteor').forEach(function (m) {
          m.style.animationPlayState = e.isIntersecting ? 'running' : 'paused';
        });
      });
    }, { threshold: 0 });
    io.observe(card);
  </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>Meteor Card</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    @keyframes mcFall{
      from{transform:translate(0,0) rotate(-45deg);opacity:1}
      70%{opacity:1}
      to{transform:translate(-340px,340px) rotate(-45deg);opacity:0}
    }

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

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#05050d;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
    }

    .mc-meteor {
      position:absolute;top:-10%;width:2px;height:2px;border-radius:50%;background:#fff;box-shadow:0 0 0 1px rgba(255,255,255,.1)
    }

    .mc-meteor::before {
      content:'';position:absolute;top:50%;right:1px;width:60px;height:1px;background:linear-gradient(90deg,#a5b4fc,transparent);transform:translateY(-50%)
    }

    .mc-meteor {
      animation:mcFall linear infinite
    }

    .mc-card h3 {
      font-size:21px;font-weight:800;margin-bottom:9px
    }

    .mc-card p {
      font-size:13.5px;color:#a3a3c2;line-height:1.6;margin-bottom:20px
    }
  </style>
</head>
<body>
  <div class="p-[30px]">
    <article class="mc-card relative w-[300px] rounded-[20px] p-[26px] [background:radial-gradient(circle_at_50%_0,#1a1a33,#0a0a18)] border border-[#23233f] overflow-hidden shadow-[0_30px_70px_-30px_rgba(99,102,241,.5)]" id="mcCard">
      <div class="absolute inset-0 pointer-events-none" id="mcMeteors" aria-hidden="true"></div>
      <div class="relative z-[1]">
        <div class="w-[46px] h-[46px] rounded-xl bg-[rgba(99,102,241,.18)] border border-[rgba(99,102,241,.4)] flex items-center justify-center text-[22px] text-[#c7d2fe] mb-4">✦</div>
        <h3>Cosmic plan</h3>
        <p>Unlimited projects, priority support, and a sky full of shooting stars streaking across the card.</p>
        <button type="button" class="w-full bg-[#6366f1] text-[#fff] border-0 rounded-[11px] p-3 font-[inherit] text-sm font-bold cursor-pointer [transition:background_.2s,transform_.15s] hover:bg-[#4f46e5] hover:[transform:translateY(-1px)]">Launch</button>
      </div>
    </article>
  </div>
  <script>
    var host = document.getElementById('mcMeteors');
    var COUNT = 14;
    
    // Generate meteors with staggered start times and varied speed/position so the
    // shower never looks like a synchronized grid of identical streaks.
    for (var i = 0; i < COUNT; i++) {
      var m = document.createElement('span');
      m.className = 'mc-meteor';
      var left = Math.random() * 130;          // start beyond the right edge sometimes
      var delay = (Math.random() * 6).toFixed(2);
      var dur = (3 + Math.random() * 4).toFixed(2);
      m.style.cssText = 'left:' + left + '%;animation-delay:' + delay +
        's;animation-duration:' + dur + 's';
      host.appendChild(m);
    }
    
    // Pause the shower when the card scrolls out of view to save cycles.
    var card = document.getElementById('mcCard');
    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (e) {
        host.style.animationPlayState = '';
        host.querySelectorAll('.mc-meteor').forEach(function (m) {
          m.style.animationPlayState = e.isIntersecting ? 'running' : 'paused';
        });
      });
    }, { threshold: 0 });
    io.observe(card);
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

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

.mc-stage{padding:30px}
.mc-card{position:relative;width:300px;border-radius:20px;padding:26px;background:radial-gradient(circle at 50% 0,#1a1a33,#0a0a18);border:1px solid #23233f;overflow:hidden;box-shadow:0 30px 70px -30px rgba(99,102,241,.5)}

.mc-meteors{position:absolute;inset:0;pointer-events:none}
.mc-meteor{position:absolute;top:-10%;width:2px;height:2px;border-radius:50%;background:#fff;box-shadow:0 0 0 1px rgba(255,255,255,.1)}
.mc-meteor::before{content:'';position:absolute;top:50%;right:1px;width:60px;height:1px;background:linear-gradient(90deg,#a5b4fc,transparent);transform:translateY(-50%)}
.mc-meteor{animation:mcFall linear infinite}
@keyframes mcFall{
  from{transform:translate(0,0) rotate(-45deg);opacity:1}
  70%{opacity:1}
  to{transform:translate(-340px,340px) rotate(-45deg);opacity:0}
}

.mc-body{position:relative;z-index:1}
.mc-icon{width:46px;height:46px;border-radius:12px;background:rgba(99,102,241,.18);border:1px solid rgba(99,102,241,.4);display:flex;align-items:center;justify-content:center;font-size:22px;color:#c7d2fe;margin-bottom:16px}
.mc-card h3{font-size:21px;font-weight:800;margin-bottom:9px}
.mc-card p{font-size:13.5px;color:#a3a3c2;line-height:1.6;margin-bottom:20px}
.mc-btn{width:100%;background:#6366f1;color:#fff;border:none;border-radius:11px;padding:12px;font-family:inherit;font-size:14px;font-weight:700;cursor:pointer;transition:background .2s,transform .15s}
.mc-btn:hover{background:#4f46e5;transform:translateY(-1px)}
`;

export default function MeteorCard() {
  // 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 host = document.getElementById('mcMeteors');
    var COUNT = 14;
    
    // Generate meteors with staggered start times and varied speed/position so the
    // shower never looks like a synchronized grid of identical streaks.
    for (var i = 0; i < COUNT; i++) {
      var m = document.createElement('span');
      m.className = 'mc-meteor';
      var left = Math.random() * 130;          // start beyond the right edge sometimes
      var delay = (Math.random() * 6).toFixed(2);
      var dur = (3 + Math.random() * 4).toFixed(2);
      m.style.cssText = 'left:' + left + '%;animation-delay:' + delay +
        's;animation-duration:' + dur + 's';
      host.appendChild(m);
    }
    
    // Pause the shower when the card scrolls out of view to save cycles.
    var card = document.getElementById('mcCard');
    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (e) {
        host.style.animationPlayState = '';
        host.querySelectorAll('.mc-meteor').forEach(function (m) {
          m.style.animationPlayState = e.isIntersecting ? 'running' : 'paused';
        });
      });
    }, { threshold: 0 });
    io.observe(card);
    // 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="mc-stage">
        <article className="mc-card" id="mcCard">
          <div className="mc-meteors" id="mcMeteors" aria-hidden="true"></div>
          <div className="mc-body">
            <div className="mc-icon">✦</div>
            <h3>Cosmic plan</h3>
            <p>Unlimited projects, priority support, and a sky full of shooting stars streaking across the card.</p>
            <button type="button" className="mc-btn">Launch</button>
          </div>
        </article>
      </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 MeteorCard() {
  // 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 host = document.getElementById('mcMeteors');
    var COUNT = 14;
    
    // Generate meteors with staggered start times and varied speed/position so the
    // shower never looks like a synchronized grid of identical streaks.
    for (var i = 0; i < COUNT; i++) {
      var m = document.createElement('span');
      m.className = 'mc-meteor';
      var left = Math.random() * 130;          // start beyond the right edge sometimes
      var delay = (Math.random() * 6).toFixed(2);
      var dur = (3 + Math.random() * 4).toFixed(2);
      m.style.cssText = 'left:' + left + '%;animation-delay:' + delay +
        's;animation-duration:' + dur + 's';
      host.appendChild(m);
    }
    
    // Pause the shower when the card scrolls out of view to save cycles.
    var card = document.getElementById('mcCard');
    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (e) {
        host.style.animationPlayState = '';
        host.querySelectorAll('.mc-meteor').forEach(function (m) {
          m.style.animationPlayState = e.isIntersecting ? 'running' : 'paused';
        });
      });
    }, { threshold: 0 });
    io.observe(card);
    // 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 mcFall{
  from{transform:translate(0,0) rotate(-45deg);opacity:1}
  70%{opacity:1}
  to{transform:translate(-340px,340px) rotate(-45deg);opacity:0}
}

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

body {
  font-family:system-ui,-apple-system,sans-serif;background:#05050d;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
}

.mc-meteor {
  position:absolute;top:-10%;width:2px;height:2px;border-radius:50%;background:#fff;box-shadow:0 0 0 1px rgba(255,255,255,.1)
}

.mc-meteor::before {
  content:'';position:absolute;top:50%;right:1px;width:60px;height:1px;background:linear-gradient(90deg,#a5b4fc,transparent);transform:translateY(-50%)
}

.mc-meteor {
  animation:mcFall linear infinite
}

.mc-card h3 {
  font-size:21px;font-weight:800;margin-bottom:9px
}

.mc-card p {
  font-size:13.5px;color:#a3a3c2;line-height:1.6;margin-bottom:20px
}
      `}</style>
      <div className="p-[30px]">
        <article className="mc-card relative w-[300px] rounded-[20px] p-[26px] [background:radial-gradient(circle_at_50%_0,#1a1a33,#0a0a18)] border border-[#23233f] overflow-hidden shadow-[0_30px_70px_-30px_rgba(99,102,241,.5)]" id="mcCard">
          <div className="absolute inset-0 pointer-events-none" id="mcMeteors" aria-hidden="true"></div>
          <div className="relative z-[1]">
            <div className="w-[46px] h-[46px] rounded-xl bg-[rgba(99,102,241,.18)] border border-[rgba(99,102,241,.4)] flex items-center justify-center text-[22px] text-[#c7d2fe] mb-4">✦</div>
            <h3>Cosmic plan</h3>
            <p>Unlimited projects, priority support, and a sky full of shooting stars streaking across the card.</p>
            <button type="button" className="w-full bg-[#6366f1] text-[#fff] border-0 rounded-[11px] p-3 font-[inherit] text-sm font-bold cursor-pointer [transition:background_.2s,transform_.15s] hover:bg-[#4f46e5] hover:[transform:translateY(-1px)]">Launch</button>
          </div>
        </article>
      </div>
    </>
  );
}
Vue
<template>
  <div class="mc-stage">
    <article class="mc-card" id="mcCard">
      <div class="mc-meteors" id="mcMeteors" aria-hidden="true"></div>
      <div class="mc-body">
        <div class="mc-icon">✦</div>
        <h3>Cosmic plan</h3>
        <p>Unlimited projects, priority support, and a sky full of shooting stars streaking across the card.</p>
        <button type="button" class="mc-btn">Launch</button>
      </div>
    </article>
  </div>
</template>

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

let host;
var COUNT = 14;
let io;

onMounted(() => {
  host = document.getElementById('mcMeteors');
  // Generate meteors with staggered start times and varied speed/position so the
  // shower never looks like a synchronized grid of identical streaks.
  for (var i = 0; i < COUNT; i++) {
    var m = document.createElement('span');
    m.className = 'mc-meteor';
    var left = Math.random() * 130;          // start beyond the right edge sometimes
    var delay = (Math.random() * 6).toFixed(2);
    var dur = (3 + Math.random() * 4).toFixed(2);
    m.style.cssText = 'left:' + left + '%;animation-delay:' + delay +
      's;animation-duration:' + dur + 's';
    host.appendChild(m);
  }
  
  // Pause the shower when the card scrolls out of view to save cycles.
  var card = document.getElementById('mcCard');
  io = new IntersectionObserver(function (entries) {
    entries.forEach(function (e) {
      host.style.animationPlayState = '';
      host.querySelectorAll('.mc-meteor').forEach(function (m) {
        m.style.animationPlayState = e.isIntersecting ? 'running' : 'paused';
      });
    });
  }, { threshold: 0 });
  io.observe(card);
});
</script>

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

.mc-stage{padding:30px}
.mc-card{position:relative;width:300px;border-radius:20px;padding:26px;background:radial-gradient(circle at 50% 0,#1a1a33,#0a0a18);border:1px solid #23233f;overflow:hidden;box-shadow:0 30px 70px -30px rgba(99,102,241,.5)}

.mc-meteors{position:absolute;inset:0;pointer-events:none}
.mc-meteor{position:absolute;top:-10%;width:2px;height:2px;border-radius:50%;background:#fff;box-shadow:0 0 0 1px rgba(255,255,255,.1)}
.mc-meteor::before{content:'';position:absolute;top:50%;right:1px;width:60px;height:1px;background:linear-gradient(90deg,#a5b4fc,transparent);transform:translateY(-50%)}
.mc-meteor{animation:mcFall linear infinite}
@keyframes mcFall{
  from{transform:translate(0,0) rotate(-45deg);opacity:1}
  70%{opacity:1}
  to{transform:translate(-340px,340px) rotate(-45deg);opacity:0}
}

.mc-body{position:relative;z-index:1}
.mc-icon{width:46px;height:46px;border-radius:12px;background:rgba(99,102,241,.18);border:1px solid rgba(99,102,241,.4);display:flex;align-items:center;justify-content:center;font-size:22px;color:#c7d2fe;margin-bottom:16px}
.mc-card h3{font-size:21px;font-weight:800;margin-bottom:9px}
.mc-card p{font-size:13.5px;color:#a3a3c2;line-height:1.6;margin-bottom:20px}
.mc-btn{width:100%;background:#6366f1;color:#fff;border:none;border-radius:11px;padding:12px;font-family:inherit;font-size:14px;font-weight:700;cursor:pointer;transition:background .2s,transform .15s}
.mc-btn:hover{background:#4f46e5;transform:translateY(-1px)}
</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-meteor-card',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="mc-stage">
      <article class="mc-card" id="mcCard">
        <div class="mc-meteors" id="mcMeteors" aria-hidden="true"></div>
        <div class="mc-body">
          <div class="mc-icon">✦</div>
          <h3>Cosmic plan</h3>
          <p>Unlimited projects, priority support, and a sky full of shooting stars streaking across the card.</p>
          <button type="button" class="mc-btn">Launch</button>
        </div>
      </article>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#05050d;color:#fff;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
    
    .mc-stage{padding:30px}
    .mc-card{position:relative;width:300px;border-radius:20px;padding:26px;background:radial-gradient(circle at 50% 0,#1a1a33,#0a0a18);border:1px solid #23233f;overflow:hidden;box-shadow:0 30px 70px -30px rgba(99,102,241,.5)}
    
    .mc-meteors{position:absolute;inset:0;pointer-events:none}
    .mc-meteor{position:absolute;top:-10%;width:2px;height:2px;border-radius:50%;background:#fff;box-shadow:0 0 0 1px rgba(255,255,255,.1)}
    .mc-meteor::before{content:'';position:absolute;top:50%;right:1px;width:60px;height:1px;background:linear-gradient(90deg,#a5b4fc,transparent);transform:translateY(-50%)}
    .mc-meteor{animation:mcFall linear infinite}
    @keyframes mcFall{
      from{transform:translate(0,0) rotate(-45deg);opacity:1}
      70%{opacity:1}
      to{transform:translate(-340px,340px) rotate(-45deg);opacity:0}
    }
    
    .mc-body{position:relative;z-index:1}
    .mc-icon{width:46px;height:46px;border-radius:12px;background:rgba(99,102,241,.18);border:1px solid rgba(99,102,241,.4);display:flex;align-items:center;justify-content:center;font-size:22px;color:#c7d2fe;margin-bottom:16px}
    .mc-card h3{font-size:21px;font-weight:800;margin-bottom:9px}
    .mc-card p{font-size:13.5px;color:#a3a3c2;line-height:1.6;margin-bottom:20px}
    .mc-btn{width:100%;background:#6366f1;color:#fff;border:none;border-radius:11px;padding:12px;font-family:inherit;font-size:14px;font-weight:700;cursor:pointer;transition:background .2s,transform .15s}
    .mc-btn:hover{background:#4f46e5;transform:translateY(-1px)}
  `]
})
export class MeteorCardComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var host = document.getElementById('mcMeteors');
    var COUNT = 14;
    
    // Generate meteors with staggered start times and varied speed/position so the
    // shower never looks like a synchronized grid of identical streaks.
    for (var i = 0; i < COUNT; i++) {
      var m = document.createElement('span');
      m.className = 'mc-meteor';
      var left = Math.random() * 130;          // start beyond the right edge sometimes
      var delay = (Math.random() * 6).toFixed(2);
      var dur = (3 + Math.random() * 4).toFixed(2);
      m.style.cssText = 'left:' + left + '%;animation-delay:' + delay +
        's;animation-duration:' + dur + 's';
      host.appendChild(m);
    }
    
    // Pause the shower when the card scrolls out of view to save cycles.
    var card = document.getElementById('mcCard');
    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (e) {
        host.style.animationPlayState = '';
        host.querySelectorAll('.mc-meteor').forEach(function (m) {
          m.style.animationPlayState = e.isIntersecting ? 'running' : 'paused';
        });
      });
    }, { threshold: 0 });
    io.observe(card);
  }
}