Skip to content

Colors, fonts and logo

Your brand lives in one file, src/content/singletons/brand.json. In Keystatic it’s the Brand screen (in Handy under Pages → Brand). The brand is shared by every language, and it stays a file in your project even when the rest of your content lives in Sanity or EmDash, because fonts are chosen when the site is built.

Handy’s brand:

{
"accent": "#f26a1b",
"second": "#ffd23f",
"third": "#6fb6e8",
"corners": "round",
"fonts": {
"body": "Figtree",
"heading": "Bricolage Grotesque"
}
}
Field What it does in Handy Allowed values
accent Hi-vis orange: buttons, marks, the ticks on the to-do list A hex color with six digits, e.g. #f26a1b
second Safety yellow: the to-do note, the price tag, the tape measure Hex color
third Painter’s tape blue: a second fill and the tape under marked words Hex color
corners How round cards and buttons are round or soft
fonts.body The text font One of the text fonts below
fonts.heading The heading font One of the heading fonts below

The exact list for your template is the brand part of src/content/model.ts; the Brand screen in Keystatic shows the same fields with their descriptions. Templates differ here:

  • Most templates have accent, second and third. Toon adds a fourth; Encore has two inks, misregistration and grain; Chalk has a texture switch.
  • Aurora uses accent, secondary, radius (sharp, soft, round) and buttons (pill or rounded). Lava picks a ready-made palette or custom with ground, accent and secondary.
  • Many templates have a third font: mono (code, times or prices), hand (handwriting) or label.

Colors must be written as # plus six hex digits. Anything else (#fff, orange) is refused by the editor and by the build with “Use a hex color like #d6402e”.

In Keystatic: with npm run dev running, open /keystatic, then Brand. Pick or type the colors, choose the corners and fonts, and click Save. Colors and corners show on the next reload.

In the file: edit src/content/singletons/brand.json in your editor and save. The result is the same.

Fonts are the exception: they’re set up when the site starts. After changing a font, stop npm run dev (Ctrl C) and start it again.

You choose a few colors; the template works out all the others for both themes in src/lib/brand.ts and checks each pair against the WCAG AA contrast rules. In Handy:

Derived color Made from Rule
Buttons The accent, darkened White button text reaches 4.5:1
Links and accent-colored text The accent’s hue 4.5:1 against the page, cards and sunken panels, in each theme
Marks (ticks, icons, lines) The accent 3:1 against the backgrounds (the rule for graphics)
Fills (second, third) Your second and third colors, lightened Dark text on them reaches 4.5:1
Secondary text, borders, the night board Fixed shades of the template Checked against the backgrounds the same way

So the colors on the page aren’t always exactly the ones you typed: with Handy’s #f26a1b, the buttons come out a slightly darker orange, #c3520c, so their white text is readable. The lightness changes; the hue stays yours.

If no shade of a color can pass, the build (and npm run dev) stops with a message like:

Brand: no readable button color (white text on the accent) for these colors. Pick a different color in brand.json.

The message names the color that failed: the button, the link color in one theme, the second or third color, and so on. To fix it, pick a different color for that field in brand.json and save. With Handy’s way of deriving colors this is rare: the template keeps moving the lightness until the pair passes, and only stops if it reaches black or white without passing.

A typo stops it too: Brand: "#12345" is not a hex color like #1626a8.

Templates whose text sits directly on the accent (Toon and Slab, for example) need a light enough accent; the field’s description in the Brand screen tells you what each color is used for and which way it gets adjusted.

Handy offers:

Text font (fonts.body) Heading font (fonts.heading)
Figtree, Work Sans, Manrope, DM Sans, Barlow, Atkinson Hyperlegible Bricolage Grotesque, Archivo Black, Rubik, Bowlby One, Darker Grotesque, Sora

Each template has its own list, chosen to fit its look: BODY_FONTS and HEADING_FONTS (and MONO_FONTS or HAND_FONTS where there’s a third font) at the top of src/content/model.ts.

