← bramble

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.

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

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.

← back to the plants