Design system

Rosette

The colors, type, icons and components behind succulents.info. Bold enough to pop, built so everyone can read it.

  • AAA contrast, tested
  • 16 components
  • 29 icons

Start here

Principles

Four rules every page and component follows. When in doubt, these win.

  • Bold, never hard to read

    Bright color blocks always carry dark ink text. Every text pairing reaches WCAG AAA (7:1).

  • Words carry the meaning

    Color and icons only reinforce. Status like “Toxic to pets” is always spelled out.

  • Content first, scripts second

    Everything readable works without JavaScript. Interactive helpers are a bonus.

  • Room to grow

    Layouts reflow at 320px wide and 400% zoom, with large text, dark mode and reduced motion.

Tokens

Color

Defined once in src/styles/tokens.css. The numbers below are calculated from that file every time the site builds.

Accents

The same in light and dark themes. Always paired with --on-accent ink text.

  • Aa--bloom#b7dcc3Echeveria-tip pink. Primary actions, “act now”.11.0:1 with ink
  • Aa--sun#ece6d6Sunny yellow. Focus halo, warnings, step numbers.13.2:1 with ink
  • Aa--mint#ece6d6Fresh mint. Success, “totally normal”, tips.13.2:1 with ink
  • Aa--lilac#ece6d6Soft lilac. Notes, the plant finder.13.2:1 with ink
  • Aa--coral#ece6d6Terracotta coral. Warm accents.13.2:1 with ink

Core

These switch between themes. Light value first, dark value second.

  • --paper#fff7e8 / #0d1912Page background
  • --surface#ffffff / #16261dCards and inputs
  • --text#0e2318 / #f4efe2Body text, outlines
  • --text-muted#3d5246 / #c0d0c5Secondary text
  • --shadow#0e2318 / #9be8b4Soft shadow colour
Text contrast in both themes (AAA target: 7:1)
PairingUsed forLightDark
--text on --paperBody text on the page15.5:1 Pass15.7:1 Pass
--text on --surfaceText on cards16.5:1 Pass13.8:1 Pass
--text-muted on --paperSecondary text on the page7.9:1 Pass11.2:1 Pass
--text-muted on --surfaceSecondary text on cards8.4:1 Pass9.8:1 Pass
--paper on --textInverted buttons and selected choices15.5:1 Pass15.7:1 Pass
--on-accent on --bloomText on pink11.0:1 Pass11.0:1 Pass
--on-accent on --sunText on yellow13.2:1 Pass13.2:1 Pass
--on-accent on --mintText on mint13.2:1 Pass13.2:1 Pass
--on-accent on --lilacText on lilac13.2:1 Pass13.2:1 Pass
--on-accent on --coralText on coral13.2:1 Pass13.2:1 Pass

Tokens

Type

Two typefaces. Sizes scale fluidly with the screen and with the reader’s chosen text size.

Bricolage Grotesque

Display: headings, buttons, labels. Chunky, friendly, and wide enough to read at a glance.

Atkinson Hyperlegible Next

Body: everything else. Designed by the Braille Institute so similar letters like I, l and 1 are easy to tell apart.

  • --step-5Homepage heroKeep them alive
  • --step-4Page titles (h1)Keep them alive
  • --step-3Section titles (h2)Keep them alive
  • --step-2Card titlesKeep them alive
  • --step-1Ledes, h3Keep them alive
  • --step-0Body textKeep them alive
  • --step--1Small print, chipsKeep them alive

Tokens

Shape & space

Thin 1px outlines, no offset shadows and no tilts. Calm surfaces let the text lead.

  • --radius-sm 12px
  • --radius 20px
  • --radius-pill pill
  • --space-1
  • --space-2
  • --space-3
  • --space-4
  • --space-5
  • --space-6
  • --space-7
  • --space-8

Assets

Icons

24×24 line icons with round caps. Decorative by default; pass a label when an icon stands alone.

  • sun
  • cloud
  • drop
  • paw
  • thermo
  • soil
  • sprout
  • calendar
  • ruler
  • globe
  • menu
  • close
  • sliders
  • arrow
  • check
  • alert
  • stethoscope
  • book
  • bag
  • heart
  • cactus
  • rosette
  • pot
  • leaf
  • plus
  • minus
  • search
  • info
  • scissors

Assets

Illustrations