Fonts come from Fontsource through the Astro Fonts API, set in astro.config.mjs from brand.json:

fonts: [
{ name: brand.fonts.body, cssVariable: '--font-body', provider: fontProviders.fontsource(), weights: [400, 700], styles: ['normal'], subsets: ['latin'], fallbacks },
{ name: brand.fonts.heading, cssVariable: '--font-heading', provider: fontProviders.fontsource(), weights: [HEADING_WEIGHTS[brand.fonts.heading] ?? 800], styles: ['normal'], subsets: ['latin'], fallbacks },
],
  • The font files are downloaded when the site is built and served from your own site: no requests to a font service, nothing to declare in a cookie banner.
  • The text font loads in two weights (400 and 700), the heading font in one.
  • Astro adds a fallback font with matched metrics, so the page doesn’t jump when the font arrives.
  • Only the latin character set is included. If you add a language that needs more characters (for example Czech or Polish, which need latin-ext), add that subset to both lines.

Each heading font is drawn in one weight, listed in HEADING_WEIGHTS:

export const HEADING_WEIGHTS: Record<string, number> = { 'Bricolage Grotesque': 800, 'Archivo Black': 400, Rubik: 800, 'Bowlby One': 400, 'Darker Grotesque': 900, Sora: 800 };

The list is in two places, astro.config.mjs (which weight to download) and src/lib/brand.ts (which weight the headings use). Keep them the same. Aurora and Lava don’t have this list.

  1. Check that the font is on Fontsource and note its name exactly as Fontsource writes it, and, for a heading font, which weight you want.
  2. Add the name to BODY_FONTS or HEADING_FONTS in src/content/model.ts.
  3. For a heading font, add 'Font Name': weight to HEADING_WEIGHTS in both astro.config.mjs and src/lib/brand.ts.
  4. Put the name in brand.json (or pick it in the Brand screen) and restart npm run dev.
  5. Add the font and its license to LICENSES.md if you keep one for your site.

Every template has two themes, and every color is derived and checked for both. The visitor switches with the toggle in the header; the choice is remembered in their browser (theme in local storage) and set before the page is drawn, so there’s no flash.

Which theme opens first depends on the template: Handy always opens in the day theme (a pale pegboard), even when the visitor’s system is dark, and the toggle switches to the night (a charcoal board). Your template’s page in these docs says which it uses.

You don’t set the night colors yourself: they come from the same brand.json. In the code, the theme is the data-theme attribute on <html> (dark or light), and the colors are CSS variables (--brand-bg, --brand-text, --brand-link…) that src/lib/brand.ts writes into every page’s head. src/styles/brand.css only holds defaults.

corners sets two CSS variables, the corner radius of cards and panels and the radius of buttons. In Handy, round is 1rem (buttons 0.8rem) and soft 0.4rem. Other templates offer other values (square, sharp, rounded): the Brand screen lists them.

The logo is drawn in code, not an image file:

  • src/components/Logo.astro: a small SVG mark (in Handy, a wrench on a hi-vis block) next to your site’s name from Site settings → Name, linking to the home page. The mark uses the brand’s colors, so it follows your accent and both themes.
  • src/pages/favicon.svg.ts: the same mark as the browser-tab icon, drawn at build time in your brand’s colors and served at /favicon.svg.

Changing the name in Site settings is enough for most sites. To use your own mark:

  1. Replace the <svg> in src/components/Logo.astro with yours. Keep aria-hidden="true" on it (the link already has a label for screen readers) and color it with the theme variables (var(--brand-button), var(--brand-ink)…) rather than fixed colors, so it works in both themes.
  2. Put the same drawing in src/pages/favicon.svg.ts. It reads the colors from colors.light (from src/data/brand.ts), because a favicon can’t use CSS variables.

Prefer an image file? Put it in src/assets/ and show it with Astro’s <Image> in Logo.astro; for the favicon, put the file in public/ and change the <link rel="icon"> line in src/layouts/Base.astro.

The browser’s address bar color (theme-color) also comes from the brand: it’s the day theme’s background.