macbetter

Content

Artwork

Draw an abstract picture from a seed, for album covers, card headers and placeholders.

Golden Hour Tapes
The Saltmarsh
Low Tide
Harbor Lights
Night Garden
Juniper & Ash
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.

aurora
bloom
orbit
strata
prism
halftone
rays
glyph
ribbon
topo
grain
mosaic
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>
  )
}
VariantWhat it draws
auroraSoft blobs of color melting into each other, like a mesh gradient.
bloomPetals fanned out from an edge, shaded like folded silk.
orbitConcentric rings with bright arcs and moons around a glowing core.
strataLayered dunes or hills, hazier toward the horizon, with a sun.
prismTranslucent planes of light crossing each other.
halftoneA field of dots that swell and shrink, on a dark ground or paper.
raysA sunburst, or beams of light falling from one point.
glyphA Swiss grid of circles, half circles and quarter circles.
ribbonWide ribbons twisting through space, two colors front and back.
topoContour lines around a summit, filled like an elevation map.
grainA two-color gradient with a soft orb and a printed sun, under film grain.
mosaicSoft 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.

Morning Coffee
Deep Focus
Late Night Jazz
Road Trip
Rainy Day
Sunday Soul
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.

accent
sunset
ocean
forest
berry
citrus
dusk
ember
glacier
candy
mono
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.

square
rounded
circle
app
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.

flat
gloss
grain
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.

New seriesYoga for Runners

Week one: hips and hamstrings

Six 10-minute sessions to do after a run.

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.

Now Playing: Velvet Hours
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 (or aria-label) when the picture stands alone. The SVG becomes an image with that name, and title also 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.
  • animated respects 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

PropTypeDefaultDescription
seedstring | 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.
colorsstring[]—Your own CSS colors: up to four hues, then an optional ground. Replaces `palette`.
shape"square" | "rounded" | "circle" | "app""rounded"The corners.
radiusnumber | string—A corner radius in pixels or any CSS length. Replaces `shape`.
aspectnumber1Width over height, like `16 / 9`. The composition is drawn for it.
sizenumber | 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`.
animatedbooleanfalseA slow drift of a few layers. Stops under Reduce Motion.
titlestring—Names the picture and makes it an image. Without it or `aria-label`, it's decorative.
childrenReactNode—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 variableOnDescription
data-variant, data-palette, data-shape, data-finish.mb-artworkThe resolved props, after auto. data-palette is custom with colors.
data-animated.mb-artworkPresent when animated is on.
--mb-artwork-1 … --mb-artwork-4.mb-artworkThe hues. Set them in CSS to make a palette of your own.
--mb-artwork-ground.mb-artworkThe deep base most variants are painted on.
--mb-artwork-light.mb-artworkHighlights, glows and suns.
--mb-artwork-paper, --mb-artwork-ink.mb-artworkThe pale ground some glyph and halftone pictures use, and the dark color drawn on it.
--mb-artwork-size.mb-artworkThe width, from size.
--mb-artwork-radius.mb-artworkThe corner radius, from radius.
--mb-artwork-aspect.mb-artworkThe aspect ratio, from aspect.