Quote
Blockquotes with icon, citation, avatar and labels.
On this page
Quote renders a blockquote with an optional quote mark, the quoted text, and a citation with an avatar and two labels. Every part is a separate component, so you compose only what a testimonial or pull quote needs.
We replaced three style guides with one token file. Designers change a color in Figma and the site follows after the next sync.
Maya Lindqvist
Head of Design, Northwind
import {
Paragraph,
Quote,
QuoteAvatar,
QuoteCite,
QuoteIcon,
QuoteLabels,
QuotePrimaryLabel,
QuoteSecondaryLabel,
} from '@systhemaui/next'
export default function Demo() {
return (
<Quote className="max-w-xl">
<QuoteIcon />
<Paragraph type="lead">
We replaced three style guides with one token file. Designers change a color in Figma and
the site follows after the next sync.
</Paragraph>
<QuoteCite>
<QuoteAvatar src="/demo-assets/avatar-1.webp" alt="" />
<QuoteLabels>
<QuotePrimaryLabel>Maya Lindqvist</QuotePrimaryLabel>
<QuoteSecondaryLabel>Head of Design, Northwind</QuoteSecondaryLabel>
</QuoteLabels>
</QuoteCite>
</Quote>
)
}ImportLink to this section
import {
Quote,
QuoteAvatar,
QuoteCite,
QuoteIcon,
QuoteLabels,
QuotePrimaryLabel,
QuoteSecondaryLabel,
} from '@systhemaui/next'In a React app without Next.js, import them from @systhemaui/react. There QuoteAvatar renders a plain <img>.
Variants and tagsLink to this section
Each part has a default element, shorthands for the other common ones, and an as prop for anything else:
| Part | Default | Shorthands | Class |
|---|---|---|---|
Quote | <blockquote> | Quote.blockquote, Quote.div, Quote.p, Quote.section | quote |
QuoteIcon | <span> | QuoteIcon.span, QuoteIcon.div, QuoteIcon.svg | quote-icon |
QuoteCite | <cite> | QuoteCite.cite, QuoteCite.div, QuoteCite.footer | quote-cite |
QuoteAvatar | <img> | QuoteAvatar.img | quote-avatar |
QuoteLabels | <div> | QuoteLabels.div, QuoteLabels.span | quote-cite-labels |
QuotePrimaryLabel | <p> | QuotePrimaryLabel.p, .span, .div | quote-label-primary |
QuoteSecondaryLabel | <p> | QuoteSecondaryLabel.p, .span, .div | quote-label-secondary |
Quote.p only fits a one-line quote with inline content, because a <p> cannot contain other block elements.
ExamplesLink to this section
Text onlyLink to this section
The smallest quote is the wrapper and a paragraph. The border, padding and background come from the quote tokens.
Good design is as little design as possible. Less, but better, because it concentrates on the essential aspects.
import { Paragraph, Quote } from '@systhemaui/next'
export default function Demo() {
return (
<Quote className="max-w-xl">
<Paragraph>
Good design is as little design as possible. Less, but better, because it concentrates on
the essential aspects.
</Paragraph>
</Quote>
)
}Quote markLink to this section
An empty QuoteIcon draws the default quote mark, sized by --quote-icon-size and colored by the quote icon token. Put your own glyph inside it, or render it as the SVG itself with QuoteIcon.svg.
The default mark, drawn from the configured icon.
A custom SVG, sized and colored the same way.
import { Paragraph, Quote, QuoteIcon } from '@systhemaui/next'
export default function Demo() {
return (
<div className="grid w-full gap-6 sm:grid-cols-2">
<Quote>
<QuoteIcon />
<Paragraph>The default mark, drawn from the configured icon.</Paragraph>
</Quote>
<Quote>
<QuoteIcon.svg viewBox="0 -960 960 960" fill="currentColor" aria-hidden="true">
<path d="m233-120 65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Z" />
</QuoteIcon.svg>
<Paragraph>A custom SVG, sized and colored the same way.</Paragraph>
</Quote>
</div>
)
}Change the default mark for the whole project with blocks.quote (see Component CSS blocks).
Citation without an avatarLink to this section
Leave out QuoteAvatar and the labels take the full width. Use one label when a name is enough.
The migration took an afternoon, and nobody noticed the switch.
import {
Paragraph,
Quote,
QuoteCite,
QuoteLabels,
QuotePrimaryLabel,
QuoteSecondaryLabel,
} from '@systhemaui/next'
export default function Demo() {
return (
<Quote className="max-w-xl">
<Paragraph>The migration took an afternoon, and nobody noticed the switch.</Paragraph>
<QuoteCite.footer>
<QuoteLabels>
<QuotePrimaryLabel>Jonas Berg</QuotePrimaryLabel>
<QuoteSecondaryLabel>Lead developer</QuoteSecondaryLabel>
</QuoteLabels>
</QuoteCite.footer>
</Quote>
)
}Testimonials side by sideLink to this section
Quotes in a grid stretch to the same height, which makes a tidy testimonial row.
Our editors build landing pages from blocks without asking for help.

Maya Lindqvist
Head of Design
Dark mode was one token file away. We shipped it in a day.

