PullQuote text
Editoriaal citaat als content break. 4 varianten: editorial, minimal, accent, bordered. Avatar + auteur.
src/components/text/PullQuote.astro
---
interface Props {
quote: string;
author?: string;
role?: string;
avatar?: string;
variant?: 'editorial' | 'minimal' | 'accent' | 'bordered';
size?: 'md' | 'lg';
align?: 'center' | 'left';
}
const {
quote,
author,
role,
avatar,
variant = 'editorial',
size = 'md',
align = 'center',
} = Astro.props;
---
<section class={`pq__section pq__variant-${variant} pq__size-${size} pq__align-${align}`}>
<div class="pq__inner">
{variant === 'editorial' && (
<span class="pq__bg-quote" aria-hidden="true">“</span>
)}
<blockquote class="pq__quote">
<p class="pq__text">{quote}</p>
{(author || role) && (
<footer class="pq__author">
{avatar && (
<img class="pq__avatar" src={avatar} alt={author ?? ''} width="40" height="40" loading="lazy" />
)}
<div class="pq__author-meta">
{author && <span class="pq__author-name">{author}</span>}
{role && <span class="pq__author-role">{role}</span>}
</div>
</footer>
)}
</blockquote>
</div>
</section>
<script>
const sections = document.querySelectorAll<HTMLElement>('.pq__section');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('pq--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;
}
@keyframes pq-fade-up {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.pq__section {
padding: 5rem 1.5rem;
opacity: 0;
transform: translateY(24px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.pq__section.pq--visible {
opacity: 1;
transform: translateY(0);
}
.pq__inner {
position: relative;
max-width: 760px;
margin: 0 auto;
}
.pq__align-left .pq__inner {
margin-left: 0;
}
.pq__quote {
margin: 0;
position: relative;
z-index: 1;
}
.pq__text {
margin: 0;
font-weight: 600;
line-height: 1.4;
}
/* Size */
.pq__size-md .pq__text {
font-size: clamp(1.5rem, 3vw, 2.5rem);
}
.pq__size-lg .pq__text {
font-size: clamp(2rem, 4vw, 3.25rem);
}
/* Align */
.pq__align-center {
text-align: center;
}
.pq__align-center .pq__author {
justify-content: center;
}
.pq__align-left {
text-align: left;
}
/* --- Variant: editorial --- */
.pq__variant-editorial .pq__text {
font-style: italic;
}
.pq__bg-quote {
position: absolute;
top: -0.5em;
left: -0.1em;
font-size: 12rem;
font-weight: 900;
color: var(--color-accent);
opacity: 0.06;
line-height: 1;
pointer-events: none;
user-select: none;
z-index: 0;
}
/* --- Variant: minimal --- */
.pq__variant-minimal .pq__inner {
padding-left: 1.5rem;
border-left: 3px solid var(--color-accent);
}
.pq__variant-minimal .pq__text {
font-size: clamp(1.125rem, 2vw, 1.5rem);
font-style: normal;
color: var(--color-primary);
}
/* --- Variant: accent --- */
.pq__variant-accent {
background: var(--color-accent);
}
.pq__variant-accent .pq__text,
.pq__variant-accent .pq__author-name {
color: #fff;
}
.pq__variant-accent .pq__author-role {
color: rgba(255, 255, 255, 0.75);
}
.pq__variant-accent .pq__text {
font-style: italic;
}
/* --- Variant: bordered --- */
.pq__variant-bordered .pq__inner {
padding: 2rem 0;
border-top: 3px solid var(--color-primary);
border-bottom: 3px solid var(--color-primary);
}
/* Author row */
.pq__author {
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 1.5rem;
}
.pq__avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.pq__author-meta {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.pq__author-name {
font-size: 0.9375rem;
font-weight: 700;
color: var(--color-primary);
}
.pq__author-role {
font-size: 0.8125rem;
color: var(--color-muted);
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
quote * | string | — | Het citaat |
author | string | — | Auteur naam |
role | string | — | Functie/rol |
avatar | string | — | Avatar afbeelding URL |
variant | 'editorial' | 'minimal' | 'accent' | 'bordered' | 'editorial' | Visuele stijl |
size | 'md' | 'lg' | 'md' | Citaat grootte |
* = verplicht