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
| Pairing | Used for | Light | Dark |
|---|---|---|---|
--text on --paper | Body text on the page | 15.5:1 Pass | 15.7:1 Pass |
--text on --surface | Text on cards | 16.5:1 Pass | 13.8:1 Pass |
--text-muted on --paper | Secondary text on the page | 7.9:1 Pass | 11.2:1 Pass |
--text-muted on --surface | Secondary text on cards | 8.4:1 Pass | 9.8:1 Pass |
--paper on --text | Inverted buttons and selected choices | 15.5:1 Pass | 15.7:1 Pass |
--on-accent on --bloom | Text on pink | 11.0:1 Pass | 11.0:1 Pass |
--on-accent on --sun | Text on yellow | 13.2:1 Pass | 13.2:1 Pass |
--on-accent on --mint | Text on mint | 13.2:1 Pass | 13.2:1 Pass |
--on-accent on --lilac | Text on lilac | 13.2:1 Pass | 13.2:1 Pass |
--on-accent on --coral | Text on coral | 13.2:1 Pass | 13.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.
- Keep them alive
- Keep them alive
- Keep them alive
- Keep them alive
- Keep them alive
- Keep them alive
- Keep them alive
Tokens
Shape & space
Thin 1px outlines, no offset shadows and no tilts. Calm surfaces let the text lead.
--radius-sm12px--radius20px--radius-pillpill
--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.
sunclouddroppawthermosoilsproutcalendarrulerglobemenucloseslidersarrowcheckalertstethoscopebookbagheartcactusrosettepotleafplusminussearchinfoscissors
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.
rosettecolonyaloezebradottedjadepearlstailpebblesswordfuzzybushbloomschandeliercolumnmoundpadsstarfishstrands
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
| Prop | Type | Default | What 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. |
href | string | — | Renders a link instead of a button. |
icon / iconAfter | IconName | — | Decorative icon before or after the label. |
block | boolean | false | Fill 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.
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
| Prop | Type | Default | What it does |
|---|---|---|---|
variant | 'plain' | 'ink' | Tone | 'plain' | Tones: bloom, sun, mint, lilac, coral. |
icon | IconName | — | 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
| Prop | Type | Default | What it does |
|---|---|---|---|
as | HTML tag | 'div' | Use li, article, section, a… for the right semantics. |
tone | Tone | — | 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
| Prop | Type | Default | What it does |
|---|---|---|---|
kind | 'tip' | 'note' | 'warning' | 'pets' | 'tip' | Sets color, icon and label. |
title | string | — | Optional bold first line. |
label | string | — | 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
| Prop | Type | Default | What it does |
|---|---|---|---|
kind | 'light' | 'water' | — | Suns or drops. |
level | 1 | 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.
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
| Prop | Type | Default | What it does |
|---|---|---|---|
legend | string | — | The question. Always visible. |
name | string | — | Form field name. |
options | { value, label, checked? }[] | — | The choices. |
type | 'radio' | 'checkbox' | 'radio' | One answer or many. |
hint | string | — | Extra guidance, linked with aria-describedby. |
number | number | — | Numbered circle for step-by-step forms. |
required / optional | boolean | — | 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
| Prop | Type | Default | What it does |
|---|---|---|---|
label | string | — | Always visible. Placeholders are never used as labels. |
name / id | string | — | id defaults to field-{name}. |
type | 'text' | 'search' | 'email' | 'tel' | 'url' | 'text' | Brings the right mobile keyboard. |
hint | string | — | Linked with aria-describedby. |
error | string | — | Shown below with a ⚠ and sets aria-invalid. |
icon | IconName | — | 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
| Prop | Type | Default | What it does |
|---|---|---|---|
label | string | — | Visible label; also names the buttons (“Increase quantity”). |
value / min / max | number | 1 / 1 / 99 | Typed 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.
- SoakWater until it runs out of the drainage hole.
- DrainNever leave the pot sitting in water.
- 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
| Prop | Type | Default | What it does |
|---|---|---|---|
items | string[] | { 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.
| Season | Roughly |
|---|---|
| Spring and summer | Every 1–2 weeks |
| Autumn | Every 2–3 weeks |
| Winter, indoors | Every 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
| Prop | Type | Default | What it does |
|---|---|---|---|
caption | string | — | Required. Shown above the table and names its scroll region. |
striped | boolean | false | Shades every other body row. |
stack | boolean | false | Below 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
| Prop | Type | Default | What it does |
|---|---|---|---|
label | string | — | Required. Names the tabs for screen readers. |
items | { id, label }[] | — | One per tab. Put each panel in a slot named after its id. |
selected | string | first id | The tab that starts selected. A matching URL hash wins. |
headingLevel | 2 | 3 | 4 | 3 | Level 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
| Prop | Type | Default | What it does |
|---|---|---|---|
summary | string | — | The question. Or use the “summary” slot for markup. |
open | boolean | false | Start expanded. |
group | string | — | 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.
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
| Prop | Type | Default | What it does |
|---|---|---|---|
title | string | — | The heading text. |
eyebrow / lede | string | — | Optional small label above and summary below. |
as | 'h2' | 'h3' | 'h2' | Keep heading levels in order. |
id | string | — | 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.
PageHeader
The color band at the top of every page — including this one. Breadcrumbs, an art slot, and room for extras.
See the top of this page for a live example.
Usage
<PageHeader tone="mint" eyebrow="Plant library" title="Find your succulent"
lede="12 popular succulents, each with a care card."
crumbs={[{ href: '/', label: 'Home' }, { label: 'Plants' }]}>
<PlantArt slot="art" kind="aloe" leaf="#5aa679" />
</PageHeader>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title / tone | string / Tone | — | Required. The h1 and the band color. |
eyebrow / lede | string | — | Optional label and summary. |
crumbs | { href?, label }[] | — | Renders Breadcrumbs above the title. |
artStyle | 'plain' | 'badge' | 'plain' | “badge” frames the art in a cream circle. |
Accessibility
- Contains the page’s only h1.
- Art in the art slot is hidden from screen readers.
Breadcrumbs
Shows where a page sits in the site.
Usage
<Breadcrumbs items={[{ href: '/', label: 'Home' }, { href: '/plants/', label: 'Plant library' }, { label: 'Jade Plant' }]} />Props
| Prop | Type | Default | What it does |
|---|---|---|---|
items | { href?, label }[] | — | The last item, without href, is the current page. |
Accessibility
- A labelled nav with an ordered list. The current page has aria-current="page".
- Separators are hidden from screen readers.
Divider
A decorative dashed line with a row of little succulents.
Usage
<Divider />
<Divider icons={['cactus', 'pot', 'cactus']} />Props
| Prop | Type | Default | What it does |
|---|---|---|---|
icons | IconName[] | ['rosette', 'cactus', 'rosette'] | The little plants in the middle. |
Accessibility
- Purely decorative and hidden from screen readers. Use headings to structure content.