Daniel Okafor
Frontend lead
import {
Paragraph,
Quote,
QuoteAvatar,
QuoteCite,
QuoteLabels,
QuotePrimaryLabel,
QuoteSecondaryLabel,
} from '@systhemaui/next'
const testimonials = [
{
text: 'Our editors build landing pages from blocks without asking for help.',
name: 'Maya Lindqvist',
role: 'Head of Design',
avatar: '/demo-assets/avatar-1.webp',
},
{
text: 'Dark mode was one token file away. We shipped it in a day.',
name: 'Daniel Okafor',
role: 'Frontend lead',
avatar: '/demo-assets/avatar-2.webp',
},
]
export default function Demo() {
return (
<div className="grid w-full gap-6 sm:grid-cols-2">
{testimonials.map((item) => (
<Quote.div key={item.name}>
<Paragraph>{item.text}</Paragraph>
<QuoteCite.div>
<QuoteAvatar src={item.avatar} alt="" />
<QuoteLabels>
<QuotePrimaryLabel>{item.name}</QuotePrimaryLabel>
<QuoteSecondaryLabel>{item.role}</QuoteSecondaryLabel>
</QuoteLabels>
</QuoteCite.div>
</Quote.div>
))}
</div>
)
}PropsLink to this section
QuoteLink to this section
Quote takes the props of the element it renders. disableAnimation drops the configured animationClasses; the quote then also stops suppressing the reveals of its children.
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'blockquote' | |
className | string | - | |
disableAnimation | boolean | - | |
…and all <blockquote> attributes |
Quote partsLink to this section
QuoteAvatar defaults alt to '', width and height to 64, and in React loading to 'lazy' and decoding to 'async'. The other parts take the props of the element they render.
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'span' | |
className | string | - | |
…and all <span> attributes |
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'cite' | |
className | string | - | |
…and all <cite> attributes |
| Prop | Type | Default | Description |
|---|---|---|---|
alt | string | - | |
className | string | - | |
…and all <img> attributes |
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'div' | |
className | string | - | |
…and all <div> attributes |
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'p' | |
className | string | - | |
…and all <p> attributes |
| Prop | Type | Default | Description |
|---|---|---|---|
as | ElementType | 'p' | |
className | string | - | |
…and all <p> attributes |
HTML and CSSLink to this section
The same markup with classes only. An empty .quote-icon draws the default mark; the labels use text-body plus their own color class:
<blockquote class="quote">
<span class="quote-icon"></span>
<p class="text-lead color-body">We replaced three style guides with one token file.</p>
<cite class="quote-cite">
<img class="quote-avatar" src="/avatar.webp" alt="" width="64" height="64" loading="lazy" />
<div class="quote-cite-labels">
<p class="text-body quote-label-primary">Maya Lindqvist</p>
<p class="text-body quote-label-secondary">Head of Design, Northwind</p>
</div>
</cite>
</blockquote>Every size, gap and color is a quote token: --quote-border-width, --quote-padding-x, --quote-icon-size, --quote-avatar-size, --color-quote-background and so on.
| Class | Styles |
|---|---|
quote | & { border-left-style: solid; border-left-width: var(--quote-border-width); padding-left: var(--quote-padding-x); padding-block: var(--quote-padding-y); background-color: var(--color-quote-background); border-color: var(--color-quote-border); }& > * { margin-block: var(--quote-gap-y); color: var(--color-quote-content); }.quote > * { &:first-child { margin-top: 0; } }.quote > * { &:last-child { margin-bottom: 0; } } |
quote-icon | & { object-position: center; display: block; overflow: visible; object-fit: contain; width: var(--quote-icon-size); height: var(--quote-icon-size); font-size: var(--quote-icon-size); line-height: var(--quote-icon-size); color: var(--color-quote-icon); }div.quote-icon:empty { mask-image: url("data:image/svg+xml,…"); mask-repeat: no-repeat; mask-position: center; mask-size: var(--quote-icon-size) var(--quote-icon-size); background-color: var(--color-quote-icon); }+ 1 more rule |
quote-avatar | object-position: center;flex-grow: 0;flex-shrink: 0;overflow: hidden;object-fit: cover;width: var(--quote-avatar-size);height: var(--quote-avatar-size);border-radius: var(--quote-avatar-border-radius); |
quote-cite | & { display: flex; justify-content: flex-start; align-items: center; column-gap: var(--quote-cite-gap-y); }& * { flex-shrink: 0; } |
quote-cite-labels | & { flex-grow: 1; }& * { margin-block: var(--quote-labels-gap-y); }.quote-cite-labels * { &:first-child { margin-top: 0; } }.quote-cite-labels * { &:last-child { margin-bottom: 0; } } |
quote-label-primary | color: var(--color-quote-primary-label); |
quote-label-secondary | color: var(--color-quote-secondary-label); |
Next.jsLink to this section
The @systhemaui/next QuoteAvatar renders next/image, so the avatar is resized and served in a modern format. It defaults width and height to 64; pass the size you display it at. The other parts render the same markup as in React.
| Prop | Type | Default | Description |
|---|---|---|---|
alt | string | - | |
…and props from next | |||
| …and all inherited HTML attributes |
AccessibilityLink to this section
- The default quote mark is a CSS mask on an empty element, so there is nothing to announce. Give a custom
QuoteIconchildaria-hidden="true". - Leave the avatar's
altempty when the name is in the labels next to it, so the name is not read twice. - HTML reserves
<cite>for the title of a work. To attribute a person,QuoteCite.footerorQuoteCite.divis the more accurate element.