How this was built.
Bramble is a fictional home-goods & plant-care DTC brand — one site in a 25-site design showcase, each with its own identity, stack, and signature technique. These are the honest build notes.
The idea
Plant commerce has a trust problem: people don't avoid buying plants because they dislike plants — they avoid buying plants because they've killed plants. So the whole site is engineered around forgiveness. The headline is a pardon ("Plants that forgive you"), every plant is scored 1–5 for light, water, and effort in plain dots, the badges are blunt ("Hard to kill", "Thrives on neglect"), and the guarantee is written like a friend talking ("no photos of the crime scene required").
The patterns came from studying shipped plant-care and DTC products on Mobbin: Greg's plain-language care chips and first-plant-person microcopy, Little Amps Coffee's comparable meter trio on every product card, Yellowbird's corner badge pills and filled-icon heat meters, and Dollar Shave Club's confident quiz teaser. Plants are presented as characters with the Latin name demoted to mono small type — you bond with "The Introvert", not with Sansevieria taciturna.
The stack
Astro 5, static output, zero client framework. The choice is deliberate:
the page is 95% content that never changes — cards, illustrations, copy — and Astro
renders all of it to plain HTML at build time. The six plant cards and six hidden
quiz-result cards are the same component (PlantCard.astro)
rendered twelve times at build, so the quiz needs no client-side templating at all.
- One global stylesheet with design tokens in
:root; Astro hashes and inlines what it should. - One vanilla
<script>(TypeScript, bundled by Astro) for reveals, draw-ons, counters, and the quiz — about 130 lines. - Type: DM Serif Display for the botanical voice, Work Sans for body, DM Mono for care data — the mono is doing the "plant nursery label" job.
- GitHub Pages project site:
base: '/bramble-home', built to./docsso Pages serves straight from the main branch.
Signature technique 1 — organic blobs that morph
Every image mask and backdrop is a blob made from an eight-value
border-radius. Animating between three radius states makes the shape
slowly breathe — the hero photo is a living shape, not a rectangle:
.blob { border-radius: 58% 42% 55% 45% / 48% 56% 44% 52%; }
@keyframes blob-morph {
0%,100% { border-radius: 58% 42% 55% 45% / 48% 56% 44% 52%; }
33% { border-radius: 45% 55% 48% 52% / 56% 44% 58% 42%; }
66% { border-radius: 52% 48% 42% 58% / 44% 58% 46% 56%; }
}
.blob--morph { animation: blob-morph 18s ease-in-out infinite; }
The trick to keeping it organic: the horizontal and vertical radii never match, and the
background blob runs the same keyframes reversed at a different duration
(26s vs 18s), so the two shapes drift out of phase forever. Under
prefers-reduced-motion the animation is removed and the blob holds its
first frame — still a blob, just a calm one.
Signature technique 2 — botanical linework that draws itself
The vine down the hero edge and the CTA leaves are stroke-only SVGs. Each path gets
pathLength="1", which normalizes its length so one CSS rule animates
every path regardless of geometry:
<path pathLength="1" style="--i:1" d="M27 96 C 18 88 ..." />
.draw path {
stroke-dasharray: 1;
stroke-dashoffset: 1; /* hidden: dash exactly off the path */
transition: stroke-dashoffset 1.6s var(--ease-settle);
transition-delay: calc(var(--i, 0) * .22s);
}
.draw.drawn path { stroke-dashoffset: 0; } /* added by IntersectionObserver */
The stem is --i:0 and each leaf increments, so the vine grows stem-first,
leaves after — the same choreography a real vine would use. Without JavaScript the
html:not(.js) fallback sets the offset to zero, so the artwork is simply
already drawn.
Signature technique 3 — a quiz with no framework
The "find your plant" quiz is three chip-toggle questions scoring six plant profiles.
All six result cards are prerendered and hidden; the client script only picks which
hidden attribute to remove. Matching is a tiny penalty function —
distance from your light, water demands over your attention budget, and a hard filter
on pet safety:
let score = -Math.abs(plant.sun - sunPref) * 2
- Math.max(0, plant.water - attentionCap) * 2
- Math.max(0, plant.effort - attentionCap) * 3;
if (care === 'hover') score += Math.min(plant.water, 4) * .75;
if (pets === 'yes' && !plant.petSafe) continue; // hard filter
Effort is penalized harder than water (×3 vs ×2) because giving a forgetful person a
calathea is how you lose a customer. The chips are real <button>
elements with aria-pressed, the whole thing is keyboard-operable, and the
result container is an aria-live="polite" region so screen readers hear
the match change.
Details that matter
- The easing is the brand.
--ease-grow: cubic-bezier(.26,1.25,.38,1)has a soft overshoot — things settle like a stem finding vertical. Reveals use a damped companion curve so text never bounces. - Care meters are honest UI, not decoration. Filled dots are
aria-hidden, with a visually-hidden "2 of 5" so the data survives a screen reader. - Contrast was checked, not vibes-checked. Fern on ecru is ~7:1 and carries all text; the leaf and clay tones sit below 3:1, so they're restricted to icons, dots, stripes, and rules — never prose.
- One photograph, everything else drawn. The pothos hero is the only raster asset; all six plants, four tools, vines, and leaves are hand-written SVG paths inheriting the palette via
var(--fern). - Progressive enhancement. A synchronous one-liner adds
.jsto<html>; every reveal, draw-on, and counter has a no-JS and reduced-motion final state. Content is never permanently hidden. - The illustrations dodge an id collision. Because each plant renders twice (grid + quiz result), every SVG's
<defs>ids get a per-render suffix.
Ship it
Astro builds the static site into docs/, and GitHub Pages serves it from the main branch:
// astro.config.mjs
export default defineConfig({
site: 'https://bswxyz.github.io',
base: '/bramble-home',
outDir: './docs',
}); npm run build
git add -A && git commit
gh repo create bswxyz/bramble-home --public --source . --push
gh api --method POST /repos/bswxyz/bramble-home/pages \
-f "source[branch]=main" -f "source[path]=/docs"
A .nojekyll file in public/ keeps Pages from running the
output through Jekyll. Total payload is one compressed photo, one CSS file, one small
JS bundle, and three font families — the rest is markup.