Content & marketing
Parallax Scroll Sequence code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/parallax-scroll-sequence.blade.php
@props([
'images' => [],
'frames' => null,
'eyebrow' => null,
'title' => null,
'description' => null,
])
@php
/*
| Scroll-sequence parallax. It swaps between a small set of image frames and
| moves a progress rail as the section passes through the viewport.
*/
$defaultFrames = [
['label' => 'Wide establish', 'copy' => 'The first plate anchors the scene and gives the scroll a clear starting point.', 'meta' => 'Establish'],
['label' => 'Move through', 'copy' => 'The middle frame carries the eye forward while the background keeps drifting.', 'meta' => 'Advance'],
['label' => 'Final hold', 'copy' => 'The last frame lands the sequence with a stronger composed moment.', 'meta' => 'Hold'],
];
$sequenceFrames = collect($frames ?? $images)
->map(function (array|string $frame, int $index) use ($defaultFrames): array {
$fallback = $defaultFrames[$index] ?? [
'label' => 'Frame '.str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT),
'copy' => 'A sequenced parallax frame with its own timing and active state.',
'meta' => 'Frame',
];
if (is_string($frame)) {
return [
'src' => $frame,
'label' => $fallback['label'],
'copy' => $fallback['copy'],
'meta' => $fallback['meta'],
];
}
return [
'src' => $frame['src'] ?? $frame['image'] ?? '',
'label' => $frame['label'] ?? $fallback['label'],
'copy' => $frame['copy'] ?? $frame['description'] ?? $fallback['copy'],
'meta' => $frame['meta'] ?? $fallback['meta'],
];
})
->filter(fn (array $frame): bool => $frame['src'] !== '')
->values()
->all();
@endphp
<section
x-data="{
frameData: @js($sequenceFrames),
progress: 0,
frame: 0,
reduced: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
currentFrame() {
return this.frameData[this.frame] ?? { label: 'Frame', copy: '', meta: 'Frame' };
},
update() {
const rect = $el.getBoundingClientRect();
const travel = rect.height + window.innerHeight;
const raw = (window.innerHeight - rect.top) / travel;
this.progress = Math.min(1, Math.max(0, raw));
if (this.frameData.length === 0) {
this.frame = 0;
return;
}
this.frame = this.reduced ? 0 : Math.min(this.frameData.length - 1, Math.floor(this.progress * this.frameData.length));
},
}"
x-init="update()"
@scroll.window.passive="update()"
{{ $attributes->merge(['class' => 'relative isolate min-h-[170vh] overflow-hidden bg-gray-950']) }}
>
<div class="sticky top-0 flex min-h-screen items-center overflow-hidden">
@foreach ($sequenceFrames as $index => $sequenceFrame)
<img
src="{{ $sequenceFrame['src'] }}"
alt=""
aria-hidden="true"
class="pointer-events-none absolute inset-0 -z-20 h-full w-full scale-[1.1] object-cover transition-opacity duration-700 will-change-transform motion-reduce:scale-100 motion-reduce:transform-none"
:class="frame === {{ $index }} ? 'opacity-100' : 'opacity-0'"
style="transform: translate3d(0, 0, 0) scale(1.1);"
:style="reduced ? 'transform: translate3d(0, 0, 0) scale(1);' : `transform: translate3d(${(progress - 0.5) * -26}px, ${(progress - 0.5) * -64}px, 0) scale(${1.1 + (progress * 0.04)});`"
>
@endforeach
<div class="absolute inset-0 -z-10 bg-gradient-to-r from-gray-950/90 via-gray-950/28 to-gray-950/82" aria-hidden="true"></div>
<div class="absolute inset-x-0 top-0 -z-10 h-36 bg-gradient-to-b from-gray-950/80 to-transparent" aria-hidden="true"></div>
<div class="absolute inset-x-0 bottom-0 -z-10 h-44 bg-gradient-to-t from-gray-950 to-transparent" aria-hidden="true"></div>
<div class="mx-auto grid w-full max-w-7xl items-center gap-10 px-6 py-16 sm:px-8 lg:grid-cols-[0.76fr_1.24fr] lg:px-10">
<div>
@if ($eyebrow)
<x-ui.badge variant="brand" icon="film-strip" class="bg-white/12 text-white ring-1 ring-white/20 backdrop-blur dark:bg-white/12 dark:text-white">
{{ $eyebrow }}
</x-ui.badge>
@endif
@if ($title)
<h2 class="mt-5 text-4xl font-semibold tracking-tight text-white text-balance sm:text-6xl">{{ $title }}</h2>
@endif
@if ($description)
<p class="mt-5 max-w-2xl text-base leading-7 text-white/78 text-pretty sm:text-lg">{{ $description }}</p>
@endif
@isset($actions)
<div class="mt-8 flex flex-col gap-3 sm:flex-row">{{ $actions }}</div>
@endisset
</div>
<div class="relative overflow-hidden rounded-3xl border border-white/15 bg-white/10 p-4 shadow-2xl shadow-gray-950/50 backdrop-blur-md">
<div class="relative aspect-[16/9] overflow-hidden rounded-2xl border border-white/10 bg-gray-950/70">
@foreach ($sequenceFrames as $index => $sequenceFrame)
<img
src="{{ $sequenceFrame['src'] }}"
alt=""
aria-hidden="true"
class="absolute inset-0 h-full w-full object-cover transition-opacity duration-700"
:class="frame === {{ $index }} ? 'opacity-100' : 'opacity-0'"
>
@endforeach
<div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-gray-950/92 via-gray-950/20 to-transparent p-4">
<div class="flex items-end justify-between gap-4">
<div>
<p class="text-xs font-medium text-brand-200">Active frame</p>
<p class="mt-1 text-lg font-semibold text-white" x-text="currentFrame().label"></p>
</div>
<p class="rounded-full border border-white/15 bg-white/10 px-3 py-1 text-sm font-medium text-white" x-text="String(frame + 1).padStart(2, '0') + ' / ' + String(frameData.length).padStart(2, '0')"></p>
</div>
</div>
</div>
<div class="mt-5 flex items-center justify-between gap-4 text-sm text-white">
<span x-text="currentFrame().meta"></span>
<span x-text="Math.round(progress * 100) + '%'"></span>
</div>
<div class="mt-3 h-2 overflow-hidden rounded-full bg-white/15">
<div class="h-full rounded-full bg-brand-300 transition-[width] duration-200" :style="`width: ${Math.round(progress * 100)}%;`"></div>
</div>
<p class="mt-4 text-sm leading-6 text-white/70" x-text="currentFrame().copy"></p>
<div class="mt-5 grid gap-3">
@foreach ($sequenceFrames as $index => $sequenceFrame)
<div
class="grid grid-cols-[4.5rem_1fr] gap-3 rounded-2xl border p-2 transition"
:class="frame === {{ $index }} ? 'border-brand-300/60 bg-brand-300/12' : 'border-white/10 bg-white/5'"
>
<img src="{{ $sequenceFrame['src'] }}" alt="" aria-hidden="true" class="h-16 w-full rounded-xl object-cover">
<div>
<p class="text-sm font-semibold text-white">{{ $sequenceFrame['label'] }}</p>
<p class="mt-1 text-sm leading-5 text-white/62">{{ $sequenceFrame['copy'] }}</p>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</section>