Weather
import { useMemo, useState } from 'react';
const css = `
@keyframes card-weather-rain { from { transform: translateY(-30px) rotate(12deg); } to { transform: translateY(190px) rotate(12deg); } }
@keyframes card-weather-drift { from { transform: translateX(-12px); } to { transform: translateX(12px); } }
@keyframes card-weather-flash {
0%, 86%, 92%, 100% { opacity: 0; }
88%, 90% { opacity: 1; }
89% { opacity: 0.3; }
}
.card-weather {
position: relative;
width: 220px;
height: 170px;
padding: 14px;
border-radius: 18px;
background: linear-gradient(light-dark(#ebedf0, #232529), light-dark(#fbfcfd, #121417));
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
overflow: hidden;
cursor: pointer;
transition: background-color 0.6s ease;
}
.card-weather:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.card-weather__drop {
position: absolute;
top: 0;
width: 1px;
height: 14px;
background: linear-gradient(transparent, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 55%, transparent));
animation: card-weather-rain var(--t) linear var(--d) infinite;
}
.card-weather__drop[data-storm] { opacity: 0; transition: opacity 0.4s ease; }
.card-weather[data-storm] .card-weather__drop[data-storm] { opacity: 1; }
.card-weather__cloud {
position: absolute;
right: 18px;
top: 16px;
width: 64px;
height: 24px;
border-radius: 999px;
background: light-dark(#6d7179, #7d8087);
box-shadow: 14px -10px 0 2px light-dark(#6d7179, #7d8087), -10px -6px 0 -2px light-dark(#6d7179, #7d8087);
animation: card-weather-drift 4s ease-in-out infinite alternate;
transition: background-color 0.6s ease, box-shadow 0.6s ease;
}
.card-weather[data-storm] .card-weather__cloud { background: light-dark(#cbced3, #393c41); box-shadow: 14px -10px 0 2px light-dark(#cbced3, #393c41), -10px -6px 0 -2px light-dark(#cbced3, #393c41); }
.card-weather__bolt {
position: absolute;
right: 44px;
top: 40px;
width: 18px;
height: 34px;
background: light-dark(#373a40, #f1f2f3);
clip-path: polygon(55% 0, 10% 55%, 45% 55%, 30% 100%, 90% 40%, 55% 40%, 75% 0);
opacity: 0;
filter: drop-shadow(0 0 6px light-dark(#373a40, #f1f2f3));
}
.card-weather__flash { position: absolute; inset: 0; background: color-mix(in oklab, light-dark(#373a40, #f1f2f3) 16%, transparent); opacity: 0; pointer-events: none; }
.card-weather[data-storm] .card-weather__bolt,
.card-weather[data-storm] .card-weather__flash { animation: card-weather-flash 2.6s linear infinite; }
.card-weather__info { position: absolute; left: 14px; bottom: 12px; }
.card-weather__temp { color: light-dark(#373a40, #f1f2f3); font-size: 40px; font-weight: 300; line-height: 1; letter-spacing: -0.04em; }
.card-weather__cond { margin-top: 4px; color: light-dark(#42454c, #e5e6e9); font-size: 12px; font-weight: 600; }
.card-weather__city { color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
@media (prefers-reduced-motion: reduce) {
.card-weather__drop, .card-weather__cloud, .card-weather[data-storm] .card-weather__bolt, .card-weather[data-storm] .card-weather__flash { animation: none; }
}
`;
export function WeatherCard() {
const [storm, setStorm] = useState(false);
// Seeded drops; the second half only falls in the storm.
const drops = useMemo(() => {
let seed = 41;
const rand = () => ((seed = (seed * 16807) % 2147483647) / 2147483647);
return Array.from({ length: 44 }, (_, i) => ({
left: `${(rand() * 110 - 5).toFixed(1)}%`,
'--t': `${(0.55 + rand() * 0.35).toFixed(2)}s`,
'--d': `${(-rand() * 1).toFixed(2)}s`,
storm: i >= 18,
}));
}, []);
return (
<>
<style>
{css}
</style>
<div
className="card-weather"
data-storm={storm || undefined}
tabIndex={0}
role="group"
aria-label={`Lisbon, ${storm ? '14 degrees, thunderstorm' : '17 degrees, light rain'}`}
onPointerEnter={() => setStorm(true)}
onPointerLeave={() => setStorm(false)}
onFocus={() => setStorm(true)}
onBlur={() => setStorm(false)}
>
{drops.map(({ storm: s, ...style }, i) => (
<span key={i} className="card-weather__drop" data-storm={s || undefined} style={style as React.CSSProperties} aria-hidden="true" />
))}
<span className="card-weather__flash" aria-hidden="true" />
<span className="card-weather__cloud" aria-hidden="true" />
<span className="card-weather__bolt" aria-hidden="true" />
<div className="card-weather__info">
<div className="card-weather__temp">{storm ? '14°' : '17°'}</div>
<div className="card-weather__cond">{storm ? 'Thunderstorm' : 'Light rain'}</div>
<div className="card-weather__city">Lisbon · H 19° L 12°</div>
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardWeather(el) on the mounted element instead.
<div class="card-weather" tabindex="0" role="group" aria-label="Lisbon, 17 degrees, light rain">
<span class="card-weather__drop" style="left:-5.0%;--t:0.69s;--d:-0.98s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:83.5%;--t:0.85s;--d:-0.98s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:97.2%;--t:0.84s;--d:-0.85s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:30.5%;--t:0.80s;--d:-0.30s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:2.7%;--t:0.70s;--d:-0.19s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:73.9%;--t:0.73s;--d:-0.32s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:74.2%;--t:0.81s;--d:-0.12s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:12.3%;--t:0.60s;--d:-0.15s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:71.2%;--t:0.76s;--d:-0.77s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:84.2%;--t:0.57s;--d:-0.75s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:30.5%;--t:0.64s;--d:-0.76s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:99.1%;--t:0.61s;--d:-0.46s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:98.2%;--t:0.55s;--d:-0.63s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:102.7%;--t:0.60s;--d:-0.28s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:64.2%;--t:0.86s;--d:-0.71s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:102.8%;--t:0.86s;--d:-0.27s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:14.9%;--t:0.86s;--d:-0.43s" aria-hidden="true"></span>
<span class="card-weather__drop" style="left:59.6%;--t:0.81s;--d:-0.27s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:75.3%;--t:0.84s;--d:-0.95s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:83.4%;--t:0.65s;--d:-0.48s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:5.0%;--t:0.79s;--d:-0.17s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:3.8%;--t:0.71s;--d:-0.25s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:95.3%;--t:0.80s;--d:-0.86s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:-2.4%;--t:0.74s;--d:-0.75s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:34.6%;--t:0.71s;--d:-0.72s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:-0.3%;--t:0.75s;--d:-0.05s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:19.4%;--t:0.75s;--d:-0.94s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:10.4%;--t:0.78s;--d:-0.23s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:60.8%;--t:0.83s;--d:-0.19s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:76.6%;--t:0.89s;--d:-0.43s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:56.7%;--t:0.75s;--d:-0.39s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:-0.1%;--t:0.63s;--d:-0.69s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:93.5%;--t:0.73s;--d:-0.04s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:-1.6%;--t:0.56s;--d:-0.18s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:80.3%;--t:0.66s;--d:-0.30s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:43.2%;--t:0.86s;--d:-0.36s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:95.4%;--t:0.70s;--d:-0.17s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:69.8%;--t:0.81s;--d:-0.52s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:63.1%;--t:0.73s;--d:-0.55s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:74.5%;--t:0.66s;--d:-0.35s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:75.4%;--t:0.64s;--d:-0.12s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:103.5%;--t:0.59s;--d:-0.10s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:44.9%;--t:0.84s;--d:-0.42s" aria-hidden="true"></span>
<span class="card-weather__drop" data-storm="true" style="left:-2.9%;--t:0.68s;--d:-0.73s" aria-hidden="true"></span>
<span class="card-weather__flash" aria-hidden="true"></span>
<span class="card-weather__cloud" aria-hidden="true"></span>
<span class="card-weather__bolt" aria-hidden="true"></span>
<div class="card-weather__info">
<div class="card-weather__temp">17°</div>
<div class="card-weather__cond">Light rain</div>
<div class="card-weather__city">Lisbon · H 19° L 12°</div>
</div>
</div>@keyframes card-weather-rain { from { transform: translateY(-30px) rotate(12deg); } to { transform: translateY(190px) rotate(12deg); } }
@keyframes card-weather-drift { from { transform: translateX(-12px); } to { transform: translateX(12px); } }
@keyframes card-weather-flash {
0%, 86%, 92%, 100% { opacity: 0; }
88%, 90% { opacity: 1; }
89% { opacity: 0.3; }
}
.card-weather {
position: relative;
width: 220px;
height: 170px;
padding: 14px;
border-radius: 18px;
background: linear-gradient(light-dark(#ebedf0, #232529), light-dark(#fbfcfd, #121417));
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
overflow: hidden;
cursor: pointer;
transition: background-color 0.6s ease;
}
.card-weather:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.card-weather__drop {
position: absolute;
top: 0;
width: 1px;
height: 14px;
background: linear-gradient(transparent, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 55%, transparent));
animation: card-weather-rain var(--t) linear var(--d) infinite;
}
.card-weather__drop[data-storm] { opacity: 0; transition: opacity 0.4s ease; }
.card-weather[data-storm] .card-weather__drop[data-storm] { opacity: 1; }
.card-weather__cloud {
position: absolute;
right: 18px;
top: 16px;
width: 64px;
height: 24px;
border-radius: 999px;
background: light-dark(#6d7179, #7d8087);
box-shadow: 14px -10px 0 2px light-dark(#6d7179, #7d8087), -10px -6px 0 -2px light-dark(#6d7179, #7d8087);
animation: card-weather-drift 4s ease-in-out infinite alternate;
transition: background-color 0.6s ease, box-shadow 0.6s ease;
}
.card-weather[data-storm] .card-weather__cloud { background: light-dark(#cbced3, #393c41); box-shadow: 14px -10px 0 2px light-dark(#cbced3, #393c41), -10px -6px 0 -2px light-dark(#cbced3, #393c41); }
.card-weather__bolt {
position: absolute;
right: 44px;
top: 40px;
width: 18px;
height: 34px;
background: light-dark(#373a40, #f1f2f3);
clip-path: polygon(55% 0, 10% 55%, 45% 55%, 30% 100%, 90% 40%, 55% 40%, 75% 0);
opacity: 0;
filter: drop-shadow(0 0 6px light-dark(#373a40, #f1f2f3));
}
.card-weather__flash { position: absolute; inset: 0; background: color-mix(in oklab, light-dark(#373a40, #f1f2f3) 16%, transparent); opacity: 0; pointer-events: none; }
.card-weather[data-storm] .card-weather__bolt,
.card-weather[data-storm] .card-weather__flash { animation: card-weather-flash 2.6s linear infinite; }
.card-weather__info { position: absolute; left: 14px; bottom: 12px; }
.card-weather__temp { color: light-dark(#373a40, #f1f2f3); font-size: 40px; font-weight: 300; line-height: 1; letter-spacing: -0.04em; }
.card-weather__cond { margin-top: 4px; color: light-dark(#42454c, #e5e6e9); font-size: 12px; font-weight: 600; }
.card-weather__city { color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
@media (prefers-reduced-motion: reduce) {
.card-weather__drop, .card-weather__cloud, .card-weather[data-storm] .card-weather__bolt, .card-weather[data-storm] .card-weather__flash { animation: none; }
}function initCardWeather(root) {
var temp = root.querySelector('.card-weather__temp');
var cond = root.querySelector('.card-weather__cond');
if (!temp || !cond) return;
function set(storm) {
root.toggleAttribute('data-storm', storm);
temp.textContent = storm ? '14°' : '17°';
cond.textContent = storm ? 'Thunderstorm' : 'Light rain';
root.setAttribute('aria-label', 'Lisbon, ' + (storm ? '14 degrees, thunderstorm' : '17 degrees, light rain'));
}
function on() { set(true); }
function off() { set(false); }
root.addEventListener('pointerenter', on);
root.addEventListener('pointerleave', off);
root.addEventListener('focus', on);
root.addEventListener('blur', off);
return function () {
root.removeEventListener('pointerenter', on);
root.removeEventListener('pointerleave', off);
root.removeEventListener('focus', on);
root.removeEventListener('blur', off);
};
}
document.querySelectorAll('.card-weather').forEach(initCardWeather);Goal: Create a "Weather" card.
Visual behavior: A weather card with light rain falling behind the reading. Hover brings the storm in: the rain thickens, the cloud darkens, lightning flashes with the sky, and the reading changes to match.
Technique:
- Forty-four thin gradient raindrops with seeded positions, speeds and negative delays fall on a slanted keyframe; the second half are hidden until the storm.
- A data-storm attribute set on hover reveals the extra drops, darkens the cloud (one pill plus two box-shadow puffs drifting side to side) and starts the flash.
- The bolt is a clip-path polygon and the sky flash an overlay; both share one keyframe with a double flicker near the end of each 2.6s cycle.
- The temperature and condition text change with the state.
Accessibility: The card is focusable (tabIndex 0) with role="group" and an aria-label that sums it up; keyboard focus plays the same reveal as hover. The aria-label follows the conditions. Under prefers-reduced-motion nothing falls or flashes.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Weather" card.
Visual behavior: A weather card with light rain falling behind the reading. Hover brings the storm in: the rain thickens, the cloud darkens, lightning flashes with the sky, and the reading changes to match.
Technique:
- Forty-four thin gradient raindrops with seeded positions, speeds and negative delays fall on a slanted keyframe; the second half are hidden until the storm.
- A data-storm attribute set on hover reveals the extra drops, darkens the cloud (one pill plus two box-shadow puffs drifting side to side) and starts the flash.
- The bolt is a clip-path polygon and the sky flash an overlay; both share one keyframe with a double flicker near the end of each 2.6s cycle.
- The temperature and condition text change with the state.
Accessibility: The card is focusable (tabIndex 0) with role="group" and an aria-label that sums it up; keyboard focus plays the same reveal as hover. The aria-label follows the conditions. Under prefers-reduced-motion nothing falls or flashes.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A weather card with light rain falling behind the reading. Hover brings the storm in: the rain thickens, the cloud darkens, lightning flashes with the sky, and the reading changes to match.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- hover, continuous, ambient, card
- Techniques
- keyframes, css variables, clip path
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The card is focusable (tabIndex 0) with role="group" and an aria-label that sums it up; keyboard focus plays the same reveal as hover. The aria-label follows the conditions. Under prefers-reduced-motion nothing falls or flashes.