Themes
A theme is data that sets how a page looks: nine colors, a background, font roles, shape and section layout. No CSS, no code.
The same theme JSON is rendered by the Taste app and by taste.dev, so both always match. Themes carry no code and no CSS, which is what makes it safe for anyone to publish one: every value is a hex color, a number in a fixed range, or one of a closed set of words.
The schema is theme-1.json (Schemas). The default theme is terminal.
Pick a theme
curl https://api.taste.dev/themes \
-H "Authorization: Bearer $TOKEN"curl -X PUT https://api.taste.dev/profile/theme \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"theme_id": "studio"}'GET /themes returns {"themes": [<theme>, …]} in picker order: terminal, studio, pearl. PUT /profile/theme returns 200 {"theme_id": "studio"}. An id that isn't a built-in returns 400 {"error": "invalid"}.
Your page carries the full resolved theme in GET /public/u/{username}, so a renderer never needs a second request.
Fields
Every field is required except description, background.angle and background.image. Objects allow no extra keys.
schema1requiredThe theme schema version. Only 1 exists today.
idstringrequired1–40 lowercase letters, digits and inner hyphens: ^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$.
namestringrequiredDisplay name, 1–40 characters.
authorstringrequiredThe author's Taste username (taste for built-ins), 1–30 of a-z 0-9 . _ -. Pages by other authors show "Theme by @author".
descriptionstringUp to 140 characters.
modeenumrequiredlight or dark. The status bar, browser chrome and system controls follow it.
colorsobjectrequiredNine flat #RRGGBB colors. See Colors.
backgroundobjectrequiredThe page background. See Background and effects.
effectsobjectrequiredscanlines, grain and glow. See Background and effects.
typeobjectrequiredFont roles and heading style. See Type.
shapeobjectrequiredCard radius, avatar shape, border width. See Shape.
layoutobjectrequiredSection order and each section's variant. See Layout.
Colors
| Key | Used for |
|---|---|
background | The page |
background2 | The second page tone (bottom of the page, alternating rows) |
surface | Cards, link buttons, chips |
line | Hairlines and card borders |
text | Body text |
muted | Secondary text |
accent | Buttons, selected states, links. Always flat: selected and pressed states never flip to a gradient |
onAccent | Text on the accent |
highlight | The @handle and section headings |
All nine must be 6-digit hex (#39FF6A). No alpha, no names, no gradients.
Background and effects
| Field | Values |
|---|---|
background.type | solid, gradient or image |
background.colors | 1–4 hex colors. solid uses the first; gradient blends all of them (use 2–4) |
background.angle | Gradient angle in degrees, 0–360. Renderers use 180 (top to bottom) when it's missing |
background.image | A URI. Only Taste-hosted uploads are rendered; anything else falls back to background.colors[0] |
effects.scanlines | true draws faint CRT scanlines |
effects.grain | Film grain opacity, 0–0.15 |
effects.glow | true adds a soft glow in the highlight color |
On taste.dev an image background never sits directly under text: the page content moves onto a surface card.
Type
Three font roles, each picked from a short list:
| Field | Values | Sets |
|---|---|---|
type.display | sans, serif, mono, pixel | The name |
type.body | sans, serif, mono | Body text |
type.label | sans, mono, pixel | The @handle, section headings and small labels |
type.headingStyle | prompt, label, none | How section headings read |
Each role maps to a font on each platform:
| Role | App (iOS) | Web |
|---|---|---|
sans | SF Pro (system) | Inter |
serif | New York (system serif) | Instrument Serif |
mono | SF Mono / Menlo | JetBrains Mono |
pixel | VT323 (bundled) | VT323 |
Heading styles:
headingStyle | A section heading reads |
|---|---|
prompt | > building |
label | BUILDING, in the highlight color |
none | No headings (sections keep their accessible names) |
Shape
| Field | Values |
|---|---|
shape.radius | Card corner radius in px, 0–32 |
shape.avatar | circle, rounded or square |
shape.border | Card border width in px, 0–2 |
Layout
| Field | Values |
|---|---|
layout.order | The sections, in order, no repeats, at least one: building, tools, links, projects, pay_it_forward, topics, here_for, badges |
layout.links | buttons (full-width rows), grid (two-up tiles) or chips |
layout.projects | cards (screenshot on top) or list (thumbnail rows) |
layout.tools | tiles (logo tiles) or chips (logo + name) |
A theme decides how a page looks, never what it hides. Sections left out of order still render, after the listed ones. A builder hides sections in their profile settings, not through a theme.
Contrast rules
A theme is checked for contrast (WCAG ratio) when it's accepted:
textandmutedeach reach 4.5:1 against bothsurfaceandbackground.onAccentreaches 4.5:1 againstaccent.
highlight, line and background2 aren't checked, so don't use highlight for body text. The built-ins all pass. The check lives in the API's theme validator, which will gate community themes (Contributing themes).
Built-in themes
Terminal
Green phosphor on a dark tube. The default.
{
"schema": 1,
"id": "terminal",
"name": "Terminal",
"author": "taste",
"description": "Green phosphor on a dark tube. The original.",
"mode": "dark",
"colors": {
"background": "#020803",
"background2": "#06120B",
"surface": "#0B1510",
"line": "#1D2E24",
"text": "#EAF3EE",
"muted": "#8AA597",
"accent": "#AEF8D0",
"onAccent": "#04170E",
"highlight": "#39FF6A"
},
"background": { "type": "gradient", "colors": ["#06120B", "#020803"], "angle": 180 },
"effects": { "scanlines": true, "grain": 0, "glow": true },
"type": { "display": "sans", "body": "sans", "label": "pixel", "headingStyle": "prompt" },
"shape": { "radius": 18, "avatar": "circle", "border": 1 },
"layout": {
"order": ["building", "tools", "links", "projects", "pay_it_forward", "topics", "here_for", "badges"],
"links": "buttons",
"projects": "cards",
"tools": "tiles"
}
}Studio
Clean and light, with a studio-green accent.
{
"schema": 1,
"id": "studio",
"name": "Studio",
"author": "taste",
"description": "Clean and light, with a studio-green accent.",
"mode": "light",
"colors": {
"background": "#F5F7F4",
"background2": "#EEF2EE",
"surface": "#FFFFFF",
"line": "#DCE6DF",
"text": "#0E1A14",
"muted": "#54665C",
"accent": "#14634B",
"onAccent": "#FFFFFF",
"highlight": "#14634B"
},
"background": { "type": "solid", "colors": ["#F5F7F4"] },
"effects": { "scanlines": false, "grain": 0, "glow": false },
"type": { "display": "sans", "body": "sans", "label": "sans", "headingStyle": "label" },
"shape": { "radius": 20, "avatar": "circle", "border": 1 },
"layout": {
"order": ["building", "tools", "links", "projects", "pay_it_forward", "topics", "here_for", "badges"],
"links": "buttons",
"projects": "cards",
"tools": "tiles"
}
}Pearl
Soft pearl light, a little anime. Serif name, rounded avatar, tools as chips.
{
"schema": 1,
"id": "pearl",
"name": "Pearl",
"author": "taste",
"description": "Soft pearl light, a little anime.",
"mode": "light",
"colors": {
"background": "#FBF8FF",
"background2": "#F1F6FF",
"surface": "#FFFFFF",
"line": "#E6E0F5",
"text": "#1B1730",
"muted": "#6B6485",
"accent": "#6D5BD0",
"onAccent": "#FFFFFF",
"highlight": "#6D5BD0"
},
"background": { "type": "gradient", "colors": ["#FFF1F8", "#F1F6FF", "#EFFBF6"], "angle": 160 },
"effects": { "scanlines": false, "grain": 0.04, "glow": false },
"type": { "display": "serif", "body": "sans", "label": "sans", "headingStyle": "label" },
"shape": { "radius": 26, "avatar": "rounded", "border": 1 },
"layout": {
"order": ["building", "links", "projects", "tools", "pay_it_forward", "topics", "here_for", "badges"],
"links": "buttons",
"projects": "cards",
"tools": "chips"
}
}Preview any of them on the sample page: taste.dev/@demo?theme=studio.
Fallback
A broken theme must never break a page.
- Unknown theme id. If a page's stored theme no longer exists, the API sends
terminal.themein a public page is always a complete theme. - Unreadable theme. A renderer that can't read a theme (wrong
schema, a bad color, a missing field) rendersterminal. taste.dev does exactly this. - Unknown enum value. A value newer than your renderer (say a future
type.displayfont) should render as that field's default: the valueterminaluses. See Versioning.