Skip to content

Commit 8e49db6

Browse files
Enhance HeroBanner animations and accessibility
Updated transition durations and added text variants for animations. Improved accessibility attributes and adjusted styles for controls and dots.
1 parent 53fc19d commit 8e49db6

1 file changed

Lines changed: 104 additions & 49 deletions

File tree

src/components/home/HeroBanner.jsx

Lines changed: 104 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,14 @@ const SLIDES = [
5252

5353
const slideVariants = {
5454
enter: (dir) => ({ x: dir > 0 ? '100%' : '-100%', opacity: 0 }),
55-
center: { x: 0, opacity: 1, transition: { duration: 0.55, ease: [0.25, 0.1, 0.25, 1] } },
56-
exit: (dir) => ({ x: dir < 0 ? '100%' : '-100%', opacity: 0, transition: { duration: 0.45 } }),
55+
center: { x: 0, opacity: 1, transition: { duration: 0.6, ease: [0.25, 0.1, 0.25, 1] } },
56+
exit: (dir) => ({ x: dir < 0 ? '100%' : '-100%', opacity: 0, transition: { duration: 0.5 } }),
57+
};
58+
59+
const textVariants = {
60+
hidden: { opacity: 0, y: 30 },
61+
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: 'easeOut' } },
62+
exit: { opacity: 0, y: -20, transition: { duration: 0.3 } }
5763
};
5864

