Herausgestelltes Zitat
Ein Satz aus dem Text, groß gesetzt. Neben den Absätzen, mittig, mit Textmarker oder neben einem Porträt.
Ein Satz, groß gesetzt
Ein herausgestelltes Zitat hebt einen Satz aus dem Text, damit überfliegende Leser anhalten und die Kernaussage mitnehmen. Die vier Fassungen unterscheiden sich darin, wie laut sie sind, vom leisen Einschub bis zum eigenen großen Band.
„Im Text“ rückt am breiten Bildschirm in den Rand neben die Absätze und steht am Handy mit einer Linie dazwischen. „Mittig“ steht allein und groß auf ruhigem Band und baut sich Wort für Wort unter einem gezeichneten Anführungszeichen auf. „Mit Textmarker“ zieht beim Hereinscrollen einen Marker über die Kernaussage und bietet einen Knopf zum Kopieren. „Mit Porträt“ stellt die sprechende Person mit Name, Rolle und Anlass neben das Zitat.
Wähl einen Satz, der für sich allein trägt, Leser sehen ihn vor dem Absatz drumherum. Nimm ein Zitat pro Artikel, in langen Texten höchstens zwei. Die Fassung mit Porträt passt zu Interviews und Kundengeschichten.
Details
Was Herausgestelltes Zitat mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Artikel-Bausteine
- Bereich
- Content
- Variants
- 4
- Version
- 1.0.0 · seit 1.2.0
- Datei
- components/ui/pull-quote.tsx
- Bringt mit
- utilskiticonsectionrevealmotionword-reveal
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 3 eigene Texte
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle PullQuoteProps aus components/ui/pull-quote.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalPullQuoteVariantinsetcenteredhighlightportrait
quotePflichtstringThe quote, `*words*` mark a phrase
citeoptionalstringWho said it
roleoptionalstringRole or place of the speaker
sourceoptionalstringWhere it was said, e.g. an interview or a book
eyebrowoptionalReact.ReactNodebodyoptionalstring[]inset: the paragraphs around the quote
atoptionalnumberinset: the quote follows this paragraph (0 based), default 0
imageoptionalReact.ReactNodeportrait: the picture of the speaker
Eingebaute Texte
Diese Texte schreibt Herausgestelltes Zitat selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 3 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| copy | Englisch Copy quote | Deutsch Zitat kopieren |
| copied | Englisch Copied | Deutsch Kopiert |
| quote | Englisch Quote | Deutsch Zitat |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-live, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle p.
- Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 235 Zeilen
'use client';
import { useState } from 'react';
import { cn, useInView, useTexts } from '@/lib/kit';
import { Icon } from '@/components/ui/icon';
import { Eyebrow, Section } from '@/components/ui/section';
import { Reveal } from '@/components/ui/reveal';
import { Parallax } from '@/components/ui/motion';
import { WordReveal } from '@/components/ui/word-reveal';
/*
* One sentence lifted out of a text and set large, so a reader who skims
* still takes it with them.
*
* inset beside the paragraphs, leaning into the margin on wide
* screens, between them with a rule on phones
* centered alone and large, rising word by word under a drawn mark
* highlight a phrase that a marker sweeps over, with a copy button
* portrait the speaker's picture beside the quote, with the source
*
* `*words*` in the quote mark a phrase: accent in inset and centered, the
* marker in highlight.
*/
export type PullQuoteVariant = 'inset' | 'centered' | 'highlight' | 'portrait';
const TEXTS = {
en: { copy: 'Copy quote', copied: 'Copied', quote: 'Quote' },
de: { copy: 'Zitat kopieren', copied: 'Kopiert', quote: 'Zitat' },
};
type Texts = (typeof TEXTS)['en'];
export interface PullQuoteProps {
variant?: PullQuoteVariant;
/** The quote, `*words*` mark a phrase */
quote: string;
/** Who said it */
cite?: string;
/** Role or place of the speaker */
role?: string;
/** Where it was said, e.g. an interview or a book */
source?: string;
eyebrow?: React.ReactNode;
/** inset: the paragraphs around the quote */
body?: string[];
/** inset: the quote follows this paragraph (0 based), default 0 */
at?: number;
/** portrait: the picture of the speaker */
image?: React.ReactNode;
labels?: Partial<Texts>;
className?: string;
}
/** Splits `*phrase*` out of the text, the phrase goes through `mark` */
function parts(text: string, mark: (s: string, i: number) => React.ReactNode) {
return text.split(/\*([^*]+)\*/g).map((s, i) => (i % 2 ? mark(s, i) : s));
}
const plain = (text: string) => text.replace(/\*([^*]+)\*/g, '$1');
/** The drawn opening mark, in a calm tint of the accent */
function Mark({ className }: { className?: string }) {
return (
<svg aria-hidden viewBox="0 0 64 48" className={cn('h-8 w-auto text-[color-mix(in_oklab,var(--primary)_70%,var(--border-strong))]', className)}>
<path d="M0 48V28C0 12 8 2 24 0l3 7C18 10 14 16 14 24h12v24H0Zm37 0V28C37 12 45 2 61 0l3 7c-9 3-13 9-13 17h12v24H37Z" fill="currentColor" />
</svg>
);
}
/** The source of the quote, a figcaption unless it sits deeper inside one () */
function Cite({ cite, role, source, className, as: Tag = 'figcaption' }: Pick<PullQuoteProps, 'cite' | 'role' | 'source'> & { className?: string; as?: 'figcaption' | 'div' }) {
if (!cite && !role && !source) return null;
return (
<Tag className={cn('text-[14px] leading-snug', className)}>
{cite && <span className="block font-semibold text-foreground">{cite}</span>}
{(role || source) && <span className="mt-1 block text-muted-foreground">{[role, source].filter(Boolean).join(' · ')}</span>}
</Tag>
);
}
const PROSE = 'text-pretty text-[17px] leading-[1.75] text-[color-mix(in_oklab,var(--foreground)_86%,var(--background))] sm:text-[18px]';
/* ------------------------------------------------------------------ */
function Inset(p: PullQuoteProps) {
const body = p.body ?? [];
const at = Math.min(Math.max(0, p.at ?? 0), Math.max(0, body.length - 1));
const accent = (s: string, i: number) => (
<span key={i} className="kit-accent-text">
{s}
</span>
);
const quote = (
<Reveal effect="rise" className="my-10 lg:float-right lg:my-2 lg:-mr-56 lg:ml-10 lg:w-[19rem] xl:-mr-64 xl:w-[21rem]">
<figure className="border-l-2 border-primary pl-6 lg:border-l-0 lg:border-t-2 lg:pl-0 lg:pt-6">
<blockquote className="text-balance text-[clamp(1.375rem,1.2rem+0.6vw,1.625rem)] font-medium leading-[1.25] tracking-[-0.025em] text-foreground">
<p>{parts(p.quote, accent)}</p>
</blockquote>
<Cite cite={p.cite} role={p.role} source={p.source} className="mt-5" />
</figure>
</Reveal>
);
return (
<article className="mx-auto max-w-[36rem] lg:mr-[max(16rem,calc(50%-12rem))] lg:ml-auto xl:mr-[max(18rem,calc(50%-14rem))]">
{p.eyebrow && <Eyebrow>{p.eyebrow}</Eyebrow>}
<div className={cn(PROSE, '[&>p+p]:mt-6')}>
{body.length === 0 && quote}
{body.map((b, i) => (
<div key={i} className="contents">
<p className={cn(i > 0 && 'mt-6')}>{b}</p>
{i === at && quote}
</div>
))}
</div>
<div className="clear-both" />
</article>
);
}
function Centered(p: PullQuoteProps) {
const text = plain(p.quote);
return (
<figure className="mx-auto max-w-[52rem] text-center">
<Parallax speed={36} className="mx-auto w-fit">
<Mark className="mx-auto mb-8 h-9 sm:h-11" />
</Parallax>
{p.eyebrow && <p className="sr-only">{p.eyebrow}</p>}
<blockquote>
<WordReveal as="p" text={text} stagger={40} duration={700} className="text-balance text-[clamp(1.75rem,1.1rem+2.4vw,3rem)] font-medium leading-[1.14] tracking-[-0.035em] text-foreground" />
</blockquote>
<figcaption className="mt-10">
<Reveal delay={250} className="flex flex-col items-center gap-4">
<span aria-hidden className="h-px w-12 bg-border-strong" />
<Cite as="div" cite={p.cite} role={p.role} source={p.source} className="text-[15px]" />
</Reveal>
</figcaption>
</figure>
);
}
function Highlight(p: PullQuoteProps & { t: Texts }) {
const [ref, inView] = useInView<HTMLQuoteElement>('0px 0px -20% 0px');
const [copied, setCopied] = useState(false);
const copy = async () => {
try {
await navigator.clipboard.writeText(`„${plain(p.quote)}“${p.cite ? ` (${p.cite})` : ''}`);
setCopied(true);
window.setTimeout(() => setCopied(false), 1800);
} catch {
/* the browser refused, nothing to show */
}
};
const marker = (s: string, i: number) => (
<mark
key={i}
className={cn(
'rounded-[0.2em] bg-transparent bg-no-repeat px-[0.08em] text-foreground [box-decoration-break:clone] transition-[background-size] delay-300 duration-1000 ease-[var(--ease-kit)] motion-reduce:transition-none',
inView ? 'bg-[length:100%_100%]' : 'bg-[length:0%_100%]',
)}
style={{ backgroundImage: 'linear-gradient(color-mix(in oklab, var(--primary) 28%, transparent), color-mix(in oklab, var(--primary) 28%, transparent))' }}
>
{s}
</mark>
);
return (
<div className="mx-auto max-w-[56rem]">
<div className="grid grid-cols-[minmax(0,1fr)] gap-8 sm:grid-cols-[auto_minmax(0,1fr)] sm:gap-10">
<Mark className="h-7 sm:mt-2 sm:h-9" />
<figure>
{p.eyebrow && <Eyebrow>{p.eyebrow}</Eyebrow>}
<blockquote ref={ref}>
<p className="text-pretty text-[clamp(1.5rem,1.15rem+1.4vw,2.375rem)] font-medium leading-[1.3] tracking-[-0.03em] text-[color-mix(in_oklab,var(--foreground)_80%,var(--background))]">{parts(p.quote, marker)}</p>
</blockquote>
<figcaption className="mt-8 flex flex-col gap-5 border-t border-border pt-6 sm:flex-row sm:items-center sm:justify-between">
<Cite as="div" cite={p.cite} role={p.role} source={p.source} />
<button
type="button"
onClick={copy}
className="inline-flex min-h-11 cursor-pointer items-center gap-2 self-start rounded-lg border border-border bg-background px-4 text-[13px] font-medium text-foreground shadow-[var(--shadow-sm)] transition-colors hover:bg-accent outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-ring sm:self-auto"
>
<Icon name={copied ? 'check' : 'copy'} size={15} className={cn(copied && 'text-success-foreground')} />
<span aria-live="polite">{copied ? p.t.copied : p.t.copy}</span>
</button>
</figcaption>
</figure>
</div>
</div>
);
}
function Portrait(p: PullQuoteProps) {
const accent = (s: string, i: number) => (
<span key={i} className="kit-accent-text">
{s}
</span>
);
return (
<div className="grid grid-cols-[minmax(0,1fr)] items-center gap-10 md:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] md:gap-12 lg:gap-20">
<Reveal effect="scale" className="relative mx-auto w-full max-w-[22rem] md:max-w-none">
<div aria-hidden className="absolute -inset-4 -z-10 rounded-[2rem] bg-[radial-gradient(closest-side,var(--glow),transparent)] blur-2xl" />
<div data-clip-ok className="relative aspect-[4/5] overflow-hidden rounded-2xl border border-border bg-muted shadow-[var(--shadow-lg)] [&>*]:size-full">{p.image}</div>
<span aria-hidden className="absolute -bottom-5 -right-3 flex size-14 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-[var(--shadow-lg)] sm:-right-5">
<Mark className="h-5 text-primary-foreground" />
</span>
</Reveal>
<figure>
{p.eyebrow && <Eyebrow>{p.eyebrow}</Eyebrow>}
<blockquote>
<Reveal effect="blur">
<p className="text-balance text-[clamp(1.625rem,1.15rem+1.7vw,2.625rem)] font-medium leading-[1.18] tracking-[-0.03em] text-foreground">{parts(p.quote, accent)}</p>
</Reveal>
</blockquote>
<figcaption>
<Reveal delay={200}>
<Cite as="div" cite={p.cite} role={p.role} source={p.source} className="mt-8 border-l-2 border-primary pl-4 text-[15px]" />
</Reveal>
</figcaption>
</figure>
</div>
);
}
/**
* A pull quote: inset beside the text, centered and large, with a sweeping
* marker and copy button, or beside a portrait of the speaker.
* <PullQuote variant="inset" quote="We keep *one day* free." cite="Mira Kade" body={['…', '…']} />
*/
export function PullQuote(props: PullQuoteProps) {
const { variant = 'inset', className, labels } = props;
const t = useTexts(TEXTS, labels);
return (
<Section className={cn(variant === 'centered' && 'border-y border-border bg-surface', className)}>
{variant === 'centered' ? <Centered {...props} /> : variant === 'highlight' ? <Highlight {...props} t={t} /> : variant === 'portrait' ? <Portrait {...props} /> : <Inset {...props} />}
</Section>
);
}Häufige Fragen
Wie installiere ich Herausgestelltes Zitat?
Mit aiw add @ai-website/pull-quote. Der Befehl legt die Datei components/ui/pull-quote.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, section, reveal, motion und word-reveal. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Herausgestelltes Zitat?
4 Varianten, Im Text, Mittig, Mit Textmarker und Mit Porträt. Du wählst sie mit der Prop variant, zum Beispiel variant="centered".
Kann ich die Texte von Herausgestelltes Zitat übersetzen?
Ja. Herausgestelltes Zitat bringt 3 eigene Texte auf Deutsch und Englisch mit, etwa „Zitat kopieren“, „Kopiert“ und „Zitat“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Herausgestelltes Zitat einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Herausgestelltes Zitat mit aiw_components und installieren sie mit aiw_add @ai-website/pull-quote. Nenn im Prompt die Variante, etwa variant="inset", dann schreibt der Assistent deine Inhalte direkt in die Props.