Smoke Background Hero
HeroFull-viewport hero with an animated smoke/fluid canvas background, an alert pill, shimmering heading, dual CTAs, and a tech-stack logo row.
Create a Svelte 5 (runes) full-viewport hero (`min-h-[calc(100dvh-header)]`) with an animated smoke/fluid simulation running on a canvas absolutely filling the background, tinted with a theme-aware color that swaps between light/dark mode values via `mode-watcher`. Centered content: a pill-shaped 'alert' link (dark rounded-full background, a small primary-colored label chip on the left, message text, and a trailing chevron icon), a large shimmering-text heading (white, with the shimmer sweep colored `var(--primary)`), a subtext paragraph, two CTA buttons full-width on mobile (solid with white shine + outline with primary-tinted shine, both using a looping shine/sheen hover effect), and a 'tech stack' row of small logo+label pairs that fade in with a staggered entrance. Add manual CSS keyframe entrance animations (staggered slide-down for the alert, slide-up for the heading/CTAs, fade for subtext/tech-row) each with their own delay, and respect `prefers-reduced-motion` by disabling them. Data-driven via `content: { alert:{label,text,href}, heading, subtext, primary_cta, secondary_cta, tech_stack_label, tech_stack: string[] }`.
<script lang="ts">
import { mode } from 'mode-watcher';
import { Button } from '$lib/components/ui/button';
import ShimmeringText from '$lib/components/animate-ui/ShimmeringText.svelte';
import Shine from '$lib/components/animate-ui/Shine.svelte';
import SmokeBackground from '$lib/components/backgrounds/SmokeBackground.svelte';
type AlertContent = { label: string; text: string; href: string };
type CtaContent = { label: string; href: string };
type Props = {
content: {
alert: AlertContent;
heading: string;
subtext: string;
primary_cta: CtaContent;
secondary_cta: CtaContent;
tech_stack_label: string;
tech_stack: string[];
};
};
let { content }: Props = $props();
const techStackFilename: Record<string, string> = {
Golang: 'Go',
PostgreSQL: 'PostgresSQL',
'C#': 'C# (CSharp)',
'C++': 'C++ (CPlusPlus)',
'Next.js': 'Next.js',
'Node.js': 'Node.js',
'Vue.js': 'Vue.js',
'Nuxt JS': 'Nuxt JS',
'Nest.js': 'Nest.js',
'Tailwind CSS': 'Tailwind CSS',
'NET core': 'NET core',
'.NET': 'NET',
};
function techSrc(name: string) {
return `/techstacks/${techStackFilename[name] ?? name}.svg`;
}
const smokeColor = $derived(mode.current === 'dark' ? '#024C77' : '#0078B7');
</script>
<section class="relative z-0 overflow-hidden min-h-[calc(100dvh-var(--header-height))] flex items-center">
<div class="absolute inset-0 w-full h-full">
<SmokeBackground {smokeColor} />
</div>
<div class="relative z-10 py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16 lg:px-12 w-full">
<a
href={content.alert.href}
class="inline-flex justify-between items-center py-1 px-1 pr-4 mb-7 text-sm text-white bg-gray-800 rounded-full hover:bg-gray-700 hero-anim-1"
>
<span class="text-xs bg-primary rounded-full text-primary-foreground px-4 py-1.5 mr-3">
{content.alert.label}
</span>
<span class="text-sm font-medium">{content.alert.text}</span>
<svg class="ml-2 w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
</a>
<h1 class="mb-4 text-4xl font-extrabold tracking-tight leading-none text-white md:text-5xl lg:text-6xl hero-anim-2">
<ShimmeringText text={content.heading} color="#ffffff" shimmeringColor="var(--primary)" />
</h1>
<p class="mb-8 text-lg font-normal text-gray-400 lg:text-xl sm:px-16 xl:px-48 hero-anim-3">
{content.subtext}
</p>
<div class="flex flex-col mb-8 lg:mb-16 space-y-4 sm:flex-row sm:justify-center sm:space-y-0 sm:space-x-4 hero-anim-4">
<Shine enableOnHover color="#ffffff" opacity={0.4} class="rounded-lg">
<Button size="lg" class="h-16 px-8 w-full" href={content.primary_cta.href}>
{content.primary_cta.label}
<svg class="ml-2 -mr-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</Button>
</Shine>
<Shine enableOnHover color="#0078B7" opacity={0.3} class="rounded-lg">
<Button variant="outline" size="lg" class="h-16 px-8 w-full text-white" href={content.secondary_cta.href}>
{content.secondary_cta.label}
</Button>
</Shine>
</div>
<div class="px-4 mx-auto text-center md:max-w-screen-md lg:max-w-screen-lg lg:px-36 hero-anim-5">
<span class="font-semibold text-gray-400 uppercase">{content.tech_stack_label}</span>
<div class="flex flex-wrap justify-center items-center mt-6 gap-x-10 gap-y-6">
{#each content.tech_stack as name}
<div class="flex flex-col items-center gap-2 opacity-80 hover:opacity-100 transition-opacity">
<img src={techSrc(name)} alt={name} class="h-10 w-10 object-contain" loading="lazy" decoding="async" />
<span class="text-xs font-semibold text-gray-400 tracking-wide uppercase">{name}</span>
</div>
{/each}
</div>
</div>
</div>
</section>
<style>
@keyframes hero-down {
from { opacity: 0; transform: translateY(-16px); }
to { opacity: 1; transform: none; }
}
@keyframes hero-up {
from { opacity: 0; transform: translateY(28px); }
to { opacity: 1; transform: none; }
}
@keyframes hero-fade {
from { opacity: 0; }
to { opacity: 1; }
}
.hero-anim-1 { animation: hero-down 0.6s cubic-bezier(0.22, 1, 0.36, 1) both 0ms; }
.hero-anim-2 { animation: hero-up 0.7s cubic-bezier(0.22, 1, 0.36, 1) both 150ms; }
.hero-anim-3 { animation: hero-fade 0.7s ease-out both 300ms; }
.hero-anim-4 { animation: hero-up 0.7s cubic-bezier(0.22, 1, 0.36, 1) both 450ms; }
.hero-anim-5 { animation: hero-fade 0.8s ease-out both 620ms; }
@media (prefers-reduced-motion: reduce) {
.hero-anim-1, .hero-anim-2, .hero-anim-3, .hero-anim-4, .hero-anim-5 { animation: none; }
}
</style>