Files
iammiti99/themes/bonsai
tiennm99 401296a468 feat(theme): v0.4 a11y + polish pass
Address the 17 findings from the 260510 UI/UX audit (10 shipped in the
review pass, 7 deferred items + Q4 noscript guard tackled here).

A11y
- sakura accent #d4456a -> #c93f63 (4.04 -> 4.49 vs bg, WCAG AA)
- koi    accent #c8521e -> #bd4c1c (4.17 -> 4.63 vs bg, WCAG AA)
- /themes/, /variants/ wrapped in <section aria-labelledby> keyed
  to the page <h1>; variant cards demoted from <section> to <article>
- prior pass: koi-light --bonsai-muted #8a6f5c -> #7a5e48; gallery
  card headings <h3> -> <h2>; theme-toggle 36px -> 44px hit target;
  reduced-motion extended to .theme-toggle and :active

Polish
- favicon system: opt-in params.faviconSvg + params.appleTouchIcon
  with conditional <link> tags; existing favicon path unchanged
- .link:hover adds subtle accent tint via color-mix(in oklab, ...)
  alongside existing border + 1px lift
- theme toggle hidden via <noscript> when JS disabled (button is JS-only)
- prior pass: inline FOUC-blocking script in <head>; avatar SVG uses
  currentColor + safeCSS; fallback external-link icon matches Lucide
  stroke style; .link gains position:relative; theme-toggle :active

i18n
- theme-toggle aria-label/title use {{ with i18n }}{{ . }}{{ else }}
  fallback so missing translation keys never leak into the DOM

Internal
- drop non-standard data-theme="auto" from <html>; CSS only ever
  queried light/dark, the auto value was a no-op
- README CSS budget claim relaxed from "< 3 KB gzipped" to "~ 3 KB"
  to match reality (currently 3,113 B minified+gzipped)

Verification
- hugo --gc --minify clean (no warnings)
- rendered HTML at /, /themes/, /variants/ inspected via curl
- WCAG ratios re-computed for sakura+koi against bg and surface
- no headless browser available on host; visual screenshots not produced

Plan: plans/260510-0107-deferred-ui-ux-fixes/
Audit: plans/reports/ui-ux-review-260510-0026-bonsai-theme.md
2026-05-10 02:14:26 +07:00
..
2026-05-01 10:04:59 +07:00
2026-05-01 10:04:59 +07:00
2026-05-10 02:14:26 +07:00

Bonsai

build license Hugo

A minimalist Hugo theme for link-in-bio pages, inspired by Linktree and the Japanese art of bonsai — small, curated, intentional.

→ Live demo · Icon gallery · Layout variants · Color themes

盆栽 (bonsai): "tray planting" — the art of growing miniature trees through patient, deliberate cultivation. Every branch placed with care.

Bonsai treats your bio page the same way: a quiet, well-pruned page that surfaces only what matters — your name, who you are, and where people can find you.

Features

  • Single-page bio — name, avatar, tagline, links. Nothing else.
  • Data-driven links — defined in [[params.links]]; no content files required.
  • 35 icons out of the box — 25 brand (GitHub, Mastodon, Bluesky, X, Threads, LinkedIn, Instagram…) + 10 utility (mail, globe, rss…). Vendored from Simple Icons and Lucide.
  • Light & dark mode — respects prefers-color-scheme; optional toggle.
  • Zero JavaScript by default — pure HTML + CSS; opt-in JS for theme toggle only.
  • Fast — ~3 KB gzipped CSS, no web fonts (system stack), no runtime fetches.
  • Accessible — semantic HTML, focus-visible outlines, prefers-reduced-motion.
  • Responsive — mobile-first, looks right at every viewport.

Quick Start

As a Git submodule (simplest)

git submodule add https://github.com/tiennm99/bonsai.git themes/bonsai

Add to hugo.toml:

theme = "bonsai"

As a Hugo Module

hugo mod init github.com/<you>/<your-site>
hugo mod get github.com/tiennm99/bonsai

Add to hugo.toml:

[module]
  [[module.imports]]
    path = "github.com/tiennm99/bonsai"

Configuration

Minimal hugo.toml:

baseURL = "https://example.com/"
title   = "Your Name"
theme   = "bonsai"

# Single-page bio — disable everything Hugo doesn't need.
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]

[params]
  name    = "Your Name"
  tagline = "Tending my little corner of the internet"
  bio     = "Short bio. One sentence is plenty."
  avatar  = "/images/avatar.jpg"

  [[params.links]]
    title = "GitHub"
    url   = "https://github.com/yourname"
    icon  = "github"

  [[params.links]]
    title = "Email"
    url   = "mailto:you@example.com"
    icon  = "mail"

All parameters

