HeadingWithLine heading
Headline geflankeerd door animerende horizontale regels (scaleX op scroll). Links, rechts of beide kanten.
src/components/heading/HeadingWithLine.astro
---
interface Props {
eyebrow?: string;
headline: string;
sub?: string;
linePosition?: 'left' | 'right' | 'both';
lineColor?: string;
align?: 'center' | 'left';
}
const {
eyebrow,
headline,
sub,
linePosition = 'both',
lineColor,
align = 'center',
} = Astro.props;
const lineStyle = lineColor ? `--hwl-line-color: ${lineColor};` : '';
---
<section class={`hwl__section hwl__align-${align} hwl__pos-${linePosition}`} style={lineStyle}>
{eyebrow && <p class="hwl__eyebrow">{eyebrow}</p>}
<div class="hwl__headline-row">
{(linePosition === 'left' || linePosition === 'both') && (
<span class="hwl__rule hwl__rule--left" aria-hidden="true"></span>
)}
<h2 class="hwl__headline" set:html={headline} />
{(linePosition === 'right' || linePosition === 'both') && (
<span class="hwl__rule hwl__rule--right" aria-hidden="true"></span>
)}
</div>
{sub && <p class="hwl__sub">{sub}</p>}
</section>
<script>
const sections = document.querySelectorAll<HTMLElement>('.hwl__section');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('hwl--visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.15 }
);
sections.forEach((el) => observer.observe(el));
</script>
<style>
:root {
--color-primary: #0a0a0a;
--color-accent: #6366f1;
--color-bg: #fff;
--color-muted: #6b7280;
--radius: 0.5rem;
}
.hwl__section {
padding: 5rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.hwl__align-center {
align-items: center;
text-align: center;
}
.hwl__align-left {
align-items: flex-start;
text-align: left;
}
.hwl__eyebrow {
margin: 0;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-accent);
}
.hwl__headline-row {
display: flex;
align-items: center;
gap: 1.25rem;
width: 100%;
}
.hwl__align-center .hwl__headline-row {
justify-content: center;
}
.hwl__align-left .hwl__headline-row {
justify-content: flex-start;
}
.hwl__rule {
display: block;
height: 2px;
background: var(--hwl-line-color, var(--color-accent));
flex: 1;
max-width: 120px;
transform: scaleX(0);
transform-origin: left center;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.hwl__rule--right {
transform-origin: right center;
}
.hwl--visible .hwl__rule {
transform: scaleX(1);
}
.hwl__headline {
margin: 0;
font-size: clamp(2rem, 4vw, 3.5rem);
font-weight: 800;
letter-spacing: -0.02em;
color: var(--color-primary);
line-height: 1.1;
white-space: nowrap;
}
.hwl__headline em {
font-style: italic;
color: var(--color-accent);
}
.hwl__sub {
margin: 0;
font-size: 1.125rem;
line-height: 1.7;
color: var(--color-muted);
max-width: 580px;
}
/* Single-side alignment overrides */
.hwl__pos-left .hwl__rule--left {
transform-origin: left center;
}
.hwl__pos-right .hwl__rule--right {
transform-origin: right center;
}
@media (max-width: 640px) {
.hwl__headline {
white-space: normal;
}
.hwl__rule {
max-width: 60px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
headline * | string | — | Sectie headline — gebruik <em> voor accent |
linePosition | 'left' | 'right' | 'both' | 'both' | Positie van de animerende lijn(en) |
eyebrow | string | — | Label boven headline |
sub | string | — | Ondertekst |
* = verplicht