Docs

This page isn't translated yet

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.

Quote

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:

PartDefaultShorthandsClass
Quote<blockquote>Quote.blockquote, Quote.div, Quote.p, Quote.sectionquote
QuoteIcon<span>QuoteIcon.span, QuoteIcon.div, QuoteIcon.svgquote-icon
QuoteCite<cite>QuoteCite.cite, QuoteCite.div, QuoteCite.footerquote-cite
QuoteAvatar<img>QuoteAvatar.imgquote-avatar
QuoteLabels<div>QuoteLabels.div, QuoteLabels.spanquote-cite-labels
QuotePrimaryLabel<p>QuotePrimaryLabel.p, .span, .divquote-label-primary
QuoteSecondaryLabel<p>QuoteSecondaryLabel.p, .span, .divquote-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.

Text only

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.

Default and custom quote marks

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.

Labels only

The migration took an afternoon, and nobody noticed the switch.

Jonas Berg

Lead developer

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.

Testimonial grid

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.

PropTypeDefaultDescription
asElementType'blockquote'
classNamestring-
disableAnimationboolean-
…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.

PropTypeDefaultDescription
asElementType'span'
classNamestring-
…and all <span> attributes
PropTypeDefaultDescription
asElementType'cite'
classNamestring-
…and all <cite> attributes
PropTypeDefaultDescription
altstring-
classNamestring-
…and all <img> attributes
PropTypeDefaultDescription
asElementType'div'
classNamestring-
…and all <div> attributes
PropTypeDefaultDescription
asElementType'p'
classNamestring-
…and all <p> attributes
PropTypeDefaultDescription
asElementType'p'
classNamestring-
…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.

ClassStyles
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-avatarobject-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-primarycolor: var(--color-quote-primary-label);
quote-label-secondarycolor: 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.

PropTypeDefaultDescription
altstring-
…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 QuoteIcon child aria-hidden="true".
  • Leave the avatar's alt empty 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.footer or QuoteCite.div is the more accurate element.