STYLED - Add CSS hero entrance, title halo, and scroll cue

Replace the framer-motion hero entrance with pure-CSS staggered animations so
the hero is painted immediately instead of flashing blank until hydration.
Add a focused brand glow behind the hero title and an animated scroll cue at
the bottom of the home hero. Entrance and cue respect prefers-reduced-motion.
This commit is contained in:
moh
2026-09-20 18:53:48 +02:00
parent bb9f1a2f6e
commit 5b019052b1
2 changed files with 102 additions and 37 deletions
+89
View File
@@ -422,6 +422,95 @@ html[lang="ar"] .eyebrow {
}
}
/* ---- Hero entrance (pure CSS so it never flashes blank before hydration) ---- */
.hero-rise {
opacity: 0;
animation: hero-rise-in 0.62s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.hero-content > *:nth-child(1) {
animation-delay: 0.06s;
}
.hero-content > *:nth-child(2) {
animation-delay: 0.14s;
}
.hero-content > *:nth-child(3) {
animation-delay: 0.22s;
}
.hero-content > *:nth-child(4) {
animation-delay: 0.3s;
}
.hero-content > *:nth-child(5) {
animation-delay: 0.38s;
}
.hero-content > *:nth-child(6) {
animation-delay: 0.46s;
}
@keyframes hero-rise-in {
from {
opacity: 0;
transform: translateY(18px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Focused glow directly behind the hero title */
.hero-title {
position: relative;
isolation: isolate;
}
.hero-title::before {
content: "";
position: absolute;
left: 50%;
top: 46%;
z-index: -1;
width: 84%;
height: 74%;
transform: translate(-50%, -50%);
background: radial-gradient(closest-side, hsl(var(--brand-primary) / 0.2), transparent 78%);
filter: blur(48px);
pointer-events: none;
}
.dark .hero-title::before {
background: radial-gradient(closest-side, hsl(var(--brand-primary) / 0.3), transparent 78%);
}
/* Scroll cue pinned to the bottom of the home hero */
.hero-scroll-anchor {
position: absolute;
bottom: 1.5rem;
left: 50%;
z-index: 10;
transform: translateX(-50%);
opacity: 0;
animation: hero-fade-in 0.6s ease 0.7s forwards;
}
.hero-scroll-anchor:hover {
transform: translateX(-50%) translateY(2px);
}
@keyframes hero-fade-in {
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.hero-rise,
.hero-scroll-anchor {
animation: none;
opacity: 1;
}
}
.hero-title-line {
display: inline-block;
padding-inline: 0.02em;
+13 -37
View File
@@ -1,30 +1,11 @@
"use client";
import type { ReactNode } from "react";
import { ArrowDown } from "lucide-react";
import { motion } from "framer-motion";
import { Container } from "@/components/layout/container";
import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
import { cn } from "@/lib/utils";
const heroItemVariants = {
hidden: { opacity: 0, y: 16 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.54, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
},
};
const heroContainerVariants = {
hidden: {},
visible: {
transition: { staggerChildren: 0.13, delayChildren: 0.08 },
},
};
type HeroTone = "default" | "accent" | "soft";
type HeroLineAlign = "start" | "center" | "end";
@@ -128,6 +109,14 @@ export function HeroShell({
{children}
</div>
</Container>
{variant === "home" ? (
<HeroScrollLink
href="#home-content"
label="Scroll to content"
className="hero-scroll-anchor hidden sm:inline-flex"
/>
) : null}
</section>
);
}
@@ -136,27 +125,14 @@ export function HeroContentMotion({
children,
className,
}: HeroContentMotionProps) {
return (
<motion.div
className={className}
initial="hidden"
animate="visible"
variants={heroContainerVariants}
>
{children}
</motion.div>
);
return <div className={cn("hero-content", className)}>{children}</div>;
}
export function HeroMotionItem({
children,
className,
}: HeroContentMotionProps) {
return (
<motion.div className={className} variants={heroItemVariants}>
{children}
</motion.div>
);
return <div className={cn("hero-rise", className)}>{children}</div>;
}
export function HeroTitle({
@@ -167,7 +143,7 @@ export function HeroTitle({
}: HeroTitleProps) {
const isArabic = locale === "ar";
const titleClassName = cn(
"text-balance font-semibold text-[hsl(var(--hero-ink))]",
"hero-title hero-rise text-balance font-semibold text-[hsl(var(--hero-ink))]",
variant === "home"
? isArabic
? "mt-6 flex w-full max-w-[26rem] flex-col gap-y-1 px-4 text-[clamp(2.85rem,11vw,5.9rem)] leading-[1.06] tracking-[-0.03em] sm:max-w-[30rem] sm:px-0 md:max-w-[36rem] lg:max-w-[32rem]"
@@ -177,7 +153,7 @@ export function HeroTitle({
);
return (
<motion.h1 className={titleClassName} variants={heroItemVariants}>
<h1 className={titleClassName}>
{lines.map((line, index) => (
<span
key={`${index}-${line.text}`}
@@ -202,7 +178,7 @@ export function HeroTitle({
)}
</span>
))}
</motion.h1>
</h1>
);
}