Content
Artwork
Draw an abstract picture from a seed, for album covers, card headers and placeholders.
import { Artwork } from "macbetter/artwork"
const albums = [
{ title: "Golden Hour Tapes", artist: "The Saltmarsh", variant: "bloom", palette: "sunset" },
{ title: "Low Tide", artist: "Harbor Lights", variant: "strata", palette: "ocean" },
{ title: "Night Garden", artist: "Juniper & Ash", variant: "aurora", palette: "berry" },
] as const
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 180px))", gap: 20 }}>
{albums.map((album) => (
<div key={album.title}>
<Artwork seed={album.title} variant={album.variant} palette={album.palette} />
<div className="mb-text-subheadline" style={{ marginTop: 8, fontWeight: 500 }}>{album.title}</div>
<div className="mb-text-subheadline mb-label-secondary">{album.artist}</div>
</div>
))}
</div>
)
}When to use it
Use artwork where a picture helps people tell things apart but you don't have one: playlists, albums and podcasts without cover art, a featured story, a workspace or a project. The same seed always draws the same picture, so an item keeps its cover everywhere it appears.
- Seed it with something stable, like an id or a title, so the picture doesn't change between visits.
- Pick one or two variants for a collection and let the seed vary them. A shelf where every cover is a different style looks random.
- Don't use it in place of content people need to recognize, like a real album cover or a person's photo. It's decoration that carries identity, not meaning.
Usage
import { Artwork } from "macbetter/artwork"Pass a seed. The picture is an inline SVG that fills its container's width, at the aspect you give it (square by default). It's drawn on the server, with no client JavaScript.
Variants
Twelve styles. Each uses the seed for its composition (where the shapes go, how many, how big) as well as for the order of its colors. Leave variant out, or pass auto, to let the seed pick one.
import { Artwork, artworkVariants } from "macbetter/artwork"
const palettes = ["sunset", "ocean", "berry", "forest", "dusk", "citrus", "glacier", "ember", "candy", "sunset", "ocean", "berry"] as const
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))", gap: 20, width: "100%" }}>
{artworkVariants.map((variant, index) => (
<figure key={variant} style={{ margin: 0 }}>
<Artwork seed="macbetter" variant={variant} palette={palettes[index]} />
<figcaption className="mb-text-subheadline" style={{ marginTop: 8 }}>
{variant}
</figcaption>
</figure>
))}
</div>
)
}| Variant | What it draws |
|---|---|
aurora | Soft blobs of color melting into each other, like a mesh gradient. |
bloom | Petals fanned out from an edge, shaded like folded silk. |
orbit | Concentric rings with bright arcs and moons around a glowing core. |
strata | Layered dunes or hills, hazier toward the horizon, with a sun. |
prism | Translucent planes of light crossing each other. |
halftone | A field of dots that swell and shrink, on a dark ground or paper. |
rays | A sunburst, or beams of light falling from one point. |
glyph | A Swiss grid of circles, half circles and quarter circles. |
ribbon | Wide ribbons twisting through space, two colors front and back. |
topo | Contour lines around a summit, filled like an elevation map. |
grain | A two-color gradient with a soft orb and a printed sun, under film grain. |
mosaic | Soft rounded tiles shaded across the grid, with one or two bright accents. |
Seeds
Within one variant, each seed draws a different composition. Seed covers with their title or id.
import { Artwork } from "macbetter/artwork"
const playlists = ["Morning Coffee", "Deep Focus", "Late Night Jazz", "Road Trip", "Rainy Day", "Sunday Soul"]
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))", gap: 16, width: "100%" }}>
{playlists.map((name) => (
<figure key={name} style={{ margin: 0 }}>
<Artwork seed={name} variant="ribbon" palette="dusk" />
<figcaption className="mb-text-subheadline" style={{ marginTop: 6 }}>
{name}
</figcaption>
</figure>
))}
</div>
)
}Palettes
A palette is a set of system colors, so the art follows light and dark mode and your theme's palette. accent is built from your theme's accent color; mono from its gray. Leave palette out, or pass auto, to let the seed pick one of the colorful ones.
import { Artwork, artworkPalettes } from "macbetter/artwork"
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(110px, 1fr))", gap: 16, width: "100%" }}>
{artworkPalettes.map((palette) => (
<figure key={palette} style={{ margin: 0 }}>
<Artwork seed="Palettes" variant="aurora" palette={palette} />
<figcaption className="mb-text-subheadline" style={{ marginTop: 6 }}>
{palette}
</figcaption>
</figure>
))}
</div>
)
}To use your own colors, pass colors: up to four CSS colors for the hues and an optional fifth for the ground. With fewer than four the hues repeat; without a ground one is mixed from the first hue.
import { Artwork } from "macbetter/artwork"
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 20 }}>
<Artwork seed="Brand launch" variant="prism" colors={["#ff6b3d", "#ffc53d", "#ff3d8b", "#7a3dff", "#1a0b2e"]} size={160} />
<Artwork seed="Brand launch" variant="bloom" colors={["var(--mb-accent)", "var(--mb-accent-secondary)"]} size={160} />
</div>
)
}Shapes
rounded, the default, has the small corner of album art. app is an app icon's rounded square, drawn with Apple's continuous corner where the browser supports corner-shape. circle is round, or a capsule when the aspect isn't square. Pass radius for any other corner.
import { Artwork } from "macbetter/artwork"
const shapes = ["square", "rounded", "circle", "app"] as const
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 20 }}>
{shapes.map((shape) => (
<figure key={shape} style={{ margin: 0, textAlign: "center" }}>
<Artwork seed="Shapes" variant="orbit" palette="ocean" shape={shape} size={112} />
<figcaption className="mb-text-subheadline" style={{ marginTop: 8 }}>
{shape}
</figcaption>
</figure>
))}
</div>
)
}Finishes
gloss, the default, adds a soft light along the top edge and a gentle vignette, so the picture reads as a printed surface rather than a flat fill. grain adds film grain on top of that. flat adds nothing, for small sizes or when you're compositing the art yourself.
import { Artwork } from "macbetter/artwork"
const finishes = ["flat", "gloss", "grain"] as const
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 20 }}>
{finishes.map((finish) => (
<figure key={finish} style={{ margin: 0 }}>
<Artwork seed="Finish" variant="glyph" palette="sunset" finish={finish} size={160} />
<figcaption className="mb-text-subheadline" style={{ marginTop: 8 }}>
{finish}
</figcaption>
</figure>
))}
</div>
)
}Card headers and text on art
Put children inside to lay text over the picture. They sit at the bottom on a soft scrim, in white.
import { Artwork } from "macbetter/artwork"
import { Button } from "macbetter/button"
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "macbetter/card"
export default function Example() {
return (
<Card variant="elevated" style={{ width: 320, overflow: "hidden", padding: 0 }}>
<Artwork seed="Fitness Plus Yoga" variant="strata" palette="sunset" aspect={16 / 9} shape="square">
<span className="mb-text-footnote" style={{ fontWeight: 600, textTransform: "uppercase", opacity: 0.85 }}>
New series
</span>
<span className="mb-text-title-3">Yoga for Runners</span>
</Artwork>
<CardHeader style={{ padding: "14px 16px 0" }}>
<CardTitle>Week one: hips and hamstrings</CardTitle>
<CardDescription>Six 10-minute sessions to do after a run.</CardDescription>
</CardHeader>
<CardFooter style={{ padding: 16 }}>
<Button size="sm" variant="filled">
Start Session
</Button>
</CardFooter>
</Card>
)
}Icons in lists
At small sizes, the app shape works as an icon for things that aren't people: podcasts, projects, workspaces.
import { Artwork } from "macbetter/artwork"
import { List, ListRow, ListRowAccessory, ListRowSubtitle, ListRowTitle, ListSection } from "macbetter/list"
const shows = [
{ title: "Field Notes", host: "Inès Moreau", variant: "topo", palette: "forest" },
{ title: "Night Shift", host: "Kofi Mensah", variant: "rays", palette: "dusk" },
{ title: "Small Batch", host: "Wren Takahashi", variant: "halftone", palette: "citrus" },
{ title: "Open Water", host: "Amara Okafor", variant: "mosaic", palette: "glacier" },
] as const
export default function Example() {
return (
<List aria-label="Podcasts" style={{ width: 360 }}>
<ListSection>
{shows.map((show) => (
<ListRow key={show.title} href={`#${show.title}`}>
<Artwork
seed={show.title}
variant={show.variant}
palette={show.palette}
shape="app"
size={40}
style={{ gridColumn: 1, gridRow: "1 / -1", marginInlineEnd: 12 }}
/>
<ListRowTitle>{show.title}</ListRowTitle>
<ListRowSubtitle>{show.host}</ListRowSubtitle>
<ListRowAccessory type="disclosure" />
</ListRow>
))}
</ListSection>
</List>
)
}Motion
animated makes a few layers drift slowly. Use it for one large picture at a time, like a Now Playing screen; it's off by default because every frame repaints the SVG. The drift stops under Reduce Motion.
import { Artwork } from "macbetter/artwork"
export default function Example() {
return <Artwork seed="Now Playing" variant="aurora" palette="dusk" animated size={240} title="Now Playing: Velvet Hours" />
}Platform behavior
Artwork looks the same on every platform: its size follows the layout, not the platform's text size. Corners are proportional, so a cover keeps the same shape from a 36-point mini player to a full-width Now Playing screen. Under Increase Contrast and Reduce Transparency nothing changes, because the picture isn't a material.
Accessibility
- By default artwork is decorative: the SVG is hidden from assistive technology, because the title next to it already names the item.
- Pass
title(oraria-label) when the picture stands alone. The SVG becomes an image with that name, andtitlealso shows as a tooltip. - Text you lay over the art is real text, read in order. The scrim behind it keeps white text above 4.5:1 on most compositions; check yours if you use a pale palette like
candy. animatedrespects Reduce Motion.- Gradient and filter ids are unique to each instance, so any number of pictures can share a page, including ones with the same seed.
API
Artwork
| Prop | Type | Default | Description |
|---|---|---|---|
seed | string | number | — | Same seed, same picture, on the server and in the browser. |
variant | "aurora" | "bloom" | "orbit" | "strata" | "prism" | "halftone" | "rays" | "glyph" | "ribbon" | "topo" | "grain" | "mosaic" | "auto" | "auto" | The style. `auto` picks one from the seed. |
palette | "accent" | "sunset" | "ocean" | "forest" | "berry" | "citrus" | "dusk" | "ember" | "glacier" | "candy" | "mono" | "auto" | "auto" | System colors to paint with. `auto` picks one from the seed. |
colors | string[] | — | Your own CSS colors: up to four hues, then an optional ground. Replaces `palette`. |
shape | "square" | "rounded" | "circle" | "app" | "rounded" | The corners. |
radius | number | string | — | A corner radius in pixels or any CSS length. Replaces `shape`. |
aspect | number | 1 | Width over height, like `16 / 9`. The composition is drawn for it. |
size | number | string | — | The width, in pixels or any CSS length. Without it, the art fills its container's width. |
finish | "flat" | "gloss" | "grain" | "gloss" | A top light and vignette, plus film grain with `grain`. |
animated | boolean | false | A slow drift of a few layers. Stops under Reduce Motion. |
title | string | — | Names the picture and makes it an image. Without it or `aria-label`, it's decorative. |
children | ReactNode | — | Text laid over the bottom of the picture. |
Every other prop goes to the outer span. artworkVariants and artworkPalettes list the names, for building pickers.
Data attributes and CSS variables
| Attribute or variable | On | Description |
|---|---|---|
data-variant, data-palette, data-shape, data-finish | .mb-artwork | The resolved props, after auto. data-palette is custom with colors. |
data-animated | .mb-artwork | Present when animated is on. |
--mb-artwork-1 … --mb-artwork-4 | .mb-artwork | The hues. Set them in CSS to make a palette of your own. |
--mb-artwork-ground | .mb-artwork | The deep base most variants are painted on. |
--mb-artwork-light | .mb-artwork | Highlights, glows and suns. |
--mb-artwork-paper, --mb-artwork-ink | .mb-artwork | The pale ground some glyph and halftone pictures use, and the dark color drawn on it. |
--mb-artwork-size | .mb-artwork | The width, from size. |
--mb-artwork-radius | .mb-artwork | The corner radius, from radius. |
--mb-artwork-aspect | .mb-artwork | The aspect ratio, from aspect. |