More Cards Snippets
Meteor Card — Free HTML CSS JS Shooting Star Snippet
Meteor Card · Cards · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Meteor Card — Shooting Stars Streaking Across a Card

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
- 1Paste HTML, CSS, and JSA dark card renders with meteors streaking diagonally across it.
- 2Watch the showerThin shooting stars cross the card at scattered times and speeds.
- 3Note the clippingMeteors appear and vanish at the card's rounded edges.
- 4Scroll it off-screenThe shower pauses automatically to save cycles.
- 5Change the densityEdit COUNT for more or fewer meteors.
- 6Recolor the trailsAdjust the tail gradient and length to your theme.
Real-world uses
Common Use Cases
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
<!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><!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>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>
</>
);
}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>
</>
);
}<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>// @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);
}
}