Every plant is drawn by <PlantArt> from a style, a leaf color and a tip color. No photos needed until we have real ones.

  • rosette
  • colony
  • aloe
  • zebra
  • dotted
  • jade
  • pearls
  • tail
  • pebbles
  • sword
  • fuzzy
  • bush
  • blooms
  • chandelier
  • column
  • mound
  • pads
  • starfish
  • strands

Library

Components

Import from @/components/ui. Each component renders the shared classes in src/styles/ui.css, so markup built by scripts can match exactly.

Button

Rounded pills with a 1px outline and no shadow. A link when given href, otherwise a real <button>.

Usage

<Button href="/plant-finder/" variant="primary" iconAfter="arrow">Find your plant</Button>
<Button type="submit" variant="ink">Get my answer</Button>
<Button size="sm" icon="sliders">Display</Button>

Props

Button props
PropTypeDefaultWhat it does
variant'primary' | 'secondary' | 'ink''secondary'Pink for the main action, ink for strong secondary actions.
size'sm' | 'md' | 'lg''md'Even small buttons are at least 44px tall.
hrefstring—Renders a link instead of a button.
icon / iconAfterIconName—Decorative icon before or after the label.
blockbooleanfalseFill the container width.

Accessibility

  • Use a link for navigation and a button for actions — the component picks the right element for you.
  • Labels are always visible text. Icons are hidden from screen readers.
  • Touch targets are at least 44×44px.

Chip

Small pills for facts and statuses. Outlined, inverted ink, or any accent tone.

Beginner friendlyPet-safeToxic to petsPet safety unknownCopes with low lightFrost hardyTotally normalKeep an eye on itAct now

Usage

<Chip icon="sprout">Beginner friendly</Chip>
<Chip variant="ink" icon="alert">Toxic to pets</Chip>
<Chip variant="mint" icon="check">Totally normal</Chip>

Props

Chip props
PropTypeDefaultWhat it does
variant'plain' | 'ink' | Tone'plain'Tones: bloom, sun, mint, lilac, coral.
iconIconName—Optional leading icon.

Accessibility

  • The words carry the meaning; color only reinforces it.
  • Group related chips in a list with an aria-label.
  • Plant facts come from one component, PlantFacts, so cards and plant pages always use the same words and icons.

Card

The basic surface. Add a tone for a color block. Put one .card-link inside to make the whole card clickable.

Plain card

Surface color with an ink outline.

Sun card

A color block. Text stays dark ink.

Lilac card

Not clickable — no card-link inside.

Usage

<Card tone="sun">
  <h3><a class="card-link" href="/care/watering/">Watering</a></h3>
  <p>The soak-and-dry method, step by step.</p>
</Card>

Props

Card props
PropTypeDefaultWhat it does
asHTML tag'div'Use li, article, section, a… for the right semantics.
toneTone—Color-blocked background with ink text.
pad'none' | 'sm' | 'md' | 'lg''md'Inner spacing.

Accessibility

  • The stretched-link pattern keeps one focusable link per card, with a clear name, instead of wrapping everything in a link.
  • Focus shows on the whole card with the yellow halo.

Callout

Notes for tips, warnings and pet safety. Each kind has its own color, icon and visible label.

Tip

Use terracotta pots

Unglazed clay breathes, so the soil dries faster between waterings.

Good to know

Farina is not mold

The white powder on some leaves is natural sunscreen. Don’t wipe it off.

Watch out

Ease into strong sun

Move plants into brighter light over a week or two to avoid sunburn.

Pet safety

Jade is toxic to cats and dogs

Keep it on a high shelf if your pets like to chew.

Usage

<Callout kind="tip" title="Use terracotta pots">
  <p>Unglazed clay breathes, so soil dries faster.</p>
</Callout>

Props

Callout props
PropTypeDefaultWhat it does
kind'tip' | 'note' | 'warning' | 'pets''tip'Sets color, icon and label.
titlestring—Optional bold first line.
labelstring—Override the label, e.g. “Pet emergency”.

Accessibility

  • Rendered with role="note", named by its label and title. Not an <aside>, so it adds no landmark inside <main>.
  • The kind is written out, not shown by color alone.

CareMeter

A three-step scale for light and water needs, shown on every plant card and profile.

  • LightBright shade (1 of 3)
  • LightSome sun (2 of 3)
  • LightFull sun (3 of 3)
  • WaterLow (1 of 3)
  • WaterModerate (2 of 3)
  • WaterThirstier (3 of 3)

Usage

<CareMeter kind="light" level={3} />
<CareMeter kind="water" level={1} />

Props

