Zoeken...  ⌘K GitHub

HeadingWithLine heading

Headline geflankeerd door animerende horizontale regels (scaleX op scroll). Links, rechts of beide kanten.

/heading-with-line
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