ContentQuote content
Full-width citaat als content break. Decoratief aanhalingsteken, auteur met avatar. 4 achtergrondvarianten.
src/components/content/ContentQuote.astro
---
interface Props {
quote: string;
author?: string;
role?: string;
avatar?: string;
accentColor?: string;
size?: 'md' | 'lg' | 'xl';
centered?: boolean;
bg?: 'white' | 'light' | 'accent' | 'dark';
}
const {
quote,
author,
role,
avatar,
accentColor,
size = 'lg',
centered = false,
bg = 'white',
} = Astro.props;
const bgClass = `cqu__section--bg-${bg}`;
const sizeClass = `cqu__quote--size-${size}`;
const centeredClass = centered ? 'cqu__section--centered' : '';
// Override accent color via inline style if provided
const accentStyle = accentColor ? `--cqu-accent: ${accentColor};` : '';
---
<section class={`cqu__section ${bgClass} ${centeredClass}`} style={accentStyle}>
<div class="cqu__inner cqu-reveal">
<!-- Decorative quotation mark -->
<span class="cqu__mark" aria-hidden="true">“</span>
<blockquote class={`cqu__quote ${sizeClass}`}>
<p class="cqu__text">{quote}</p>
{(author || role || avatar) && (
<footer class="cqu__author">
{avatar && (
<img
src={avatar}
alt={author ?? ''}
class="cqu__avatar"
loading="lazy"
decoding="async"
/>
)}
<div class="cqu__author-meta">
{author && <span class="cqu__author-name">{author}</span>}
{author && role && <span class="cqu__dot" aria-hidden="true">·</span>}
{role && <span class="cqu__author-role">{role}</span>}
</div>
</footer>
)}
</blockquote>
</div>
</section>
<script>
const reveals = document.querySelectorAll<HTMLElement>('.cqu-reveal');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
(entry.target as HTMLElement).classList.add('cqu-reveal--visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.2 }
);
reveals.forEach((el) => observer.observe(el));
</script>
<style>
:root {
--color-primary: #0a0a0a;
--color-accent: #6366f1;
--color-bg: #fff;
--color-muted: #6b7280;
--radius: 0.5rem;
/* Internal override slot */
--cqu-accent: var(--color-accent);
}
/* Section base */
.cqu__section {
position: relative;
padding: clamp(4rem, 10vw, 8rem) clamp(1rem, 5vw, 2rem);
overflow: hidden;
}
/* Background variants */
.cqu__section--bg-white {
background: var(--color-bg);
color: var(--color-primary);
}
.cqu__section--bg-light {
background: rgba(0, 0, 0, 0.02);
color: var(--color-primary);
}
.cqu__section--bg-accent {
background: var(--color-accent);
color: #fff;
}
.cqu__section--bg-dark {
background: #0a0a0a;
color: #fff;
}
/* Inner wrapper */
.cqu__inner {
position: relative;
max-width: 820px;
margin: 0 auto;
}
.cqu__section--centered .cqu__inner {
text-align: center;
}
.cqu__section--centered .cqu__author {
justify-content: center;
}
.cqu__section--centered .cqu__mark {
left: 50%;
transform: translateX(-50%);
}
/* Decorative quote mark */
.cqu__mark {
position: absolute;
top: -0.15em;
left: -0.05em;
font-size: clamp(6rem, 15vw, 12rem);
line-height: 1;
font-family: Georgia, 'Times New Roman', serif;
font-weight: 900;
color: var(--cqu-accent);
opacity: 0.15;
pointer-events: none;
user-select: none;
z-index: 0;
}
.cqu__section--bg-accent .cqu__mark,
.cqu__section--bg-dark .cqu__mark {
color: #fff;
}
/* Blockquote */
.cqu__quote {
position: relative;
z-index: 1;
margin: 0;
padding: 0;
border: none;
}
/* Quote text */
.cqu__text {
font-style: italic;
font-weight: 600;
line-height: 1.55;
margin: 0 0 2rem;
}
/* Size variants */
.cqu__quote--size-md .cqu__text {
font-size: clamp(1.125rem, 2vw, 1.5rem);
}
.cqu__quote--size-lg .cqu__text {
font-size: clamp(1.25rem, 2.5vw, 2rem);
}
.cqu__quote--size-xl .cqu__text {
font-size: clamp(1.5rem, 3.5vw, 2.5rem);
}
/* Light BG: muted color for text */
.cqu__section--bg-white .cqu__text,
.cqu__section--bg-light .cqu__text {
color: var(--color-primary);
}
.cqu__section--bg-accent .cqu__text,
.cqu__section--bg-dark .cqu__text {
color: #fff;
}
/* Author row */
.cqu__author {
display: flex;
align-items: center;
gap: 0.875rem;
}
.cqu__avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.cqu__author-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.4rem;
font-size: 0.9375rem;
}
.cqu__author-name {
font-weight: 700;
}
.cqu__section--bg-white .cqu__author-name,
.cqu__section--bg-light .cqu__author-name {
color: var(--color-primary);
}
.cqu__section--bg-accent .cqu__author-name,
.cqu__section--bg-dark .cqu__author-name {
color: #fff;
}
.cqu__dot {
color: var(--color-muted);
}
.cqu__section--bg-accent .cqu__dot,
.cqu__section--bg-dark .cqu__dot {
color: rgba(255, 255, 255, 0.5);
}
.cqu__author-role {
color: var(--color-muted);
}
.cqu__section--bg-accent .cqu__author-role,
.cqu__section--bg-dark .cqu__author-role {
color: rgba(255, 255, 255, 0.65);
}
/* Scroll reveal — slides up + fades in */
.cqu-reveal {
opacity: 0;
transform: translateY(2rem);
transition: opacity 0.75s ease, transform 0.75s ease;
}
.cqu-reveal--visible {
opacity: 1;
transform: translateY(0);
}
@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 |
bg | 'white' | 'light' | 'accent' | 'dark' | 'white' | Achtergrond variant |
size | 'md' | 'lg' | 'xl' | 'md' | Citaat grootte |
* = verplicht