| name | grove-ui-design |
| description | Create warm, nature-themed UI for Grove with glassmorphism, seasonal decorations, randomized forests, and accessible design patterns. Use when building pages, enhancing UI, or adding decorative elements. |
Grove UI Design Skill
When to Activate
Activate this skill when:
- Creating or enhancing pages for Grove sites
- Adding decorative nature elements (trees, clouds, weather effects)
- Implementing glassmorphism effects for readability
- Working with the seasonal theme system
- Building navigation patterns (navbar, mobile overflow menus)
- Creating "story" pages that guide users through content
- Ensuring mobile-friendly, accessible UI
- Choosing icons or visual elements
The Grove Aesthetic
Grove is a place. It's nature-themed, warm, and inviting—like a midnight tea shop with good documentation.
Core Principles
Warm, introspective, queer, unapologetically building something meaningful.
Write with the warmth of a midnight tea shop and the clarity of good documentation.
Every design choice should feel:
- Welcoming — like entering a cozy space
- Organic — natural, not rigid or corporate
- Readable — content-first, decorations enhance, never obstruct
- Alive — subtle animations, seasonal changes, randomization
Grove Mode & Terminology (GroveTerm V2)
Grove has a terminology system that automatically switches between Grove-themed terms and standard terms based on the user's Grove Mode setting. Always use GroveTerm components instead of hardcoding Grove terminology in UI.
By default, Grove Mode is OFF for new visitors. They see familiar terms: "Posts" instead of "Blooms", "Dashboard" instead of "Arbor", "Support" instead of "Porch". When users opt in via the footer toggle, they see the full nature-themed vocabulary with interactive definitions.
The Component Suite:
import {(GroveTerm, GroveSwap, GroveText, GroveSwapText, GroveIntro)} from '@autumnsgrove/lattice/ui';
import groveTermManifest from '$lib/data/grove-term-manifest.json';
| Component | Use Case | Behavior |
|---|
GroveTerm | Interactive terms with popup definitions | Colored underline when ON, click for popup. Shows standard term when OFF. |
GroveSwap | Silent text replacement | Reactively swaps text. No underline, no interaction. |
GroveText | Parse [[term]] syntax in data strings | Renders [[bloom|posts]] as interactive GroveTerm components. |
GroveSwapText | Parse [[term]] syntax silently | Same parsing, silent swaps (no popups). |
GroveIntro | "We call it X" page banners | Standardized intro below page titles. |
Usage:
<!-- Interactive term with popup -->
<GroveTerm term="bloom" manifest={groveTermManifest} />
<!-- Custom display text -->
<GroveTerm term="wanderer" manifest={groveTermManifest}>wanderers</GroveTerm>
<!-- Silent swap (no popup, no underline) -->
<GroveSwap term="arbor" manifest={groveTermManifest} />
<!-- Parse [[term]] syntax in data strings (ideal for FAQ items, pricing, etc.) -->
<GroveText
content="Your [[bloom|posts]] live in your [[garden|blog]]."
manifest={groveTermManifest}
/>
<!-- Silent text swap (non-interactive, default behavior) -->
<GroveTerm term="meadow" />
<!-- With leaf icon for Grove Mode hint -->
<GroveTerm term="meadow" icon />
Key Rules:
- Never hardcode Grove terms in user-facing UI. Always use GroveTerm components.
- Default is OFF for new visitors. Standard, familiar terminology first.
- URLs stay as Grove terms (
/porch, /garden) regardless of display mode.
- Brand terms (Grove) and subscription tiers (Seedling/Sapling/Oak/Evergreen) always show as-is.
- Use
[[term]] syntax for data-driven content (FAQ items, pricing fineprint, feature lists).
- Grove Mode store:
groveModeStore from @autumnsgrove/lattice/ui/stores. Toggle lives in the footer.
See docs/grove-user-identity.md for the full identity language documentation and libs/engine/src/lib/ui/components/ui/groveterm/ for component source.
Glassmorphism Pattern
Glass effects create readability while revealing hints of background decoration.
The Layering Formula
Background (gradients, vines, nature)
↓
Decorative Elements (trees, clouds, particles)
↓
Glass Surface (translucent + blur)
↓
Content (text, cards, UI)
Glass Components
import {(Glass, GlassCard, GlassButton, GlassOverlay)} from '@lattice/ui/ui';
<!-- Container with glass effect -->
<Glass variant="tint" class="p-6 rounded-xl">
<p>Readable text over busy backgrounds</p>
</Glass>
<!-- Card with glass styling -->
<GlassCard title="Settings" variant="default" hoverable>Content here</GlassCard>
<!-- Glass button -->
<GlassButton variant="accent">Subscribe</GlassButton>
Glass Variants
| Variant | Use Case | Light Mode | Dark Mode |
|---|
surface | Headers, navbars | 95% white | 95% slate |
tint | Text over backgrounds | 60% white | 50% slate |
card | Content cards | 80% white | 70% slate |
accent | Callouts, highlights | 30% accent | 20% accent |
overlay | Modal backdrops | 50% black | 60% black |
muted | Subtle backgrounds | 40% white | 30% slate |
CSS Utility Classes
<div class="glass rounded-xl p-4">Basic glass</div>
<div class="glass-tint p-6">Text container</div>
<div class="glass-accent p-4">Highlighted section</div>
<nav class="glass-surface sticky top-0">Navbar</nav>
Key Pattern: Sticky Navigation
<nav
class="sticky top-[73px] z-30 bg-white/80 dark:bg-slate-900/80 backdrop-blur-sm border-b border-divider"
>
<!-- Navigation content -->
</nav>
Seasonal Theme System
Grove uses four seasons, each with distinct colors, weather effects, and moods.
Season Detection
import {season} from '$lib/stores/season'; const isSpring = $derived($season === 'spring'); const isAutumn
= $derived($season === 'autumn'); const isWinter = $derived($season === 'winter'); // Summer is the default
(no flag needed)
Color Palette System
Import from: @autumnsgrove/lattice/ui/nature or $lib/components/nature/palette
Core Palettes (Year-Round)
Accent colors: These palette hex values are for nature SVGs, illustrations, and brand use. For CSS accent-colored surfaces (buttons, links, highlights), use var(--grove-accent-*) tokens instead — the user's accent color may not be green. See the accent scale: var(--grove-accent) (solid), var(--grove-accent-dark) (hover), var(--grove-accent-N) (opacity tints, stops 5-80). Pre-commit hook enforces this.
import { greens, bark, earth, natural } from "@autumnsgrove/lattice/ui/nature";
greens.darkForest;
greens.deepGreen;
greens.grove;
greens.meadow;
greens.spring;
greens.mint;
greens.pale;
bark.darkBark;
bark.bark;
bark.warmBark;
bark.lightBark;
(earth.soil, earth.mud, earth.clay, earth.sand, earth.stone, earth.pebble, earth.slate);
(natural.cream, natural.aspenBark, natural.bone, natural.mushroom, natural.birchWhite);
Spring Palettes
import {
springFoliage,
springSky,
wildflowers,
cherryBlossoms,
cherryBlossomsPeak,
} from "@autumnsgrove/lattice/ui/nature";
springFoliage.sprout;
springFoliage.newLeaf;
springFoliage.freshGreen;
springFoliage.budding;
springFoliage.tender;
springSky.clear;
springSky.soft;
wildflowers.buttercup;
wildflowers.daffodil;
wildflowers.crocus;
wildflowers.violet;
wildflowers.purple;
wildflowers.lavender;
wildflowers.tulipPink;
wildflowers.tulipRed;
wildflowers.white;
cherryBlossoms.deep;
cherryBlossoms.standard;
cherryBlossoms.light;
cherryBlossoms.pale;
cherryBlossoms.falling;
cherryBlossomsPeak.deep;
cherryBlossomsPeak.standard;
cherryBlossomsPeak.light;
cherryBlossomsPeak.pale;
cherryBlossomsPeak.falling;
Unified Flowers Palette (NEW!)
The flowers namespace consolidates all flower colors into one organized structure:
import { flowers } from "@autumnsgrove/lattice/ui/nature";
flowers.wildflower.buttercup;
flowers.wildflower.daffodil;
flowers.wildflower.crocus;
flowers.wildflower.violet;
flowers.wildflower.purple;
flowers.wildflower.lavender;
flowers.wildflower.tulipPink;
flowers.wildflower.tulipRed;
flowers.wildflower.white;
flowers.cherry.deep;
flowers.cherry.standard;
flowers.cherry.light;
flowers.cherry.pale;
flowers.cherry.falling;
flowers.cherryPeak.deep;
flowers.cherryPeak.standard;
flowers.cherryPeak.light;
flowers.cherryPeak.pale;
flowers.cherryPeak.falling;
Use flowers.wildflower instead of accents.flower — the accents version is deprecated.
Autumn & Winter Palettes
import { autumn, autumnReds, winter } from "@autumnsgrove/lattice/ui/nature";
autumn.rust;
autumn.ember;
autumn.pumpkin;
autumn.amber;
autumn.gold;
autumn.honey;
autumn.straw;
autumnReds.crimson;
autumnReds.scarlet;
autumnReds.rose;
autumnReds.coral;
(winter.snow, winter.frost, winter.ice, winter.glacier);
(winter.frostedPine, winter.winterGreen, winter.coldSpruce);
(winter.winterSky, winter.twilight, winter.overcast);
(winter.bareBranch, winter.frostedBark, winter.coldWood);
(winter.hillDeep, winter.hillMid, winter.hillNear, winter.hillFront);
Accent Palettes
import { accents, wildflowers } from "@autumnsgrove/lattice/ui/nature";
(accents.mushroom.redCap, accents.mushroom.orangeCap, accents.mushroom.brownCap);
(accents.mushroom.spots, accents.mushroom.gill);
(accents.firefly.glow, accents.firefly.warmGlow, accents.firefly.body);
(accents.berry.ripe, accents.berry.elderberry, accents.berry.red);
(accents.water.surface, accents.water.deep, accents.water.shallow, accents.water.lily);
(accents.sky.dayLight, accents.sky.dayMid, accents.sky.sunset, accents.sky.night, accents.sky.star);
(accents.bird.cardinalRed, accents.bird.cardinalMask, accents.bird.cardinalBeak);
(accents.bird.chickadeeCap, accents.bird.chickadeeBody, accents.bird.chickadeeBelly);
(accents.bird.robinBody, accents.bird.robinBreast, accents.bird.robinBeak);
(accents.bird.bluebirdBody, accents.bird.bluebirdWing, accents.bird.bluebirdBreast);
Seasonal Helper Functions
import {
getSeasonalGreens,
getCherryColors,
isTreeBare,
pickRandom,
pickFrom,
} from "@autumnsgrove/lattice/ui/nature";
const foliage = getSeasonalGreens(season);
const cherryColors = getCherryColors(season);
if (isTreeBare("cherry", "winter")) {
}
const randomGreen = pickRandom(greens);
const specificGreen = pickFrom(greens, ["grove", "meadow"]);
Deprecated Aliases (Still Work)
import { spring, pinks, springBlossoms } from "@autumnsgrove/lattice/ui/nature";
Season Mood Summary
| Season | Primary Colors | Mood |
|---|
| Spring | springFoliage, cherryBlossomsPeak, wildflowers | Renewal, hope |
| Summer | greens, cherryBlossoms | Growth, warmth |
| Autumn | autumn, autumnReds | Harvest, reflection |
| Winter | winter (frost, snow, frosted pines) | Rest, stillness |
Seasonal Weather Effects
<!-- Winter: Snowfall -->
{#if isWinter}
<SnowfallLayer count={40} zIndex={5} opacity={{ min: 0.4, max: 0.8 }} spawnDelay={8} />
{/if}
<!-- Spring: Cherry blossom petals -->
{#if isSpring}
<FallingPetalsLayer count={80} zIndex={100} opacity={{ min: 0.5, max: 0.9 }} />
{/if}
<!-- Autumn: Falling leaves (tied to trees) -->
{#if isAutumn}
<FallingLeavesLayer
trees={forestTrees}
season={$season}
minLeavesPerTree={2}
maxLeavesPerTree={4}
/>
{/if}
Seasonal Background Gradients
<main class="min-h-screen transition-colors duration-1000
{isWinter ? 'bg-gradient-to-b from-slate-200 via-slate-100 to-slate-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-700' : ''}
{isAutumn ? 'bg-gradient-to-b from-orange-100 via-amber-50 to-yellow-50 dark:from-slate-900 dark:via-amber-950 dark:to-orange-950' : ''}
{isSpring ? 'bg-gradient-to-b from-pink-50 via-sky-50 to-lime-50 dark:from-slate-900 dark:via-pink-950 dark:to-lime-950' : ''}
{/* Summer default */} 'bg-gradient-to-b from-sky-100 via-sky-50 to-emerald-50 dark:from-slate-900 dark:via-slate-800 dark:to-emerald-950'
">
When to Use Seasons
- Roadmap pages — Show progress through seasonal metaphor
- Story/about pages — Create atmosphere and emotional connection
- Interactive demos — Let users toggle seasons (like /forest)
- Help articles — Consider seasonal decor to break up long content
- Anywhere you want magic — Use judgment based on page purpose
Randomized Forests
The forest should feel alive and different every visit.
Tree Generation Pattern
interface GeneratedTree {
id: number;
x: number;
size: number;
aspectRatio: number;
treeType: TreeType;
opacity: number;
zIndex: number;
}
const TREE_ASPECT_RATIO_RANGE = { min: 1.0, max: 1.5 };
function generateSectionTrees(count: number): GeneratedTree[] {
const trees: GeneratedTree[] = [];
const usedPositions: number[] = [];
for (let i = 0; i < count; i++) {
let x: number;
let attempts = 0;
do {
x = 5 + Math.random() * 88;
attempts++;
} while (usedPositions.some((pos) => Math.abs(pos - x) < 8) && attempts < 20);
usedPositions.push(x);
const size = 80 + Math.random() * 80;
const aspectRatio = 1.0 + Math.random() * 0.5;
const opacity = 0.5 + Math.random() * 0.4;
const zIndex = size > 130 ? 3 : size > 100 ? 2 : 1;
trees.push({
id: i,
x,
size,
aspectRatio,
treeType: pickRandom(treeTypes),
opacity,
zIndex,
});
}
return trees.sort((a, b) => a.x - b.x);
}
Regeneration Timing
- On mount — Trees generate once when page loads
- On resize (significant) — Only if viewport bracket changes dramatically
- Never on scroll — Keep forest stable during reading
Rendering Trees
{#each forestTrees as tree (tree.id)}
<div
class="absolute"
style="
left: {tree.x}%;
bottom: 0;
width: {tree.size}px;
height: {tree.size * tree.aspectRatio}px;
opacity: {tree.opacity};
z-index: {tree.zIndex};
transform: translateX(-50%);
"
>
{#if tree.treeType === "logo"}
<Logo class="w-full h-full" season={$season} animate />
{:else if tree.treeType === "pine"}
<TreePine class="w-full h-full" season={$season} animate />
{:else if tree.treeType === "cherry"}
<TreeCherry class="w-full h-full" season={$season} animate />
{:else if tree.treeType === "aspen"}
<TreeAspen class="w-full h-full" season={$season} animate />
{:else if tree.treeType === "birch"}
<TreeBirch class="w-full h-full" season={$season} animate />
{/if}
</div>
{/each}
Responsive Density
function calculateDensity(): number {
const width = window.innerWidth;
if (width < 768) return 1;
if (width < 1024) return 1.3;
if (width < 1440) return 1.8;
if (width < 2560) return 2.5;
return 3.5;
}
Nature Components
Grove has an extensive library of decorative components. Explore with:
ls landing/src/lib/components/trees/
ls landing/src/lib/components/nature/trees/
ls landing/src/lib/components/nature/weather/
ls landing/src/lib/components/nature/sky/
ls landing/src/lib/components/nature/botanical/
ls landing/src/lib/components/nature/ground/
ls landing/src/lib/components/nature/structural/
ls landing/src/lib/components/nature/creatures/
Key Components
| Component | Use | Example Props |
|---|
Logo | Grove tree, seasonal | season, animate, breathing |
TreePine | Evergreen, stays green in autumn | season, animate |
TreeCherry | Blossoms in spring, bare in winter | season, animate |
TreeAspen / TreeBirch | Deciduous, seasonal colors | season, animate |
Cloud | Decorative sky element | variant, animate, speed, direction |
SnowfallLayer | Winter particles | count, opacity, spawnDelay |
FallingPetalsLayer | Spring cherry blossoms | count, opacity, fallDuration |
FallingLeavesLayer | Autumn leaves (tied to trees) | trees, season |
Cardinal / Chickadee | Winter birds | facing |
Robin / Bluebird | Spring birds | facing |
Vine | Decorative ivy/vines | varies |
Lantern | Warm glow points | varies |
Birds by Season
<!-- Winter birds -->
{#if isWinter}
<Cardinal facing="right" style="..." />
<Chickadee facing="left" style="..." />
{/if}
<!-- Spring birds -->
{#if isSpring}
<Robin facing="right" style="..." />
<Bluebird facing="left" style="..." />
{/if}
Midnight Bloom Palette
For dreamy, far-future, mystical content. The tea shop that exists at the edge of tomorrow.
import { midnightBloom } from "$lib/components/nature/palette";
midnightBloom.deepPlum;
midnightBloom.purple;
midnightBloom.violet;
midnightBloom.amber;
midnightBloom.warmCream;
midnightBloom.softGold;
Midnight Bloom Styling
<section class="bg-gradient-to-b from-orange-950/50 via-purple-950 to-slate-950">
<!-- Stars -->
<StarCluster count={12} class="absolute top-12 left-[10%]" />
<!-- Moon -->
<Moon phase="waning-crescent" class="absolute top-20 right-[15%] w-16 h-16 opacity-60" />
<!-- Fireflies -->
<Firefly count={8} class="absolute inset-0" />
<!-- Content with purple glass -->
<blockquote class="bg-purple-900/30 backdrop-blur-sm border border-purple-700/30 rounded-lg p-6">
<p class="text-purple-200 italic">Dreamy quote here...</p>
</blockquote>
</section>
Icons: Lucide Only
NEVER use emojis. ALWAYS use Lucide icons.
import {(MapPin, Check, Leaf, Trees, Mail)} from '@lucide/svelte';
<!-- Good -->
<MapPin class="w-4 h-4" />
<Check class="w-5 h-5 text-green-500" />
<!-- Bad - NEVER do this -->
<!-- ❌ 🌱 📧 ✅ -->
Standardized Icon Mapping
Use these icons consistently across the project:
| Concept | Icon | Notes |
|---|
| Navigation | | |
| Home | Home | |
| About | Info | |
| Vision | Telescope | Looking forward |
| Roadmap | Map | Journey/direction |
| Pricing | HandCoins | Money/currency |
| Knowledge | BookOpen | Learning/docs |
| Forest | Trees | Community blogs |
| Blog | PenLine | Writing |
| Features | | |
| Email | Mail | |
| Storage | HardDrive | |
| Theming | Palette | Customization |
| Authentication | ShieldCheck | Security |
| Cloud | Cloud | Remote/serverless |
| Search | SearchCode | Code/advanced search |
| Archives | Archive | Backups |
| Upload | Upload | |
| Video | Video | |
| Comments | MessagesSquare | User discussions |
| GitHub | Github | External links to GitHub |
| States | | |
| Success | Check | Completed/valid |
| Error | X | Failed/close |
| Loading | Loader2 | With animate-spin |
| Content | | |
| Posts | FileText | Blog posts |
| Tags | Tag | Categorization |
| Growth | Sprout | Grove brand, new beginnings |
| Heart | Heart | Love, care |
| External | ExternalLink | Opens new tab |
| Location | MapPin | Current position |
| Phases | | |
| Coming Soon | Seedling | Something growing |
| Refinement | Gem | Polish, quality |
| The Dream | Sparkles | Mystical (use sparingly!) |
| Night | Star | Midnight themes |
| Actions | | |
| Getting Started | Compass | Guidance |
| What's New | Megaphone | Announcements |
| Next Steps | Lightbulb | Ideas |
Icon Mapping Tables in Files
Create a consistent icon map at the top of each component/page that uses icons:
import {
Mail,
HardDrive,
Palette,
ShieldCheck,
Cloud,
SearchCode,
Archive,
Upload,
MessagesSquare,
Github,
Check,
X,
Loader2,
FileText,
Tag,
Sprout,
Heart,
ExternalLink,
MapPin,
} from "@lucide/svelte";
export const featureIcons = {
mail: Mail,
harddrive: HardDrive,
palette: Palette,
shieldcheck: ShieldCheck,
cloud: Cloud,
searchcode: SearchCode,
} as const;
export const stateIcons = {
success: Check,
error: X,
loading: Loader2,
} as const;
Then use in components:
<script lang="ts">
import { featureIcons } from "$lib/utils/icons";
</script>
{#each features as feature}
<svelte:component this={featureIcons[feature.icon]} class="w-5 h-5" />
{/each}
Benefits:
- Single source of truth for all icons
- Prevents undefined icon errors
- Easy to maintain and extend
- Reusable across entire project
Icon Usage Guidelines
- Always use icon maps - Never hardcode icon imports in every component
- Avoid overusing Sparkles - Reserve for truly mystical/magical contexts
- Be consistent - Use the same icon for the same concept everywhere
- Semantic meaning - Choose icons that convey meaning, not just decoration
- Export from central utility - Use
landing/src/lib/utils/icons.ts for all icon sets
Icon Sizing
<!-- Inline with text -->
<span class="inline-flex items-center gap-1.5">
<Leaf class="w-4 h-4" /> Feature name
</span>
<!-- Button icon -->
<button class="p-2">
<Menu class="w-5 h-5" />
</button>
<!-- Large decorative -->
<Gem class="w-8 h-8 text-amber-400" />
Icon Composition (Building Block Pattern)
Philosophy: "The grove doesn't need to be drawn. It just needs to be arranged."
For creating custom logos, illustrations, or decorative elements, compose existing Lucide icons rather than drawing custom SVG from scratch. This ensures visual consistency with the icon system.
Why This Pattern?
- Consistency — Icons match the Lucide aesthetic (24x24 grid, 2px strokes, round caps)
- Minimal custom code — Let Lucide do the heavy lifting
- Maintainable — Updating Lucide updates your compositions
- MIT licensed — All paths come from open-source icons
How to Extract Lucide Paths
Lucide icons use a 24×24 viewBox with 2px strokes. Extract paths directly from source:
curl -s https://raw.githubusercontent.com/lucide-icons/lucide/main/icons/tree-pine.svg
Key Lucide icon paths for Grove compositions:
const treePine = {
canopy:
"m17 14 3 3.3a1 1 0 0 1-.7 1.7H4.7a1 1 0 0 1-.7-1.7L7 14h-.3a1 1 0 0 1-.7-1.7L9 9h-.2A1 1 0 0 1 8 7.3L12 3l4 4.3a1 1 0 0 1-.8 1.7H15l3 3.3a1 1 0 0 1-.7 1.7H17Z",
trunk: "M12 22v-3",
};
const treeDeciduous = {
canopy:
"M8 19a4 4 0 0 1-2.24-7.32A3.5 3.5 0 0 1 9 6.03V6a3 3 0 1 1 6 0v.04a3.5 3.5 0 0 1 3.24 5.65A4 4 0 0 1 16 19Z",
trunk: "M12 19v3",
};
const moon =
"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401";
const flame =
"M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4";
Composing with SVG Transforms
Use <g transform="..."> to position, scale, and rotate icons:
<svg viewBox="0 0 48 32" fill="none">
<!-- Left tree (larger, foreground) -->
<g
transform="translate(2, 4) scale(0.85)"
stroke={color}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d={treePine.canopy} />
<path d={treePine.trunk} />
</g>
<!-- Right tree (smaller, background, tilted) -->
<g
transform="translate(20, 8) scale(0.65) rotate(-5, 12, 12)"
stroke={color}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
opacity="0.7"
>
<path d={treePine.canopy} />
<path d={treePine.trunk} />
</g>
<!-- Add simple custom elements sparingly -->
<circle cx="30" cy="10" r="1.5" fill={glowColor} opacity="0.8" />
<!-- firefly -->
<path d="M2 28h44" stroke={color} stroke-width="1.5" opacity="0.3" />
<!-- ground -->
</svg>
Transform Cheatsheet
| Transform | Effect | Example |
|---|
translate(x, y) | Move origin | translate(20, 8) moves icon right 20, down 8 |
scale(s) | Uniform size | scale(0.65) makes icon 65% size |
rotate(deg, cx, cy) | Rotation around point | rotate(-5, 12, 12) tilts 5° around center |
| Combined | Chain transforms | translate(20, 8) scale(0.65) rotate(-5, 12, 12) |
Example: Grove Logo Compositions
See /landing/src/lib/components/logo-concepts/ for real implementations:
| Logo | Composition |
|---|
LogoFireflyForest | TreePine + TreeDeciduous + glowing circles |
LogoGatheringHearth | Two trees angled toward center Flame |
LogoStarlightPines | Two TreePines + Moon + star circles |
LogoShelter | Two TreePines forming archway + Moon |
LogoWinterGrove | TreePines + snow line accents |
Guidelines
- Use Lucide paths as primary structure — Trees, moon, flame, etc.
- Custom SVG only for simple primitives — circles (fireflies), lines (ground, snow)
- Maintain Lucide styling — 2px strokes, round caps/joins, consistent opacity
- Create depth with opacity/scale — Larger = foreground (opacity 0.9), smaller = background (0.5-0.7)
- Keep viewBox aspect ratios reasonable — 40×32 or 48×32 for horizontal compositions
When to Use
- Logos & branding — Compose icons into unique marks
- Illustrations — Scene building (forest, sky, etc.)
- Custom icons — When Lucide doesn't have exactly what you need
- Seasonal variations — Same composition, different elements (snow, blossoms)
Mobile Considerations
Overflow Menu Pattern
Desktop navigation items that don't fit should go to a mobile sheet menu:
<!-- Mobile menu button (visible md:hidden) -->
<button onclick={() => (mobileMenuOpen = true)} class="md:hidden p-2">
<Menu class="w-5 h-5" />
</button>
<!-- Sheet menu -->
<MobileMenu bind:open={mobileMenuOpen} onClose={() => (mobileMenuOpen = false)} />
Decorative Elements on Mobile
| Element | Mobile Treatment |
|---|
| Trees | Reduce count, simplify (density multiplier = 1) |
| Particles | Reduce count (40→20 snowflakes) |
| Clouds | Hide some, keep 2-3 |
| Complex animations | Reduce or disable |
| Touch targets | Minimum 44x44px |
Performance Guidelines
<!-- Reduce particle counts on mobile -->
<SnowfallLayer count={isLargeScreen ? 100 : 40} ... />
<!-- Skip complex effects for reduced-motion -->
{#if !prefersReducedMotion}
<FallingLeavesLayer ... />
{/if}
When to Use
| Pattern | Good For |
|---|
| Glassmorphism | Text over backgrounds, navbars, cards, modals |
| Randomized forests | Story pages, about pages, visual sections |
| Seasonal themes | Roadmaps, timelines, emotional storytelling |
| Midnight Bloom | Future features, dreams, mystical content |
| Weather particles | Hero sections, transitions between seasons |
| Birds | Adding life to forest scenes, seasonal indicators |
When NOT to Use
| Pattern | Avoid When |
|---|
| Heavy decoration | Data-dense pages, admin interfaces, forms |
| Particle effects | Performance-critical pages, accessibility concerns |
| Seasonal colors | Brand-critical contexts needing consistent colors |
| Multiple glass layers | Can cause blur performance issues |
| Randomization | Content that needs to match between sessions |
| Complex forests | Mobile-first pages, simple informational content |
Reference Pages
Study these for implementation patterns:
/forest — Full randomized forest with all seasons
/roadmap — Seasonal sections, progressive decoration, midnight bloom
/vision — Narrative page with glass callouts
OG Images (Social Previews)
Grove uses dynamic OG images for social media previews (Discord, Twitter, iMessage, etc.).
Architecture
OG images are generated by a separate Cloudflare Worker at og.grove.place due to WASM bundling limitations with SvelteKit + Cloudflare Pages.
grove.place/api/og?title=X
↓ 302 redirect
og.grove.place/?title=X
↓ workers-og
PNG image (1200×630)
API
GET https://og.grove.place/?title=X&subtitle=Y&accent=HEX
| Param | Default | Description |
|---|
title | "Grove" | Main title (max 100 chars) |
subtitle | "A place to Be." | Subtitle (max 200 chars) |
accent | "16a34a" | Hex color without # (forest green) |
Adding OG to New Pages
Use the SEO component which handles OG meta tags:
<script>
import SEO from "$lib/components/SEO.svelte";
</script>
<SEO
title="Page Title"
description="Page description for search engines"
ogImage="/api/og?title=Page%20Title&subtitle=Custom%20subtitle"
/>
Files
services/og-worker/ — Standalone Worker (uses workers-og)
landing/src/routes/api/og/+server.ts — Proxy to og.grove.place
landing/src/lib/components/SEO.svelte — Meta tag management
Integration with Other Skills
When writing text for Grove UI (tooltips, buttons, onboarding, error messages), invoke the grove-documentation skill first. The voice should match the visuals.
Typical flow:
- Design the UI component/page
- Activate
grove-documentation for any user-facing text
- Write content following Grove voice principles
- Return to visual implementation
Component Auditing with Showroom
Before verifying full pages, audit individual components. Showroom renders a single .svelte component in isolation, runs design compliance checks (color tokens, spacing grid, typography, focus styles, heading hierarchy), and diffs against visual baselines. It catches issues that full-page Glimpse captures miss.
uv run --project tools/glimpse glimpse showroom \
libs/engine/src/lib/ui/components/primitives/button/button.svelte
uv run --project tools/glimpse glimpse showroom \
libs/engine/src/lib/ui/components/ui/MyComponent.svelte --scaffold
uv run --project tools/glimpse glimpse showroom \
libs/engine/src/lib/ui/components/ui/MyComponent.svelte \
--scenario error --theme dark
uv run --project tools/glimpse glimpse showroom \
libs/engine/src/lib/ui/components/ui/MyComponent.svelte --update-baselines
The component gate: Every component you build or modify must pass glimpse showroom before you move to page-level Glimpse verification. Fix all compliance violations first.
Visual Verification with Glimpse
Before shipping any Grove page, look at it. CI passing is not the same as looking correct. Use Glimpse to capture the rendered page and review it yourself:
uv run --project tools/glimpse glimpse capture http://localhost:5173/[page] \
--season autumn --theme dark --logs
uv run --project tools/glimpse glimpse matrix http://localhost:5173/[page]
uv run --project tools/glimpse glimpse browse http://localhost:5173/[page] \
--do "click links, scroll down, interact with elements" --screenshot-each --logs
The iterate loop: Capture → review → fix → capture again. Repeat until the page matches the Grove aesthetic. Don't ship what you haven't seen.
Quick Checklist
Before shipping a Grove page: