Zoeken...  ⌘K GitHub

HeroProduct Hero

Product-hero: tekstkolom links (label-pill, kop met accent, sub, feature-lijst, sterren-rating, twee CTA's) en rechts een browser-frame met screenshot.

/hero-product
src/components/hero/HeroProduct.astro
---
/**
 * HeroProduct
 * Product-hero: tekstkolom links (label-pill, kop met cursief accent, sub,
 * feature-lijst met vinkjes, sterren-rating, twee CTA's) en rechts een
 * browser-frame met productscreenshot. Content ligt op de pagina-rail.
 *
 * Props: zie interface. Alle copy is prop-driven.
 */
interface Props {
  label?: string;
  headline: string;
  accent?: string;
  sub?: string;
  features?: string[];
  rating?: number;
  reviewCount?: number;
  primaryCta?: { label: string; href: string };
  ghostCta?: { label: string; href: string };
  screenshot?: { src: string; alt?: string };
}

const {
  label,
  headline,
  accent,
  sub,
  features = [],
  rating,
  reviewCount,
  primaryCta,
  ghostCta,
  screenshot,
} = Astro.props;

const fullStars = rating ? Math.floor(rating) : 0;
const hasHalf = rating ? rating - fullStars >= 0.25 && rating - fullStars < 0.75 : false;
const starPath = 'M8 1.25l1.854 3.756 4.146.603-3 2.923.708 4.128L8 10.513l-3.708 1.95.708-4.128-3-2.923 4.146-.603L8 1.25z';
---

<section class:list={['bl-section', 'hpd__root']} aria-label="Hero">
  <div class="bl-inner hpd__container">
    <div class="hpd__left">
      {label && <span class="hpd__label">{label}</span>}
      <h1 class="hpd__headline">{headline}{accent && <em> {accent}</em>}</h1>
      {sub && <p class="hpd__sub">{sub}</p>}
      {features.length > 0 && (
        <ul class="hpd__features" aria-label="Features">
          {features.map((feature) => (
            <li class="hpd__feature-item">
              <span class="hpd__check" aria-hidden="true">
                <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M2.5 7.5L5.5 10.5L11.5 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
              </span>
              {feature}
            </li>
          ))}
        </ul>
      )}
      {rating && (
        <div class="hpd__rating" aria-label={`Beoordeling: ${rating} van 5${reviewCount ? `, ${reviewCount} reviews` : ''}`}>
          <span class="hpd__stars" aria-hidden="true">
            {Array.from({ length: fullStars }).map(() => (
              <span class="hpd__star hpd__star--full"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d={starPath}></path></svg></span>
            ))}
            {hasHalf && (
              <span class="hpd__star hpd__star--half"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><defs><linearGradient id="hpd-half"><stop offset="50%" stop-color="currentColor"></stop><stop offset="50%" stop-color="transparent"></stop></linearGradient></defs><path d={starPath} fill="url(#hpd-half)" stroke="currentColor" stroke-width="0.5"></path></svg></span>
            )}
          </span>
          <span class="hpd__rating-text"><strong>{rating}</strong>{reviewCount && <>&nbsp;({reviewCount} reviews)</>}</span>
        </div>
      )}
      {(primaryCta || ghostCta) && (
        <div class="hpd__actions">
          {primaryCta && <a href={primaryCta.href} class="hpd__cta hpd__cta--primary">{primaryCta.label}</a>}
          {ghostCta && <a href={ghostCta.href} class="hpd__cta hpd__cta--ghost">{ghostCta.label}</a>}
        </div>
      )}
    </div>
    {screenshot && (
      <div class="hpd__right">
        <div class="hpd__frame" aria-hidden="true">
          <div class="hpd__frame-chrome">
            <span class="hpd__dot hpd__dot--red"></span>
            <span class="hpd__dot hpd__dot--yellow"></span>
            <span class="hpd__dot hpd__dot--green"></span>
            <div class="hpd__frame-url" aria-hidden="true"><span class="hpd__frame-url-bar"></span></div>
          </div>
          <div class="hpd__frame-screen">
            <img data-allow-img src={screenshot.src} alt={screenshot.alt ?? ''} class="hpd__screenshot" loading="eager" fetchpriority="high" decoding="async" />
          </div>
        </div>
      </div>
    )}
  </div>
</section>

<style>
.hpd__root{background:var(--color-bg, #fff);min-height:100svh;display:flex;align-items:center;overflow:hidden}
.hpd__container{display:grid;grid-template-columns:1fr 1fr;gap:clamp(3rem, 6vw, 5rem);align-items:center}
.hpd__left{display:flex;flex-direction:column;gap:1.5rem}
.hpd__label{display:inline-block;font-size:.8125rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;background:color-mix(in srgb,var(--color-accent, #6366f1) 12%,transparent);color:var(--color-accent, #6366f1);padding:.3125rem .875rem;border-radius:999px;border:1px solid color-mix(in srgb,var(--color-accent, #6366f1) 30%,transparent);width:fit-content}
.hpd__headline{font-size:clamp(2.5rem,4vw,4.5rem);font-weight:900;line-height:1.06;letter-spacing:-.03em;color:var(--color-primary, #0a0a0a);margin:0}
.hpd__headline em{font-style:normal;color:var(--color-accent, #6366f1)}
.hpd__sub{font-size:clamp(1rem,1.3vw,1.125rem);color:var(--color-muted, #6b7280);line-height:1.7;margin:0;max-width:46ch}
.hpd__features{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.625rem}
.hpd__feature-item{display:flex;align-items:center;gap:.625rem;font-size:.9375rem;color:var(--color-primary, #0a0a0a);font-weight:500}
.hpd__check{display:inline-flex;align-items:center;justify-content:center;width:1.375rem;height:1.375rem;border-radius:999px;background:color-mix(in srgb,var(--color-accent, #6366f1) 14%,transparent);color:var(--color-accent, #6366f1);flex-shrink:0}
.hpd__rating{display:flex;align-items:center;gap:.5rem}
.hpd__stars{display:flex;gap:2px;color:#f59e0b}
.hpd__star{display:inline-flex;align-items:center}
.hpd__rating-text{font-size:.9375rem;color:#4b5563}
.hpd__rating-text strong{color:var(--color-primary, #0a0a0a)}
.hpd__actions{display:flex;flex-wrap:wrap;gap:.875rem;align-items:center;margin-top:.5rem}
.hpd__cta{display:inline-flex;align-items:center;justify-content:center;padding:.875rem 1.875rem;border-radius:var(--radius, .5rem);font-size:.9375rem;font-weight:600;text-decoration:none;white-space:nowrap;transition:opacity .2s,transform .2s}
.hpd__cta:hover{opacity:.88;transform:translateY(-1px)}
.hpd__cta--primary{background:var(--color-accent, #6366f1);color:#fff}
.hpd__cta--ghost{background:transparent;color:var(--color-primary, #0a0a0a);border:1.5px solid rgba(10,10,10,.18)}
.hpd__cta--ghost:hover{border-color:#0a0a0a66}
.hpd__right{display:flex;align-items:center;justify-content:center}
.hpd__frame{width:100%;max-width:640px;border-radius:calc(var(--radius, .5rem) * 1.75);overflow:hidden;background:#1a1a1a;border:1px solid rgba(255,255,255,.12);box-shadow:0 50px 100px -20px #00000040,0 20px 40px -10px #0003,0 0 0 1px #0000000d;transform:perspective(1200px) rotateY(-8deg) rotateX(2deg);transform-origin:center center;transition:transform .5s cubic-bezier(.16,1,.3,1)}
.hpd__frame:hover{transform:perspective(1200px) rotateY(-2deg) rotateX(.5deg)}
.hpd__frame-chrome{display:flex;align-items:center;gap:.5rem;padding:.625rem 1rem;background:#252525;border-bottom:1px solid rgba(255,255,255,.06)}
.hpd__dot{width:.6875rem;height:.6875rem;border-radius:50%;flex-shrink:0}
.hpd__dot--red{background:#ff5f57}
.hpd__dot--yellow{background:#febc2e}
.hpd__dot--green{background:#28c840}
.hpd__frame-url{flex:1;margin-left:.75rem}
.hpd__frame-url-bar{display:block;height:.4375rem;border-radius:999px;background:#ffffff1a;max-width:220px}
.hpd__frame-screen{width:100%;overflow:hidden;max-height:460px;background:#f0f0f0}
.hpd__screenshot{width:100%;height:100%;object-fit:cover;object-position:top;display:block}
@media (max-width:768px){
  .hpd__container{grid-template-columns:1fr}
  .hpd__frame{transform:none;max-width:100%}
  .hpd__frame:hover{transform:none}
  .hpd__frame-screen{max-height:320px}
}
</style>

Props

Prop Type Default Beschrijving
label string - Label-pill boven de kop
headline * string - H1-kop
accent string - Cursief accent direct na de kop
sub string - Ondertitel
features string[] - Feature-lijst met vinkjes
rating number - Sterren-rating (0-5)
reviewCount number - Aantal reviews
primaryCta { label: string; href: string } - Primaire knop
ghostCta { label: string; href: string } - Ghost-knop
screenshot { src: string; alt?: string } - Screenshot in browser-frame

* = verplicht