# Backgrounds (/docs/backgrounds)



<ComponentPreview name="backgrounds" />

A card's background is data. `CardBackground` is a small JSON object describing a colour, a gradient, an image or a live [shader](/docs/shaders). Store it with the card, pass it to `Card.Root`, and the whole card follows it: `Card.Background` paints it, `data-tone` tells your text which way to go, and `<Frost />` draws it under the frost.

```tsx
import { backgroundInk, Card, type CardBackground } from "@danolekh/cardstock";

const background: CardBackground = {
  type: "image",
  src: "/backgrounds/guilloche.webp",
  srcSet: "/backgrounds/guilloche-860.webp 860w, /backgrounds/guilloche.webp 1720w",
  color: "#10151d",
  tone: "dark",
  ink: "#f1dfa6",
};

<Card.Root background={background}>
  <Card.Body>
    <Card.Front
      className="relative overflow-hidden text-(--ink)"
      style={{ "--ink": backgroundInk(background) }}
    >
      <Card.Background className="absolute inset-0" />
      <Card.Number value={pan} className="relative" />
    </Card.Front>
  </Card.Body>
</Card.Root>;
```

## The format [#the-format]

| `type`   | Fields                                                               | Paints                                                               |
| -------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `solid`  | `color`                                                              | A flat colour                                                        |
| `linear` | `stops: [color, 0..1][]`, `angle` (degrees, default 135)             | `linear-gradient()`                                                  |
| `radial` | `stops`, `at: [x, y]` (fractions, default the middle)                | `radial-gradient(circle farthest-corner at …)`                       |
| `image`  | `src`, `srcSet`, `position&#x60; (object-position), &#x2A;*`color`** | An `<img>` with `object-fit: cover`, over `color`                    |
| `shader` | `shader`, `params`, `speed`, `poster&#x60;, &#x2A;*`color`**         | Its poster over `color`; [`<Shader />`](/docs/shaders) draws it live |

Every type also takes the following, all optional:

* `tone` (`"dark"` or `"light"`): how the background reads.
* `ink`: the text colour that goes with it.
* `label`: a name for a picker.

For an image, `color` is its dominant colour. It shows while the image loads, stays if the image fails, and sits under the frost. Pick it from the artwork.

## Storing it [#storing-it]

Keep the object as JSON: in a `jsonb` column, a document field, or your API's card resource. If your app only offers fixed designs, store the preset's key and map it to the object in code. Either way, read it through `parseCardBackground` before it reaches the page:

```ts
import { parseCardBackground } from "@danolekh/cardstock/background";

const background = parseCardBackground(row.background) ?? DEFAULT_BACKGROUND;
```

`@danolekh/cardstock/background` is the format on its own, with no React: `parseCardBackground`, `backgroundTone`, `backgroundInk`, `backgroundStyle` and the types, for a server, a build script or an API route. The same functions are exported from `@danolekh/cardstock` too.

`parseCardBackground` returns a clean copy (unknown keys dropped) or `null`. It accepts only plain colours (hex, `rgb()`, `hsl()`, `oklch()` and the like, never `url()` or `var()`), and images served over `https:`, from root-relative paths, or as base64 `data:` images. A background that a user picked or uploaded can't inject CSS or load from an insecure origin.

## Tone and ink [#tone-and-ink]

`Card.Root` sets `data-tone` to `dark` or `light`. Style your text for it, or ask for a colour:

* `backgroundTone(bg)` returns `tone` if you set it. Otherwise it measures the luminance of the colour (for a gradient, the average of its stops) and picks whichever of white or near-black text contrasts more. It can only measure hex and `rgb()` colours; for artwork or other colour formats, set `tone` yourself.
* `backgroundInk(bg)` returns `ink` if you set it, or else white on dark and near-black on light.

## `Card.Background` [#cardbackground]

It paints the card's `background`, or its own `value` (for example, a plainer back face). It renders a `<div>` with `aria-hidden`, `data-type` and `data-tone`, so size and place it yourself, usually `absolute inset-0` as the face's first child.

* A colour or gradient is painted on the div itself.
* An image becomes an `<img alt="">` inside it, with `srcset`, `sizes` (pass your card's display width) and `loading` (`"lazy"` suits cards off screen, such as carousel neighbours). The div carries `data-loaded` once the image is in, which you can use for a fade, and `data-error` if it failed, in which case `color` stays.

## Frost [#frost]

`<Frost />` reads the card's background, so it needs no props (`<FrostVeil />` doesn't draw the background at all). It draws a gradient or colour exactly as the CSS paints it, and it picks up an image from the `<img>`. If an image comes from another origin, that server has to allow CORS (`Access-Control-Allow-Origin`), or the frost leaves the image out and draws `color` instead. cardstock.danolekh.com serves the house artwork that way.

