HeroPortfolio Hero
Editorial split hero: grote headline links, gestapelde case cards rechts. Hover lift animatie. Ideaal voor agency portfolios.
src/components/hero/HeroPortfolio.astro
---
interface CaseItem {
title: string;
category: string;
image: string;
href?: string;
}
interface Props {
eyebrow?: string;
headline: string;
sub?: string;
ctaLabel?: string;
ctaHref?: string;
cases: CaseItem[];
}
const {
eyebrow,
headline,
sub,
ctaLabel,
ctaHref = '#',
cases = [],
} = Astro.props;
// Clamp to 3 visible cases max
const visibleCases = cases.slice(0, 3);
---
<section class="hpf__root" aria-label="Hero">
<div class="hpf__container">
<!-- Left: content -->
<div class="hpf__left">
{eyebrow && <span class="hpf__eyebrow">{eyebrow}</span>}
<h1 class="hpf__headline" set:html={headline} />
{sub && <p class="hpf__sub">{sub}</p>}
{ctaLabel && (
<a href={ctaHref} class="hpf__cta">
<span>{ctaLabel}</span>
<svg
class="hpf__cta-arrow"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path
d="M3 8h10M9 4l4 4-4 4"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
)}
</div>
<!-- Right: case cards -->
<div class="hpf__right" aria-label="Portfolio cases">
{visibleCases.map((item, i) => (
<article
class="hpf__card"
style={`animation-delay: ${i * 120}ms`}
data-index={i}
>
{item.href ? (
<a href={item.href} class="hpf__card-link" tabindex="0">
<div class="hpf__card-img-wrap">
<img
src={item.image}
alt={item.title}
class="hpf__card-img"
loading={i === 0 ? 'eager' : 'lazy'}
fetchpriority={i === 0 ? 'high' : 'auto'}
/>
</div>
<div class="hpf__card-body">
<span class="hpf__card-cat">{item.category}</span>
<h2 class="hpf__card-title">{item.title}</h2>
</div>
</a>
) : (
<div class="hpf__card-link">
<div class="hpf__card-img-wrap">
<img
src={item.image}
alt={item.title}
class="hpf__card-img"
loading={i === 0 ? 'eager' : 'lazy'}
fetchpriority={i === 0 ? 'high' : 'auto'}
/>
</div>
<div class="hpf__card-body">
<span class="hpf__card-cat">{item.category}</span>
<h2 class="hpf__card-title">{item.title}</h2>
</div>
</div>
)}
</article>
))}
</div>
</div>
</section>
<style>
:root {
--color-primary: #0a0a0a;
--color-accent: #6366f1;
--color-bg: #fff;
--color-muted: #6b7280;
--radius: 0.5rem;
}
.hpf__root {
background: var(--color-primary);
color: #fff;
min-height: 100svh;
display: flex;
align-items: center;
overflow: hidden;
padding: clamp(4rem, 8vw, 7rem) clamp(1.5rem, 6vw, 6rem);
}
.hpf__container {
width: 100%;
max-width: 1440px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: clamp(3rem, 6vw, 8rem);
align-items: center;
}
/* Left column */
.hpf__left {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.hpf__eyebrow {
display: inline-block;
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-accent);
padding: 0.3rem 0.875rem;
border: 1px solid var(--color-accent);
border-radius: 999px;
width: fit-content;
}
.hpf__headline {
font-size: clamp(3.5rem, 5.5vw, 6rem);
font-weight: 900;
line-height: 1.0;
letter-spacing: -0.035em;
color: #fff;
margin: 0;
}
.hpf__headline em {
font-style: normal;
color: var(--color-accent);
}
.hpf__sub {
font-size: clamp(1rem, 1.4vw, 1.1875rem);
color: rgba(255, 255, 255, 0.6);
line-height: 1.7;
margin: 0;
max-width: 44ch;
}
.hpf__cta {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--color-accent);
color: #fff;
text-decoration: none;
font-weight: 600;
font-size: 0.9375rem;
padding: 0.875rem 1.875rem;
border-radius: var(--radius);
width: fit-content;
transition:
opacity 0.2s,
transform 0.2s;
}
.hpf__cta:hover {
opacity: 0.88;
transform: translateY(-1px);
}
.hpf__cta-arrow {
flex-shrink: 0;
transition: transform 0.2s;
}
.hpf__cta:hover .hpf__cta-arrow {
transform: translateX(3px);
}
/* Right column: stacked cards */
.hpf__right {
display: flex;
flex-direction: column;
gap: 1.25rem;
position: relative;
}
/* Card */
@keyframes hpf-cardin {
from {
opacity: 0;
transform: translateY(32px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hpf__card {
border-radius: calc(var(--radius) * 1.5);
overflow: hidden;
background: #161616;
border: 1px solid rgba(255, 255, 255, 0.07);
animation: hpf-cardin 0.65s cubic-bezier(0.16, 1, 0.3, 1) both;
transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform;
}
.hpf__card:hover {
transform: translateY(-4px);
}
/* First card is larger */
.hpf__card[data-index="0"] {
flex: 0 0 auto;
}
.hpf__card-link {
display: block;
text-decoration: none;
color: inherit;
}
.hpf__card-img-wrap {
width: 100%;
overflow: hidden;
aspect-ratio: 16 / 7;
background: #222;
}
.hpf__card[data-index="0"] .hpf__card-img-wrap {
aspect-ratio: 16 / 8;
}
.hpf__card-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.hpf__card:hover .hpf__card-img {
transform: scale(1.04);
}
.hpf__card-body {
padding: 1.125rem 1.375rem 1.25rem;
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.hpf__card-cat {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-accent);
}
.hpf__card-title {
font-size: clamp(1rem, 1.5vw, 1.1875rem);
font-weight: 700;
color: #fff;
margin: 0;
line-height: 1.3;
}
/* Mobile */
@media (max-width: 900px) {
.hpf__container {
grid-template-columns: 1fr;
}
.hpf__right {
/* On mobile: show cards side by side and scroll horizontally */
flex-direction: row;
overflow-x: auto;
gap: 1rem;
padding-bottom: 0.5rem;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
}
.hpf__card {
min-width: min(280px, 78vw);
scroll-snap-align: start;
}
.hpf__card-img-wrap,
.hpf__card[data-index="0"] .hpf__card-img-wrap {
aspect-ratio: 4 / 3;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
headline * | string | — | H1 — gebruik <em> voor accent |
cases * | { title: string; category: string; image: string; href?: string }[] | — | Case cards rechts (2-3 ideaal) |
eyebrow | string | — | Klein label boven headline |
sub | string | — | Ondertitel |
ctaLabel | string | — | Primaire CTA |
* = verplicht