5965
export default function HeroBanner() {
@@ -72,25 +78,44 @@ export default function HeroBanner() {
7278

7379
useEffect(() => {
7480
if (paused) return;
75-
const id = setInterval(next, 4000);
81+
const id = setInterval(next, 5000);
7682
return () => clearInterval(id);
7783
}, [next, paused]);
7884

85+
const handleDragEnd = (e, { offset }) => {
86+
const swipe = offset.x;
87+
if (swipe < -50) {
88+
next();
89+
} else if (swipe > 50) {
90+
prev();
91+
}
92+
};
93+
94+
const handleKeyDown = (e) => {
95+
if (e.key === 'ArrowLeft') prev();
96+
if (e.key === 'ArrowRight') next();
97+
};
98+
7999
const slide = SLIDES[current];
80100
const textAlign = slide.align === 'center' ? 'items-center text-center' : slide.align === 'right' ? 'items-end text-right' : 'items-start text-left';
81101

82102
return (
83103
<section
84104
id="hero-banner"
85-
className="relative w-full overflow-hidden h-[60vh] md:h-[75vh] lg:h-[85vh] min-h-[400px] max-h-[800px]"
105+
className="relative w-full overflow-hidden h-[60vh] md:h-[75vh] lg:h-[85vh] min-h-[400px] max-h-[800px] bg-black focus-ring"
86106
onMouseEnter={() => setPaused(true)}
87107
onMouseLeave={() => setPaused(false)}
88-
aria-label="Hero banner"
108+
onFocus={() => setPaused(true)}
109+
onBlur={() => setPaused(false)}
110+
onKeyDown={handleKeyDown}
111+
tabIndex={0}
112+
aria-label="Hero carousel"
113+
aria-roledescription="carousel"
89114
>
90115
{/* Subtle pattern overlay */}
91116
<div
92117
className="absolute inset-0 z-[1] opacity-[0.06] pointer-events-none"
93-
style={{ backgroundImage: `url(\${import.meta.env.BASE_URL}assets/illustrations/hero-pattern-bg.svg)` }}
118+
style={{ backgroundImage: `url(${import.meta.env.BASE_URL}assets/illustrations/hero-pattern-bg.svg)` }}
94119
aria-hidden="true"
95120
/>
96121

@@ -103,88 +128,118 @@ export default function HeroBanner() {
103128
initial="enter"
104129
animate="center"
105130
exit="exit"
106-
className="absolute inset-0 z-[2]"
131+
drag="x"
132+
dragConstraints={{ left: 0, right: 0 }}
133+
dragElastic={0.8}
134+
onDragEnd={handleDragEnd}
135+
className="absolute inset-0 z-[2] cursor-grab active:cursor-grabbing"
136+
role="group"
137+
aria-roledescription="slide"
138+
aria-label={`Slide ${current + 1} of ${total}`}
107139
>
108-
<img
140+
<motion.img
109141
src={slide.image}
110-
alt={slide.heading}
142+
alt=""
111143
width={1440}
112144
height={800}
113145
loading={current === 0 ? 'eager' : 'lazy'}
146+
fetchpriority={current === 0 ? 'high' : 'auto'}
114147
className="h-full w-full object-cover object-[center_20%]"
148+
initial={{ scale: 1 }}
149+
animate={{ scale: 1.05 }}
150+
transition={{ duration: 6, ease: 'linear' }}
115151
/>
116152
{/* Gradient overlay */}
117153
<div
118154
className="absolute inset-0"
119155
style={{
120156
background: slide.align === 'right'
121-
? 'linear-gradient(to left, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.2) 60%, transparent 100%)'
157+
? 'linear-gradient(to left, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.3) 50%, transparent 100%)'
122158
: slide.align === 'center'
123-
? 'linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.2) 50%, transparent 100%)'
124-
: 'linear-gradient(to right, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.2) 60%, transparent 100%)'
159+
? 'linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.3) 50%, transparent 100%)'
160+
: 'linear-gradient(to right, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.3) 50%, transparent 100%)'
125161
}}
126162
/>
127-
<div className="absolute inset-0 bg-black/10" />
128163
</motion.div>
129164
</AnimatePresence>
130165

131166
{/* Text overlay */}
132-
<div className={`absolute inset-0 z-[3] flex flex-col justify-center px-8 md:px-20 gap-4 ${textAlign}`}>
167+
<div className={`absolute inset-0 z-[3] flex flex-col justify-center px-8 md:px-20 gap-4 pointer-events-none ${textAlign}`}>
133168
<AnimatePresence mode="wait">
134169
<motion.div
135170
key={slide.id}
136-
initial={{ opacity: 0, y: 22 }}
137-
animate={{ opacity: 1, y: 0, transition: { delay: 0.25, duration: 0.4 } }}
138-
exit={{ opacity: 0, y: -10, transition: { duration: 0.2 } }}
139-
className={`flex flex-col gap-3 ${textAlign}`}
171+
variants={textVariants}
172+
initial="hidden"
173+
animate="visible"
174+
exit="exit"
175+
className={`flex flex-col gap-4 pointer-events-auto max-w-2xl ${textAlign}`}
176+
aria-live="polite"
140177
>
141-
<h1
142-
className="text-4xl font-normal leading-tight text-white md:text-6xl"
143-
style={{ fontFamily: slide.font || 'var(--font-heading)', textShadow: '0 2px 12px rgba(0,0,0,0.6)' }}
178+
<motion.h1
179+
className="text-4xl font-normal leading-tight text-white md:text-6xl lg:text-7xl drop-shadow-xl"
180+
style={{ fontFamily: slide.font || 'var(--font-heading)' }}
181+
initial={{ opacity: 0, y: 20 }}
182+
animate={{ opacity: 1, y: 0 }}
183+
transition={{ delay: 0.2, duration: 0.5 }}
144184
>
145185
{slide.heading}
146-
</h1>
147-
<p className="max-w-lg text-base text-white/90 md:text-lg" style={{ fontFamily: 'var(--font-body)', textShadow: '0 1px 6px rgba(0,0,0,0.6)' }}>
186+
</motion.h1>
187+
<motion.p
188+
className="text-base text-white/90 md:text-xl drop-shadow-md"
189+
style={{ fontFamily: 'var(--font-body)' }}
190+
initial={{ opacity: 0, y: 20 }}
191+
animate={{ opacity: 1, y: 0 }}
192+
transition={{ delay: 0.3, duration: 0.5 }}
193+
>
148194
{slide.subheading}
149-
</p>
150-
<Link
151-
to={slide.ctaLink}
152-
className="btn-glow mt-4 inline-flex w-fit items-center rounded-full px-8 py-3.5 text-[15px] font-bold tracking-wide uppercase text-white shadow-lg"
153-
style={{ background: 'var(--color-primary)', fontFamily: 'var(--font-syne)' }}
195+
</motion.p>
196+
<motion.div
197+
initial={{ opacity: 0, y: 20 }}
198+
animate={{ opacity: 1, y: 0 }}
199+
transition={{ delay: 0.4, duration: 0.5 }}
154200
>
155-
{slide.cta}
156-
</Link>
201+
<Link
202+
to={slide.ctaLink}
203+
className="btn-glow mt-4 inline-flex w-fit items-center rounded-full px-8 py-3.5 text-[15px] font-bold tracking-wide uppercase text-white shadow-lg focus-ring"
204+
style={{ background: 'var(--color-primary)', fontFamily: 'var(--font-syne)' }}
205+
>
206+
{slide.cta}
207+
</Link>
208+
</motion.div>
157209
</motion.div>
158210
</AnimatePresence>
159211
</div>
160212

161-
{/* Arrows */}
162-
<button
163-
onClick={prev}
164-
aria-label="Previous slide"
165-
className="absolute left-4 top-1/2 z-[4] -translate-y-1/2 flex h-10 w-10 items-center justify-center rounded-full bg-black/30 text-white backdrop-blur-sm transition hover:bg-black/50"
166-
>
167-
<FiChevronLeft size={22} />
168-
</button>
169-
<button
170-
onClick={next}
171-
aria-label="Next slide"
172-
className="absolute right-4 top-1/2 z-[4] -translate-y-1/2 flex h-10 w-10 items-center justify-center rounded-full bg-black/30 text-white backdrop-blur-sm transition hover:bg-black/50"
173-
>
174-
<FiChevronRight size={22} />
175-
</button>
213+
{/* Controls */}
214+
<div className="absolute inset-0 z-[4] pointer-events-none flex items-center justify-between px-4 md:px-8">
215+
<button
216+
onClick={prev}
217+
aria-label="Previous slide"
218+
className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full bg-black/20 text-white backdrop-blur-md transition-all hover:bg-black/50 hover:scale-110 focus-ring"
219+
>
220+
<FiChevronLeft size={24} />
221+
</button>
222+
<button
223+
onClick={next}
224+
aria-label="Next slide"
225+
className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full bg-black/20 text-white backdrop-blur-md transition-all hover:bg-black/50 hover:scale-110 focus-ring"
226+
>
227+
<FiChevronRight size={24} />
228+
</button>
229+
</div>
176230

177231
{/* Dots */}
178-
<div className="absolute bottom-5 left-1/2 z-[4] flex -translate-x-1/2 gap-2">
232+
<div className="absolute bottom-6 left-1/2 z-[4] flex -translate-x-1/2 gap-3">
179233
{SLIDES.map((_, i) => (
180234
<button
181235
key={i}
182236
onClick={() => goTo(i, i > current ? 1 : -1)}
183237
aria-label={`Go to slide ${i + 1}`}
184-
className="h-2 rounded-full transition-all duration-300"
238+
aria-current={i === current}
239+
className="relative h-2 rounded-full transition-all duration-500 focus-ring"
185240
style={{
186-
width: i === current ? '28px' : '8px',
187-
background: i === current ? 'var(--color-primary)' : 'rgba(255,255,255,0.5)',
241+
width: i === current ? '32px' : '12px',
242+
background: i === current ? 'var(--color-primary)' : 'rgba(255,255,255,0.4)',
188243
}}
189244
/>
190245
))}

0 commit comments

Comments
 (0)