## Serving images [#serving-images]

An image background is a file you host. Where it lives is up to you:

* **Your app's `public/` folder** is the simplest: same origin, so the frost can draw it with no setup.
* **A CDN or object storage** (R2, S3, a CDN in front of either) suits artwork users upload or that changes without a deploy. Allow CORS, so the frost can draw it:

```json title="R2 / S3 CORS rules"
[{ "AllowedOrigins": ["https://your.app"], "AllowedMethods": ["GET"], "AllowedHeaders": ["*"] }]
```

Cache the files for good (`Cache-Control: public, max-age=31536000, immutable`), and give a changed image a new name instead of overwriting it: `build --hash` below puts a content hash in the file name. Ship a smaller copy in `srcSet` for 1× screens and carousel neighbours; `Card.Background` passes `sizes` so the browser picks.

## The house artwork [#the-house-artwork]

cardstock has ten original backgrounds, drawn for it: guilloché engraving on ink and on sand, holographic foil, aurora and dusk meshes, topographic contours, brushed steel, laid paper, tide and a noir grid, each at 1720×1080 and 860×540, and four gradients. None of it is in the library or the registry. You download the ones you want, on demand, with `@danolekh/cardstock-backgrounds`:

```bash
npx @danolekh/cardstock-backgrounds list
npx @danolekh/cardstock-backgrounds add holo guilloche ink
```

`add` downloads the images (checked against the manifest's SHA-256) into `public/backgrounds`, and writes their data into `src/lib/card-backgrounds.ts`:

```ts
// Card backgrounds, managed by @danolekh/cardstock-backgrounds. …
import type { CardBackground } from "@danolekh/cardstock";
export const CARD_BACKGROUNDS = {
  // cardstock-backgrounds:start
  holo: {
    type: "image",
    label: "Holo",
    src: "/backgrounds/holo.webp",
    srcSet: "…",
    color: "#c7b5f4",
    tone: "light",
    ink: "#231a3d",
  },
  ink: {
    type: "linear",
    label: "Ink",
    stops: [
      ["#34322d", 0],
      ["#171614", 0.55],
      ["#080807", 1],
    ],
    ink: "#f1dfa6",
  },
  // cardstock-backgrounds:end
} as const satisfies Record<string, CardBackground & { label: string }>;
export type CardBackgroundName = keyof typeof CARD_BACKGROUNDS;
```

Edit the entries freely, and add your own outside the markers; `add` and `build` only touch the lines between them, by key. Other places:

* `--dir static/cards --base /cards`: another folder, served at another path.
* `--base https://cdn.example.com/cards`: point the data at your CDN, then upload the folder there.
* `--out app/lib/cards.ts`, or `--json cards.json` to seed a database instead of a TypeScript file.
* `--remote`: no download; the data points at cardstock.danolekh.com, which serves the files with CORS.

The artwork is MIT, like cardstock. It's drawn by a script in the docs (`pnpm --filter docs backgrounds`), and the list the CLI reads is [`manifest.json`](https://cardstock.danolekh.com/backgrounds/manifest.json).

## Your own images [#your-own-images]

Design for the card: 1.586:1, at least 1720px wide, with the top-left (the wordmark) and the band where the number sits calm enough to read over, and a little grain against banding in smooth gradients. Then prerender it:

```bash
npx @danolekh/cardstock-backgrounds build ./art/opal.png --label "Opal"
```

```
opal  color #3b2f3f  tone dark  ink #f6f1f8  contrast 11.2:1 (darkest 10%: 6.1:1)
```

`build` crops the image to the card's ratio (`--position top`, `attention`, `"30% 60%"`, …), writes it at 1720 and 860 wide as WebP (or `--format avif`), measures its colour, works out whether it reads dark or light, suggests an ink tinted toward its hue, and warns when the ink wouldn't hold 4.5:1 against the busiest tenth of the image. The entry goes into the same presets file. Everything it measures can be overridden: `--tone`, `--ink`, `--credit`.

Any image in the same format works, of course: [crd-ui](https://crd-ui.juanda.co/#backgrounds) publishes 45 CC0 card backgrounds, and `build` turns one into a card background in a line (those files are 430×270, which looks soft on a high-density screen at full size).

It also has a small API for build scripts: `buildBackground(input, options)`, `addPresets(names, options)` and `loadManifest()`.
