cards

quote card

a quote, with who said it on the strip below.

the cleanest toolkit i've touched in years. it stays out of the way while i write.
ada lovelace · analyst, engine co.

installation

new to blurrow? this sets up your project in one go: the package, the styles and the font. already done it? skip straight to usage. the installation page has the details.

bash
npx @blurrow/ui init
›rather own the code? copy the source instead

let the cli copy it, along with anything it needs:

bash
npx @blurrow/ui add quote-card

or by hand: drop these files into components/ui and import from there. quote card leans on CardShell.tsx, so those come along too.

components/ui/QuoteCard.tsxtsx
import React from "react";
import { CardShell } from "./CardShell";
interface QuoteCardProps {
quote: string;
author: string;
role?: string;
avatarSrc?: string;
theme?: "light" | "dark";
className?: string;
}
/* Built on CardShell: the quote owns the face; who said it sits on the rail,
so a stretched card grows the face instead of opening a gap mid-card. */
export function QuoteCard({
quote,
author,
role,
avatarSrc,
theme = "light",
className = "",
}: QuoteCardProps) {
const dark = theme === "dark";
// Handles like "@name" shouldn't render an "@" monogram
const initial = (author.match(/[\p{L}\p{N}]/u)?.[0] ?? "?").toLowerCase();
const rail = (
<figcaption className="flex items-center gap-2.5 min-w-0">
{avatarSrc ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={avatarSrc}
alt=""
loading="lazy"
className="w-7 h-7 rounded-full object-cover shrink-0"
style={{ boxShadow: dark ? "0 0 0 0.5px rgba(255,255,255,0.084)" : "0 0 0 0.5px rgba(17,17,17,0.048)" }}
/>
) : (
<span
aria-hidden
className={[
"w-7 h-7 rounded-full flex items-center justify-center text-[11.5px] font-semibold shrink-0",
dark ? "text-white" : "text-neutral-700",
].join(" ")}
style={{
background: dark ? "var(--tile-dark)" : "var(--face-light)",
boxShadow: dark ? "var(--tile-dark-shadow)" : "var(--tile-light-shadow)",
}}
>
{initial}
</span>
)}
<span className="min-w-0 truncate">
<cite
className={[
"not-italic font-semibold tracking-[-0.008em]",
dark ? "text-white" : "text-neutral-900",
].join(" ")}
>
{author}
</cite>
{role && <span> · {role}</span>}
</span>
</figcaption>
);
return (
<CardShell as="figure" theme={theme} className={className} rail={rail} railKind="text">
<div className="flex-1 flex flex-col p-8">
<svg
aria-hidden
width="22"
height="17"
viewBox="0 0 56 44"
className={["shrink-0 mb-auto", dark ? "text-white/25" : "text-neutral-300"].join(" ")}
>
<path
fill="currentColor"
d="M0 44V27.5C0 12.6 7.4 3.4 22.2 0l2.6 6.2C16.9 8.7 13 13.7 12.6 21H24v23H0zm32 0V27.5C32 12.6 39.4 3.4 54.2 0l2.6 6.2C48.9 8.7 45 13.7 44.6 21H56v23H32z"
/>
</svg>
<blockquote
className={[
"m-0 pt-8 text-[19px] leading-[1.55] tracking-[-0.015em] text-pretty",
dark ? "text-neutral-50" : "text-neutral-900",
].join(" ")}
>
{quote}
</blockquote>
</div>
</CardShell>
);
}
components/ui/CardShell.tsxtsx
import React from "react";
type Theme = "light" | "dark";
interface CardShellProps {
theme?: Theme;
/** Controls and context: actions, attribution, deltas. Lives on the chassis. */
rail?: React.ReactNode;
/** "actions" for buttons; "text" for plain context, indented to the face's text edge */
railKind?: "actions" | "text";
as?: "div" | "figure" | "article";
className?: string;
faceClassName?: string;
children: React.ReactNode;
}
/* Every card is built like every other object in the kit: a chassis (bezel)
holding a face. Content goes on the face; controls and context go on the
rail, the strip of chassis below it. The two are nearly the same tone —
the hairline around the face is what separates them.
Geometry, measured from the card's outer edge:
- chassis 24px radius, 6px bezel → face 18px radius (concentric)
- rail content sits 12px in from the left and bottom, 10px below the face
- a button's label then starts 30px in — the same edge as the face's text,
and plain-text rails are indented to that edge too */
export function CardShell({
theme = "light",
rail,
railKind = "actions",
as: Tag = "div",
className = "",
faceClassName = "",
children,
}: CardShellProps) {
const dark = theme === "dark";
return (
<Tag
className={[
"rim relative m-0 rounded-[24px] p-1.5 flex flex-col h-full",
rail ? "pb-0" : "",
className,
].join(" ")}
style={{
background: dark ? "var(--card-bezel-dark)" : "var(--card-bezel-light)",
boxShadow: dark ? "var(--card-dark-shadow)" : "var(--card-light-shadow)",
["--rim" as string]: dark ? "rgba(255,255,255,0.1)" : "rgba(255,255,255,1)",
} as React.CSSProperties}
>
<div
className={["relative flex-1 min-h-0 rounded-[18px] overflow-hidden flex flex-col", faceClassName].join(" ")}
style={{
background: dark ? "var(--card-dark)" : "var(--card-light)",
boxShadow: dark ? "var(--card-face-dark-shadow)" : "var(--card-face-light-shadow)",
}}
>
{children}
</div>
{rail && (
<div className="relative shrink-0 px-1.5 pt-2.5 pb-3">
<div
className={[
// 40px = a small button's full height, so text rails and button
// rails come out the same height and line up across a row
"min-h-10 flex items-center gap-2 text-[12.5px]",
railKind === "text" ? "pl-[18px] pr-3 gap-2.5" : "",
dark ? "text-neutral-400" : "text-neutral-500",
].join(" ")}
>
{rail}
</div>
</div>
)}
</Tag>
);
}

usage

tsx
import { QuoteCard } from "@blurrow/ui";
tsx
<QuoteCard quote="…" author="ada lovelace" role="analyst" />

examples

dark

we replaced half our design system with this in a weekend.
grace hopper · rear admiral, compilers

props

straight from the source, so they're always up to date.

propsts
interface QuoteCardProps {
quote: string;
author: string;
role?: string;
avatarSrc?: string;
theme?: "light" | "dark";
className?: string;
}