Param Type Default Description
name string site title Display name shown as <h1>.
tagline string — One-liner under the name.
bio string (markdown) — Short bio paragraph. Markdown supported.
avatar string (URL) — Avatar image path. If unset, theme renders an SVG circle with auto-derived initials.
avatarInitials string first letters of name Override the initials when no avatar is set.
avatarBg string (CSS color) var(--bonsai-accent) Background color of the initials circle.
favicon string (URL) /favicon.ico Favicon path (.ico fallback).
faviconSvg string (URL) — Optional SVG favicon. Modern browsers prefer this when set.
appleTouchIcon string (URL) — 180×180 PNG for iOS home-screen / Safari pinned tabs.
colorTheme string bonsai Palette: bonsai, sakura, sumi, or koi. See Color themes.
layout string stack Link arrangement: stack, grid, or inline. See Layout variants.
themeToggle bool false Render a sun/moon button in the footer + load the toggle script.
ogImage bool true Set false to suppress all og:image / twitter:image tags.
ogImageUrl string (URL) — Explicit OG preview image (1200×630 recommended). Overrides the avatar fallback and upgrades Twitter card to summary_large_image.
schema bool true Emit schema.org Person JSON-LD in <head>. Set false if you provide your own.
jobTitle string — Optional Person.jobTitle field for JSON-LD.
location string — Optional Person.address field for JSON-LD.
email string — Optional Person.email field for JSON-LD.
footer bool true Show the footer.
footerText string (HTML) © {year} {name} Override footer text. HTML allowed.
links array — Bio links. See below.

Each [[params.links]] entry:

Field Type Required Description
title string yes Link label.
url string yes Link target. mailto: and tel: are rendered without target=_blank.
icon string no Icon name from the available set (see below). Unknown names render a generic external-link glyph.

Color themes

Four built-in palettes, each with light + dark variants. Set colorTheme in [params]:

Name Vibe Accent
bonsai (default) washi paper + vermilion seal #8b3a2b
sakura cherry blossom pink #c93f63
sumi monochrome ink #1a1a1a
koi orange + cream #bd4c1c

Live preview: tiennm99.github.io/bonsai/themes/.

Layout variants

Three arrangements for [[params.links]]. Pick one via layout in [params]:

Value Look Best for
stack (default) Full-width vertical buttons ≤ 6 link bios; the classic Linktree shape.
grid Two-column responsive grid (collapses to one column under 480 px) 6–12 links; balances density and tap-target size.
inline Icon-only horizontal row Lots of accounts, short page; titles stay in DOM for screen readers.

Live preview: tiennm99.github.io/bonsai/variants/.

i18n

Theme-rendered strings (nav landmark, theme-toggle labels, default footer) live in i18n/{lang}.toml. Bundles for en and vi ship with the theme.

To use another language, set Hugo's defaultContentLanguage and add a matching i18n/{lang}.toml:

defaultContentLanguage = "fr"
# i18n/fr.toml
[nav_links_label]
other = "Liens"
[theme_toggle_label]
other = "Basculer le thème clair / sombre"
[theme_toggle_title]
other = "Basculer le thème"
[footer_default]
other = "© {{ .year }} {{ .name }}"

Missing keys fall back to en. User content (name, tagline, bio, link titles, footerText) is never auto-translated — it stays user-owned.

Available Icons

Brand / Social (25)
Name Source
github Simple Icons
gitlab Simple Icons
mastodon Simple Icons
bsky Simple Icons
x Simple Icons
threads Simple Icons
linkedin Simple Icons
instagram Simple Icons
facebook Simple Icons
tiktok Simple Icons
youtube Simple Icons
twitch Simple Icons
discord Simple Icons
telegram Simple Icons
signal Simple Icons
whatsapp Simple Icons
reddit Simple Icons
medium Simple Icons
devto Simple Icons
substack Simple Icons
hashnode Simple Icons
kofi Simple Icons
patreon Simple Icons
buymeacoffee Simple Icons
paypal Simple Icons
UI / Utility (10)
Name Source
mail Lucide
globe Lucide
link Lucide
rss Lucide
calendar Lucide
phone Lucide
mappin Lucide
filetext Lucide
extlink Lucide
share Lucide

Icons are vendored at build time — no CDN fetch at runtime. Live gallery: tiennm99.github.io/bonsai/icons/.

To refresh or add icons, edit scripts/sync-icons.sh and data/icons.yaml, then re-run the script. See CONTRIBUTING.md.

Development

git clone https://github.com/tiennm99/bonsai.git
cd bonsai/exampleSite
hugo server --themesDir ../.. --bind 0.0.0.0

Build for inspection:

cd exampleSite && hugo --themesDir ../.. --gc --minify

Contributing

PRs welcome. See CONTRIBUTING.md for dev setup, the icon-add workflow, and PR guidelines.

License

Apache-2.0 © tiennm99. See LICENSE and NOTICE for third-party attributions (Simple Icons CC0, Lucide ISC).