CareMeter props
PropTypeDefaultWhat it does
kind'light' | 'water'—Suns or drops.
level1 | 2 | 3—Filled icons, plus a word: e.g. Low / Moderate / Thirstier.

Accessibility

  • Icons are hidden from screen readers. They hear “Water: Low (1 of 3)”.
  • Empty steps are dashed outlines, not faded, so they stay visible.

ChoiceGroup

Big pill-shaped radio buttons or checkboxes inside a fieldset. Used by the plant finder, filters and display settings.

How much light does the spot get?

Direct sun means sunlight falling right on the plant.

Only show plants that are…(optional)

Usage

<ChoiceGroup
  legend="How much light does the spot get?"
  name="light"
  number={2}
  hint="Direct sun means sunlight falling right on the plant."
  options={[
    { value: 'sun', label: '4+ hours of direct sun' },
    { value: 'some', label: 'A little direct sun' },
  ]}
/>

Props

ChoiceGroup props
PropTypeDefaultWhat it does
legendstring—The question. Always visible.
namestring—Form field name.
options{ value, label, checked? }[]—The choices.
type'radio' | 'checkbox''radio'One answer or many.
hintstring—Extra guidance, linked with aria-describedby.
numbernumber—Numbered circle for step-by-step forms.
required / optionalboolean—Marks the group; “(optional)” is shown in words.
size'sm' | 'md' | 'lg''md'Legend and label size.

Accessibility

  • Real inputs underneath, so arrow keys, Space and screen readers work as expected.
  • Selected pills turn ink and gain a ✓, so selection never relies on color.

TextField

A labelled text input with an optional hint, leading icon and error message. Powers the glossary search.

Try “farina”.

Enter an email address like name@example.com

Usage

<TextField label="Search the glossary" name="q" type="search" icon="search" />
<TextField label="Email" name="email" type="email" required
  error="Enter an email address like name@example.com" />

Props

TextField props
PropTypeDefaultWhat it does
labelstring—Always visible. Placeholders are never used as labels.
name / idstring—id defaults to field-{name}.
type'text' | 'search' | 'email' | 'tel' | 'url''text'Brings the right mobile keyboard.
hintstring—Linked with aria-describedby.
errorstring—Shown below with a ⚠ and sets aria-invalid.
iconIconName—Decorative leading icon.

Accessibility

  • Errors are text, not just a red border: the field also gets a thicker dashed outline.
  • Error and hint text are both read out when the field is focused.

QuantityStepper

A number input with big − and + buttons. Ready for the shop’s cart.

Usage

<QuantityStepper label="Quantity" name="qty" value={1} min={1} max={10} />

Props

QuantityStepper props
PropTypeDefaultWhat it does
labelstring—Visible label; also names the buttons (“Increase quantity”).
value / min / maxnumber1 / 1 / 99Typed values are clamped to the range.

Accessibility

  • Buttons at the limit are marked aria-disabled instead of disabled, so keyboard focus doesn’t jump away.
  • Each change is announced politely, e.g. “Quantity: 3”.

Steps

Numbered instructions with big sun-yellow counters. Numbered lists in guides get the same style automatically.

  1. SoakWater until it runs out of the drainage hole.
  2. DrainNever leave the pot sitting in water.
  3. DryWait until the soil is dry all the way down.

Usage

<Steps items={[
  { title: 'Soak', body: 'Water until it runs out of the drainage hole.' },
  { title: 'Drain', body: 'Never leave the pot sitting in water.' },
  { title: 'Dry', body: 'Wait until the soil is dry all the way down.' },
]} />

Props

Steps props
PropTypeDefaultWhat it does
itemsstring[] | { title, body? }[]—Plain steps, or steps with a bold title.

Accessibility

  • A real ordered list, so screen readers announce “1 of 3”.

Table

A data table with a visible caption. On narrow screens it scrolls sideways, or with stack, shows each row as a labelled block.

Watering by season
SeasonRoughly
Spring and summerEvery 1–2 weeks
AutumnEvery 2–3 weeks
Winter, indoorsEvery 3–6 weeks

A starting point. Always check the soil first.

Usage

<Table caption="Watering by season" striped stack>
  <thead>
    <tr><th scope="col">Season</th><th scope="col">Roughly</th></tr>
  </thead>
  <tbody>
    <tr><th scope="row">Spring and summer</th><td>Every 1–2 weeks</td></tr>
    <tr><th scope="row">Winter, indoors</th><td>Every 3–6 weeks</td></tr>
  </tbody>
  <Fragment slot="note">A starting point. Always check the soil first.</Fragment>
