Weather Card
Original · freeA weather widget card with current conditions and a mini forecast.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/card-weather.json"use client";
import { useId, useMemo, useState, type ReactNode } from "react";
type Condition = "sunny" | "partly" | "cloudy" | "rain" | "snow" | "storm";
interface DayForecast {
key: string;
label: string;
condition: Condition;
high: number;
low: number;
feels: number;
humidity: number;
wind: number;
precip: number;
desc: string;
}
interface CityWeather {
city: string;
region: string;
localTime: string;
days: DayForecast[];
}
const SAN_FRANCISCO: CityWeather = {
city: "San Francisco",
region: "California",
localTime: "9:42 AM",
days: [
{ key: "now", label: "Today", condition: "partly", high: 19, low: 12, feels: 16, humidity: 72, wind: 18, precip: 10, desc: "Partly cloudy" },
{ key: "tue", label: "Tue", condition: "sunny", high: 22, low: 13, feels: 21, humidity: 58, wind: 12, precip: 0, desc: "Clear skies" },
{ key: "wed", label: "Wed", condition: "cloudy", high: 18, low: 12, feels: 17, humidity: 76, wind: 15, precip: 20, desc: "Overcast" },
{ key: "thu", label: "Thu", condition: "rain", high: 15, low: 11, feels: 13, humidity: 88, wind: 26, precip: 80, desc: "Steady rain" },
{ key: "fri", label: "Fri", condition: "storm", high: 14, low: 10, feels: 11, humidity: 91, wind: 34, precip: 90, desc: "Thunderstorms" },
{ key: "sat", label: "Sat", condition: "partly", high: 20, low: 12, feels: 19, humidity: 64, wind: 14, precip: 15, desc: "Some clouds" },
],
};
const TOKYO: CityWeather = {
city: "Tokyo",
region: "Japan",
localTime: "1:42 AM",
days: [
{ key: "now", label: "Now", condition: "rain", high: 24, low: 20, feels: 26, humidity: 84, wind: 22, precip: 70, desc: "Light rain" },
{ key: "d1", label: "Wed", condition: "cloudy", high: 26, low: 21, feels: 27, humidity: 70, wind: 16, precip: 30, desc: "Cloudy" },
{ key: "d2", label: "Thu", condition: "sunny", high: 29, low: 22, feels: 31, humidity: 55, wind: 10, precip: 0, desc: "Sunny" },
{ key: "d3", label: "Fri", condition: "partly", high: 28, low: 22, feels: 30, humidity: 60, wind: 12, precip: 10, desc: "Partly cloudy" },
],
};
const REYKJAVIK: CityWeather = {
city: "Reykjavík",
region: "Iceland",
localTime: "4:42 PM",
days: [
{ key: "now", label: "Now", condition: "snow", high: 1, low: -4, feels: -8, humidity: 81, wind: 38, precip: 65, desc: "Snow showers" },
{ key: "d1", label: "Wed", condition: "cloudy", high: 2, low: -3, feels: -5, humidity: 78, wind: 30, precip: 25, desc: "Grey skies" },
{ key: "d2", label: "Thu", condition: "partly", high: 3, low: -2, feels: -1, humidity: 68, wind: 22, precip: 10, desc: "Breaking cloud" },
{ key: "d3", label: "Fri", condition: "snow", high: 0, low: -6, feels: -10, humidity: 85, wind: 41, precip: 70, desc: "Heavy snow" },
],
};
const CONDITION_LABEL: Record<Condition, string> = {
sunny: "Sunny",
partly: "Partly cloudy",
cloudy: "Cloudy",
rain: "Rain",
snow: "Snow",
storm: "Storm",
};
function toDisplay(celsius: number, unit: "c" | "f"): number {
return unit === "c" ? Math.round(celsius) : Math.round((celsius * 9) / 5 + 32);
}
function WeatherGlyph({ condition, size = 24, animate }: { condition: Condition; size?: number; animate: boolean }) {
const a = (cls: string) => (animate ? cls : "");
const common = { width: size, height: size, viewBox: "0 0 48 48", fill: "none", "aria-hidden": true } as const;
if (condition === "sunny") {
return (
<svg {...common}>
<g className={a("cw-spin")} style={{ transformOrigin: "24px 24px" }}>
{[...Array(8)].map((_, i) => (
<line
key={i}
x1={24}
y1={5}
x2={24}
y2={11}
stroke="currentColor"
strokeWidth={2.6}
strokeLinecap="round"
transform={`rotate(${i * 45} 24 24)`}
/>
))}
</g>
<circle cx={24} cy={24} r={8.5} fill="currentColor" />
</svg>
);
}
if (condition === "partly") {
return (
<svg {...common}>
<g className={a("cw-spin")} style={{ transformOrigin: "18px 17px" }}>
{[...Array(8)].map((_, i) => (
<line key={i} x1={18} y1={4} x2={18} y2={8.5} stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" transform={`rotate(${i * 45} 18 17)`} />
))}
</g>
<circle cx={18} cy={17} r={6.5} fill="currentColor" />
<g className={a("cw-drift")}>
<path d="M15 34a7 7 0 0 1 .8-13.9 9 9 0 0 1 17 1.6A6.5 6.5 0 0 1 33 34Z" fill="currentColor" opacity={0.85} />
</g>
</svg>
);
}
if (condition === "cloudy") {
return (
<svg {...common}>
<g className={a("cw-drift")}>
<path d="M14 33a8 8 0 0 1 1-16 10 10 0 0 1 19 2 7.5 7.5 0 0 1-1 14Z" fill="currentColor" opacity={0.9} />
</g>
</svg>
);
}
if (condition === "rain") {
return (
<svg {...common}>
<path d="M14 29a8 8 0 0 1 1-16 10 10 0 0 1 19 2 7.5 7.5 0 0 1-1 14H15Z" fill="currentColor" opacity={0.9} />
{[13, 22, 31].map((x, i) => (
<line key={x} x1={x} y1={33} x2={x - 2} y2={42} stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" className={a("cw-rain")} style={{ animationDelay: `${i * 0.22}s` }} />
))}
</svg>
);
}
if (condition === "snow") {
return (
<svg {...common}>
<path d="M14 29a8 8 0 0 1 1-16 10 10 0 0 1 19 2 7.5 7.5 0 0 1-1 14H15Z" fill="currentColor" opacity={0.9} />
{[13, 22, 31].map((x, i) => (
<circle key={x} cx={x} cy={37} r={2} fill="currentColor" className={a("cw-flake")} style={{ animationDelay: `${i * 0.3}s` }} />
))}
</svg>
);
}
return (
<svg {...common}>
<path d="M14 28a8 8 0 0 1 1-16 10 10 0 0 1 19 2 7.5 7.5 0 0 1-1 14H15Z" fill="currentColor" opacity={0.9} />
<path d="M24 30l-5 8h4l-2 7 8-10h-5l3-5Z" fill="#f59e0b" className={a("cw-flash")} />
</svg>
);
}
function StatChip({ icon, label, value }: { icon: ReactNode; label: string; value: string }) {
return (
<div className="flex items-center gap-2.5 rounded-2xl bg-white/60 px-3 py-2.5 ring-1 ring-slate-900/5 dark:bg-white/5 dark:ring-white/10">
<span className="text-sky-500 dark:text-sky-300">{icon}</span>
<div className="min-w-0 leading-tight">
<div className="text-[11px] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">{label}</div>
<div className="text-sm font-semibold text-slate-700 dark:text-slate-100">{value}</div>
</div>
</div>
);
}
const DropIcon = (
<svg width={16} height={16} viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 3s6 6.5 6 11a6 6 0 1 1-12 0c0-4.5 6-11 6-11Z" fill="currentColor" opacity={0.85} />
</svg>
);
const WindIcon = (
<svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" aria-hidden="true">
<path d="M3 8h11a3 3 0 1 0-3-3M3 16h15a3 3 0 1 1-3 3M3 12h9" />
</svg>
);
const FeelsIcon = (
<svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" aria-hidden="true">
<path d="M12 14V5a2 2 0 0 0-4 0v9a4 4 0 1 0 4 0Z" />
</svg>
);
const PrecipIcon = (
<svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" aria-hidden="true">
<path d="M8 19v2M12 19v3M16 19v2M6 16a5 5 0 0 1 .5-9.9A7 7 0 0 1 20 8a4.5 4.5 0 0 1-.5 8" />
</svg>
);
function UnitToggle({ unit, onChange }: { unit: "c" | "f"; onChange: (u: "c" | "f") => void }) {
return (
<div className="inline-flex items-center rounded-full bg-slate-900/5 p-0.5 ring-1 ring-slate-900/5 dark:bg-white/10 dark:ring-white/10">
{(["c", "f"] as const).map((u) => (
<button
key={u}
type="button"
aria-pressed={unit === u}
onClick={() => onChange(u)}
className={`rounded-full px-3 py-1 text-xs font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
unit === u ? "bg-white text-sky-600 shadow-sm dark:bg-slate-700 dark:text-sky-300" : "text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
°{u.toUpperCase()}
</button>
))}
</div>
);
}
function PinButton({ pinned, onToggle }: { pinned: boolean; onToggle: () => void }) {
return (
<button
type="button"
onClick={onToggle}
aria-pressed={pinned}
aria-label={pinned ? "Unpin location" : "Pin location"}
className={`grid h-9 w-9 place-items-center rounded-full ring-1 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
pinned
? "bg-amber-400/90 text-amber-950 ring-amber-500/40"
: "bg-white/70 text-slate-500 ring-slate-900/10 hover:text-amber-500 dark:bg-white/5 dark:text-slate-400 dark:ring-white/10 dark:hover:text-amber-300"
}`}
>
<svg width={16} height={16} viewBox="0 0 24 24" fill={pinned ? "currentColor" : "none"} stroke="currentColor" strokeWidth={2} strokeLinejoin="round" aria-hidden="true">
<path d="M12 21s-7-5.2-7-11a7 7 0 0 1 14 0c0 5.8-7 11-7 11Z" />
<circle cx={12} cy={10} r={2.4} fill={pinned ? "#fff7ed" : "none"} />
</svg>
</button>
);
}
function FeaturedWeatherCard({ data, unit, onUnitChange, animate }: { data: CityWeather; unit: "c" | "f"; onUnitChange: (u: "c" | "f") => void; animate: boolean }) {
const [selected, setSelected] = useState(data.days[0].key);
const [pinned, setPinned] = useState(true);
const [loading, setLoading] = useState(false);
const [minutesAgo, setMinutesAgo] = useState(4);
const groupId = useId();
const active = useMemo(() => data.days.find((d) => d.key === selected) ?? data.days[0], [data.days, selected]);
const refresh = () => {
if (loading) return;
setLoading(true);
window.setTimeout(() => {
setLoading(false);
setMinutesAgo(0);
}, 900);
};
return (
<article className="w-full overflow-hidden rounded-3xl bg-gradient-to-br from-sky-50 via-white to-indigo-50 shadow-xl shadow-sky-900/5 ring-1 ring-slate-900/5 dark:from-slate-900 dark:via-slate-900 dark:to-indigo-950/60 dark:shadow-black/40 dark:ring-white/10">
<div className="flex items-start justify-between gap-3 p-6 pb-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<h3 className="truncate text-lg font-bold text-slate-800 dark:text-white">{data.city}</h3>
<PinButton pinned={pinned} onToggle={() => setPinned((p) => !p)} />
</div>
<p className="mt-0.5 text-sm text-slate-500 dark:text-slate-400">
{data.region} · {data.localTime} local
</p>
</div>
<div className="flex items-center gap-2">
<UnitToggle unit={unit} onChange={onUnitChange} />
<button
type="button"
onClick={refresh}
disabled={loading}
aria-label="Refresh forecast"
className="grid h-9 w-9 place-items-center rounded-full bg-white/70 text-slate-500 ring-1 ring-slate-900/10 transition hover:text-sky-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:opacity-60 dark:bg-white/5 dark:text-slate-400 dark:ring-white/10 dark:hover:text-sky-300 dark:focus-visible:ring-offset-slate-900"
>
<svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={loading && animate ? "cw-spin-fast" : ""} style={{ transformOrigin: "12px 12px" }}>
<path d="M3 12a9 9 0 0 1 15-6.7L21 8M21 3v5h-5M21 12a9 9 0 0 1-15 6.7L3 16M3 21v-5h5" />
</svg>
</button>
</div>
</div>
<div key={selected} className={animate ? "cw-fade" : ""}>
<div className="flex items-center gap-5 px-6 pb-5">
<div className="text-sky-500 dark:text-sky-300">
<WeatherGlyph condition={active.condition} size={80} animate={animate} />
</div>
<div className="min-w-0">
<div className="flex items-start">
<span className="text-6xl font-bold leading-none tracking-tight text-slate-800 dark:text-white">{toDisplay(active.high, unit)}</span>
<span className="mt-1 text-2xl font-semibold text-slate-400 dark:text-slate-500">°</span>
</div>
<p className="mt-1 text-sm font-medium text-slate-600 dark:text-slate-300">{active.desc}</p>
<p className="text-sm text-slate-400 dark:text-slate-500">
H {toDisplay(active.high, unit)}° · L {toDisplay(active.low, unit)}° · Feels {toDisplay(active.feels, unit)}°
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-2.5 px-6 sm:grid-cols-4">
<StatChip icon={FeelsIcon} label="Feels" value={`${toDisplay(active.feels, unit)}°`} />
<StatChip icon={DropIcon} label="Humidity" value={`${active.humidity}%`} />
<StatChip icon={WindIcon} label="Wind" value={`${active.wind} km/h`} />
<StatChip icon={PrecipIcon} label="Precip" value={`${active.precip}%`} />
</div>
</div>
<div className="mt-5 border-t border-slate-900/5 p-3 dark:border-white/10">
<div className="flex items-center justify-between px-3 pb-2">
<span className="text-xs font-semibold uppercase tracking-wide text-slate-400 dark:text-slate-500">6-day forecast</span>
<span className="text-xs text-slate-400 dark:text-slate-500" aria-live="polite">
{loading ? "Updating…" : minutesAgo === 0 ? "Updated just now" : `Updated ${minutesAgo} min ago`}
</span>
</div>
<div role="group" aria-label={`Forecast days for ${data.city}`} className="grid grid-cols-3 gap-1.5 sm:grid-cols-6">
{data.days.map((day) => {
const isSel = day.key === selected;
return (
<button
key={day.key}
type="button"
onClick={() => setSelected(day.key)}
aria-pressed={isSel}
aria-label={`${day.label}: ${CONDITION_LABEL[day.condition]}, high ${toDisplay(day.high, unit)} degrees, low ${toDisplay(day.low, unit)} degrees`}
className={`flex flex-col items-center gap-1.5 rounded-2xl px-1 py-3 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
isSel ? "bg-sky-500/10 ring-1 ring-sky-500/40 dark:bg-sky-400/10" : "hover:bg-slate-900/[0.04] dark:hover:bg-white/5"
}`}
>
<span className={`text-xs font-semibold ${isSel ? "text-sky-600 dark:text-sky-300" : "text-slate-500 dark:text-slate-400"}`}>{day.label}</span>
<span className={isSel ? "text-sky-500 dark:text-sky-300" : "text-slate-400 dark:text-slate-500"}>
<WeatherGlyph condition={day.condition} size={26} animate={false} />
</span>
<span className="text-xs font-bold text-slate-700 dark:text-slate-100">{toDisplay(day.high, unit)}°</span>
<span className="text-[11px] text-slate-400 dark:text-slate-500">{toDisplay(day.low, unit)}°</span>
</button>
);
})}
</div>
<p className="sr-only" id={groupId}>
Select a day to update the current conditions panel.
</p>
</div>
</article>
);
}
function CompactWeatherCard({ data, unit, animate }: { data: CityWeather; unit: "c" | "f"; animate: boolean }) {
const [pinned, setPinned] = useState(false);
const now = data.days[0];
return (
<article className="flex w-full flex-col overflow-hidden rounded-3xl bg-white shadow-lg shadow-slate-900/5 ring-1 ring-slate-900/5 dark:bg-slate-900 dark:shadow-black/30 dark:ring-white/10">
<div className="flex items-center justify-between gap-2 px-5 pt-5">
<div className="min-w-0">
<h3 className="truncate text-base font-bold text-slate-800 dark:text-white">{data.city}</h3>
<p className="text-xs text-slate-400 dark:text-slate-500">{data.localTime} local</p>
</div>
<PinButton pinned={pinned} onToggle={() => setPinned((p) => !p)} />
</div>
<div className="flex items-center justify-between gap-3 px-5 py-4">
<div className="flex items-baseline">
<span className="text-4xl font-bold tracking-tight text-slate-800 dark:text-white">{toDisplay(now.feels, unit)}</span>
<span className="text-lg font-semibold text-slate-400 dark:text-slate-500">°{unit.toUpperCase()}</span>
</div>
<div className="text-right">
<div className="text-sky-500 dark:text-sky-300">
<WeatherGlyph condition={now.condition} size={44} animate={animate} />
</div>
<p className="mt-0.5 text-xs font-medium text-slate-500 dark:text-slate-400">{now.desc}</p>
</div>
</div>
<div className="mt-auto flex items-center justify-between gap-1 border-t border-slate-900/5 px-3 py-3 dark:border-white/10">
{data.days.slice(1, 4).map((day) => (
<div key={day.key} className="flex flex-1 flex-col items-center gap-1 rounded-xl py-1">
<span className="text-[11px] font-semibold text-slate-400 dark:text-slate-500">{day.label}</span>
<span className="text-slate-400 dark:text-slate-500">
<WeatherGlyph condition={day.condition} size={22} animate={false} />
</span>
<span className="text-xs font-bold text-slate-700 dark:text-slate-200">{toDisplay(day.high, unit)}°</span>
</div>
))}
</div>
</article>
);
}
export default function WeatherWidgetCard() {
const [unit, setUnit] = useState<"c" | "f">("c");
const prefersReduced = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false;
const animate = !prefersReduced;
return (
<section className="relative w-full bg-slate-50 px-4 py-16 dark:bg-slate-950 sm:px-6 sm:py-24">
<style>{`
@keyframes cw-spin { to { transform: rotate(360deg); } }
@keyframes cw-spin-fast { to { transform: rotate(360deg); } }
@keyframes cw-drift { 0%,100% { transform: translateX(0); } 50% { transform: translateX(2px); } }
@keyframes cw-rain { 0% { opacity: 0; transform: translateY(-4px); } 30% { opacity: 1; } 100% { opacity: 0; transform: translateY(6px); } }
@keyframes cw-flake { 0% { opacity: 0; transform: translateY(-3px); } 40% { opacity: 1; } 100% { opacity: 0; transform: translateY(7px); } }
@keyframes cw-flash { 0%,45%,100% { opacity: 1; } 50%,60% { opacity: 0.25; } }
@keyframes cw-fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
.cw-spin { animation: cw-spin 14s linear infinite; }
.cw-spin-fast { animation: cw-spin-fast 0.7s linear infinite; }
.cw-drift { animation: cw-drift 5s ease-in-out infinite; }
.cw-rain { animation: cw-rain 1.1s ease-in infinite; }
.cw-flake { animation: cw-flake 2s ease-in infinite; }
.cw-flash { animation: cw-flash 3s ease-in-out infinite; }
.cw-fade { animation: cw-fade 0.35s ease-out; }
@media (prefers-reduced-motion: reduce) {
.cw-spin, .cw-spin-fast, .cw-drift, .cw-rain, .cw-flake, .cw-flash, .cw-fade { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-5xl">
<div className="mb-10 text-center">
<h2 className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-3xl">Weather</h2>
<p className="mx-auto mt-2 max-w-md text-sm text-slate-500 dark:text-slate-400">
Live conditions and a six-day outlook. Tap a day to see its details; the °C/°F switch applies everywhere.
</p>
</div>
<div className="grid gap-6 lg:grid-cols-5">
<div className="lg:col-span-3">
<FeaturedWeatherCard data={SAN_FRANCISCO} unit={unit} onUnitChange={setUnit} animate={animate} />
</div>
<div className="grid gap-6 sm:grid-cols-2 lg:col-span-2 lg:grid-cols-1">
<CompactWeatherCard data={TOKYO} unit={unit} animate={animate} />
<CompactWeatherCard data={REYKJAVIK} unit={unit} animate={animate} />
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
3D Tilt Cards
OriginalA responsive grid of cards that tilt in 3D toward your cursor with a light glare and lifted depth layers, powered by Framer Motion springs.

Spotlight Follow Cards
OriginalFeature cards where a soft radial spotlight and a glowing border chase the cursor on hover, with a staggered scroll reveal and an animated shimmer line.

3D Flip Cards
OriginalPricing-style cards that flip in 3D on hover or tap to reveal details on the back, keyboard accessible with a staggered entrance animation.

Animated Gradient Border Cards
OriginalCards wrapped in a live conic gradient border that rotates and speeds up on hover, with a shimmer sweep and a scrolling tag marquee.

Glow Effect Card
primitivesA reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card
gradient-card.tsxA reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card
card.tsxA reusable spotlight magic card for React and Tailwind, with hover and motion.

Tilt Spotlight Card
primitivesA reusable tilt spotlight card for React and Tailwind, with hover and motion.

Profile Card
OriginalA user profile card with avatar, stats and a follow toggle.

Product Card
OriginalA product card with image, price and add-to-cart feedback.

Pricing Single Card
OriginalA single pricing plan card with feature list and CTA.

Stat Card
OriginalA metric card with value, trend arrow and a small sparkline.

