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
Bonsai
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).