</Table>

Props

Table props
PropTypeDefaultWhat it does
captionstring—Required. Shown above the table and names its scroll region.
stripedbooleanfalseShades every other body row.
stackbooleanfalseBelow 40rem, shows each row as a block with every cell labelled by its column header.
slot="note"slot—Optional source or footnote under the table, linked with aria-describedby.

Accessibility

  • A real <table> with a <caption>. Use <th scope="col"> for column headers and <th scope="row"> for the first cell of each row.
  • The scroll region is focusable, so keyboard users can scroll it with arrow keys, and it is named by the caption.
  • Stripes are a scanning aid only; every color pairing keeps AAA contrast.
  • With stack, the markup gets explicit ARIA table roles at build time, so the table stays a table for screen readers after the layout changes. The visible cell labels are hidden from screen readers, which already announce the column header.

Tabs

Switch between a few related panels. Without JavaScript, it is a list of links and every panel is shown.

Spring

Most succulents start growing again. Water deeply when the soil is dry, and repot if needed.

Summer

Bright and warm. Ease plants into stronger sun to avoid sunburn, and watch for pests.

Winter

Most succulents rest. Water much less, and keep tender plants above about 40°F (4°C).

Usage

<Tabs
  label="Care by season"
  items={[
    { id: 'spring', label: 'Spring' },
    { id: 'summer', label: 'Summer' },
    { id: 'winter', label: 'Winter' },
  ]}
>
  <p slot="spring">Growth starts. Water when the soil is dry.</p>
  <p slot="summer">Bright and warm. Watch for sunburn.</p>
  <p slot="winter">Most rest. Water much less.</p>
</Tabs>

Props

Tabs props
PropTypeDefaultWhat it does
labelstring—Required. Names the tabs for screen readers.
items{ id, label }[]—One per tab. Put each panel in a slot named after its id.
selectedstringfirst idThe tab that starts selected. A matching URL hash wins.
headingLevel2 | 3 | 43Level of each panel heading, which shows when scripts don’t run.

Accessibility

  • Without JavaScript: a labelled nav of in-page links, then every panel with its own heading. Nothing is hidden.
  • With JavaScript: the ARIA tabs pattern. Left and Right arrows, Home and End move between tabs and select them; Tab moves into the panel.
  • The selected tab is a solid ink fill, not just a color change, and uses the system highlight in forced-colors mode.
  • Panels are not landmarks, so many tab sets on one page (like the Usage / Props / Accessibility tabs on this page) do not crowd the landmark list.

Disclosure

Expandable questions and answers built on native <details>. Give several the same group to keep only one open.

Should I mist my succulents?

No. Misting wets the leaves but barely reaches the roots. Water the soil deeply instead.

Do succulents need fertilizer?

Not much: half-strength, once or twice in the growing season.

Usage

<Disclosure summary="Should I mist my succulents?" group="faq">
  <p>No. Water the soil deeply instead.</p>
</Disclosure>

Props

Disclosure props
PropTypeDefaultWhat it does
summarystring—The question. Or use the “summary” slot for markup.
openbooleanfalseStart expanded.
groupstring—Shared name makes an exclusive accordion.

Accessibility

  • Works without JavaScript and announces expanded/collapsed state natively.
  • The + / − marker is decorative.

SectionHeading

Eyebrow, title and lede for a page section, with an optional action on the right.

Plant library

Meet the regulars

The succulents you’re most likely to find.

All plants

Usage

<SectionHeading eyebrow="Plant library" title="Meet the regulars"
  lede="The succulents you’re most likely to find.">
  <Button slot="action" href="/plants/">All plants</Button>
</SectionHeading>

Props

SectionHeading props
PropTypeDefaultWhat it does
titlestring—The heading text.
eyebrow / ledestring—Optional small label above and summary below.
as'h2' | 'h3''h2'Keep heading levels in order.
idstring—Set it to link to the section or label a landmark.

Accessibility

  • Only the title is a heading; the eyebrow is plain text so the outline stays clean.

Divider

A decorative dashed line with a row of little succulents.

Usage

<Divider />
<Divider icons={['cactus', 'pot', 'cactus']} />

Props

Divider props
PropTypeDefaultWhat it does
iconsIconName[]['rosette', 'cactus', 'rosette']The little plants in the middle.

Accessibility

  • Purely decorative and hidden from screen readers. Use headings to structure content.