Design guide

The visual language of dandelion.events as it exists in code today. This page is rendered with the site's own app.css and bootstrap5.css, so swatches, type and components are the real thing. The site is Bootstrap 5.3.8, restyled through its variables.

The root font size is 17px (19px from 1680px, 21px from 1920px) at the browser's default text size, set as a percentage so it follows the reader's own setting, and rem values scale with it and the viewport. Sizes are in rem (or em, to follow the text); px is kept for 1–2px hairlines, shadows and the focus ring, for breakpoints and the container max-widths that fit inside them, and for overrides that must line up with Google Maps. Avatars take a size class (.avatar-xs to .avatar-2xl), not a width. Colour values, radii, type sizes and the spacing scale are measured live; other sizes are copied from the stylesheets.

01

Colour

Every family is a ramp of steps generated from its 500 with oklch(), so steps at the same lightness look equally light whatever the hue. Tints (50–200) sit at fixed lightness; shades (400–800) are relative to the 500, with 800 capped dark enough for text on tints. The 400 is .06 lighter than the 500, or half way to the 200 when the 500 is too light for that (yellow), and the 300 is halfway between the 200 and 400, so the steps stay in order however light the colour.

Components use the 500 for fills, 600 for hover, 100 for alert, toast and badge backgrounds, and 800 for text on those tints. Red text (text-danger, form errors) takes red's 600, as the 500 is under 4.5:1 on white.

Each 500 is as vivid as sRGB allows at its lightness: green, orange and yellow sit at the edge, and red and lavender, which have more room, stop at four-fifths of theirs, so they sit level with the green rather than glare.

Lightness follows the hue, as each colour reads as itself only in a band: yellow only when light, red only when fairly dark. Hues stay clear of their neighbours: red short of both orange-red and pink, orange an amber at 72, set apart from yellow at 90 by lightness as well (their tints, which share a lightness, aren't, so warning badges take the 200), and lavender the default green turned to 300. Yellow's 500 is the lightest, so its 200 to 500 sit close together, set apart more by saturation than lightness.

Red
hue 25 · lightness .63
Orange
hue 72 · lightness .79
Yellow
hue 90 · lightness .86
Green
hue 153 · lightness .661 · the default theme
Lavender
hue 300 · lightness .661

Green

The theme colour: green by default. Organisations and events set --theme-500 only (through /theme.css). Every theme colour is a solid step of this ramp; the only translucent ones are the focus ring, .badge-overlay (the 500 at 70%, over images) and chart projections (the 500 at 50%, and in the contributions chart a 30% fill with a 50% border), so they read as estimates.
--theme-* · Primary and success · follows each organisation's theme
50
crumbs, newsfeed icons, date tile months, docs transcripts
100
alert, toast and badge background, crumb hover, unread messages, themed AI well start
200
progress tracks, alert, toast, blockquote and transcript borders
300
400
themed AI well end, Stripe Connect fees in the contributions chart
500
600
hover
800
text on tints
Bootstrap's rgba(var(--bs-*-rgb)) colours need an rgb triplet, so /theme.css also sets --theme-500-rgb.

Lavender

The default site's AI well and .lavender-wash backgrounds. Its steps come from --color-lavender-500.
50
.lavender-wash, image-less tiles (.block-fill), onboarding callout, features page section wash
100
AI well gradient start
200
not used yet
300
not used yet
400
AI well gradient end
500
onboarding callout icon and done steps
600
features and Luma pages: purple divider end, call-to-action glow
800
onboarding checklist: the current step

Yellow

For highlighted or rewarded things, through text-bg-yellow: Featured, Co-host, rota points, PATCH routes. Its steps come from --color-yellow-500.
50
not used yet
100
badge background
200
not used yet
300
not used yet
400
not used yet
500
contributions chart line and fill; text-bg-yellow on anything but a badge (not used yet)
600
not used yet
800
badge text

Orange

Warning: each warning class (alert-warning, toast-warning, table-warning, btn-warning, text-bg-warning) sets the steps it uses as --variant-*, and each component maps those to its roles once. The 500 is also Bootstrap's $warning in bootstrap5.scss. table-warning rows mix 30% of the way from the 50 to the 200 for hover, stripes and active rows.
Warning · --color-orange-*
50
row background. Also: features page section washes
100
alert and toast background
200
row, alert and toast border, badge background. Also: features page divider end
300
not used yet
400
Star icons, feedback stars, map areas, features page
500
fill: buttons, card headers and other text-bg-* elements that aren't badges. Also: ASN stats: bot % of 30 and over
600
button hover
800
text and links on tints, badge text

Red

Danger, through the danger classes (alert-danger, toast-danger, table-danger, btn-danger, text-bg-danger), which map its steps to their roles as orange's warning classes do. The 500 is also Bootstrap's $danger in bootstrap5.scss, and its 600 is $form-feedback-invalid-color, so form errors and text-danger (red text) are the 600. Danger badges take the 200, as red's 100 reads pink, as do warning badges (see Orange). table-danger rows mix 30% of the way from the 50 to the 200 for hover, stripes and active rows.
Danger · --color-red-*
50
row background
100
alert and toast background
200
row, alert and toast border, badge background
300
not used yet
400
Map: event and organisation markers
500
fill: buttons, card headers and other text-bg-* elements that aren't badges. Also: ASN stats: bot % of 60 and over; check-in QR scan line
600
button hover
800
text and links on tints, badge text

Greys, black and white

Bootstrap's own variables, set in bootstrap5.scss to near-neutral greys: Bootstrap's lightness steps with a trace of warmth (designed at oklch hue 75, chroma .00225–.003, then rounded to hex). Usage: 100 (--bs-tertiary-bg) body.canvas pages, wells and hovered dropdown, autocomplete and list items, and table stripes · 200 (--bs-secondary-bg) hovered nav links, table rows and secondary buttons, secondary badges and input add-ons · 300 (--bs-border-color) table and secondary button borders · 400 input borders and hr · 500 sidebar sub-menu links and grey borders (comment boxes, the editor) · 600 secondary text and the sidebar's minify button · 700 input add-on text · 800 search bar, sidebar dividers and dark badges in the sidebar · 900 header and sidebar. Body text (--bs-body-color) and emphasis text (--bs-emphasis-color, set to 900 in bootstrap5.scss in place of Bootstrap's black) are the same as 900. Black and white with transparency use rgba(var(--bs-black-rgb), a) and rgba(var(--bs-white-rgb), a), e.g. shadows and image overlays. Hairlines (card, dropdown, autocomplete and cookie banner borders, event blocks, image-less tiles, date tiles, the onboarding callout, feedback entries, the .chill card header's divider, marketing cards) use --hairline, black at 10%: Bootstrap's --bs-border-color-translucent, set in bootstrap5.scss, so its cards and dropdowns share it.
--bs-white
--bs-gray-100
--bs-gray-200
--bs-gray-300
--bs-gray-400
--bs-gray-500
--bs-gray-600
--bs-gray-700
--bs-gray-800
--bs-gray-900
--bs-black

Accents and surfaces

Single colours with one role each.
--color-paper
end of the lavender wash: .lavender-wash, .block-fill, onboarding callout
--color-highlight
mark, .highlight
--color-midnight
header sunset; root of the marketing pages' purple

Other gradients

The dark header and sidebar are a sunset: near-black fading to --color-midnight, with the orange-to-yellow --gradient-active as the sun on the active sidebar item, and the notification dot. Light content marks active states with the theme colour, so each surface has one active colour. --gradient-rainbow is only for .gradient-text headlines on marketing pages and the signed-out home.
.lavender-wash
Lavender to paper. body.lavender-wash on the sign-in pages (email, code, Ethereum), sign-up and auth failure; a .lavender-wash div, which also fades to white at the bottom, behind the hero on the marketing pages and the signed-out home.
--gradient-active
Sidebar active item, notification dot.
--gradient-rainbow
.gradient-text only.
--gradient-header
The header: 45°, --bs-gray-900 to 66%, then midnight.

Marketing pages

The features and comparison pages. Their purple gradient and the five reason colours on the Luma page are derived from --color-midnight and --color-magenta-500. Rose and magenta are only used here, so they are defined in marketing.css, which only these pages and this guide load.
Section washes
· · ·
section backgrounds: the 50 step of orange, rose, magenta and lavender
--color-rose-500, …
·
section divider: 500 to 200
--color-magenta-500, …
·
section divider: 500 to 200
Magenta to midnight
color-mix, 0–100%

Third-party brands

Only for buttons and labels that stand for these services.
--color-evm
EVM
--color-stripe
Stripe · text-bg-stripe
--color-opencollective
Open Collective
--color-coinbase
Coinbase
--color-signal
·
Signal · btn-signal
--color-paypal
PayPal
--color-gocardless
·
GoCardless
--color-mollie
Mollie
Payment badges, from badge-payment-<method>: Coinbase Mollie PayPal GoCardless GoCardless instalments OC: … EVM: …
02

Typography

HEADINGS · --header-font-family
Hanken Grotesk
Variable 100–900, used at bold. Letter-spacing −0.02em. Also .strapline and .header-font.
BODY · --body-font-family
Plus Jakarta Sans
Default weight 500, labels 700, bold for b/strong. Line height 1.5.

Scale

--font-size-base is body text. From --font-size-md up, each step is 1.2 times the last (a minor third), so every heading is bigger than the one below and h5 sits a step above body text.
h1 · .fs-1

Gatherings near you

--font-size-3xl · 2.07rem ·
h2 · .fs-2

Gatherings near you

--font-size-2xl · 1.73rem ·
h3 · .fs-3

Your upcoming events

--font-size-xl · 1.44rem ·
h4 · .fs-4

Upcoming birthdays

--font-size-lg · 1.2rem ·
h5 · .fs-5
Ticket types
--font-size-md · 1rem ·
.lead

An introductory line, like a sold-out card's

--font-size-lg · 1.2rem ·
.readable
Long-form copy such as event descriptions sets at 1rem.
--font-size-md · 1rem ·
body · nav · .btn
Body text, inputs, tables, buttons (btn-sm too), nav links, tabs, dropdown items and tooltips
--font-size-base · .875rem ·
.small
Secondary details: a location, a description
.875em of its context ·
.badge
Badges
.9375em of its context ·
.breadcrumb
--font-size-xs · .75rem ·
.h1–.h5 give any element a heading's style, e.g. <h4 class="h2"> for block titles. h5 is the smallest heading we use, a step above body text; h6, at --font-size-base, is left to user-written descriptions. bootstrap5.scss sets the heading sizes ($h1-font-size…) to these tokens, so .fs-1–.fs-5 make text a heading's size without its weight or font. .small is Bootstrap's class for smaller text without the meaning of <small>, which is for small print. Secondary text is --bs-secondary-color (--bs-gray-600), set with .text-body-secondary; form-text takes it too. .subtle is for links in it, which turn the theme colour on hover. .underline-dotted is for quiet links that read as part of a sentence (a rota's edit links) and text to copy.
03

Radius, elevation, spacing

Radius

Bootstrap's radius variables, set to one scale in bootstrap5.scss: controls on the base, surfaces on lg, sheets on xl. Inputs and buttons share a radius at every size (sm, base, lg), so they match side by side. Square corners (0) where an element sits flush against another; 50% for circles.
--bs-border-radius-sm
form-control-sm and btn-sm, Select2 tags
--bs-border-radius
Inputs, buttons, button groups, alerts, toasts, pagination, tooltips, sidebar pills; the bottom corners of menus that hang from their input (autocomplete, Select2)
--bs-border-radius-lg
Cards, modals, menus and panels: dropdowns, cookie banner, AI well, ask box. Also form-control-lg and btn-lg
--bs-border-radius-xl
Mobile bottom sheet (top corners), marketing cards, chat bubbles
--bs-border-radius-pill
Badges, nav crumbs, the header avatar, round buttons and dots

Elevation

Bootstrap's three shadows, set in bootstrap5.scss and used through its variables or the shadow-sm, shadow and shadow-lg classes. Soft, in px like hairlines: sm is a tight contact shadow (1px) alone, and the base and lg add a wide, faint one under it. Floating menus and panels (dropdowns, autocomplete, the cookie banner) also have a --hairline border, so the shadow can stay light; toasts have a border in their variant's 200, and the chat panel (_chat_card.erb: an event's discussion, an organisation's files) has the shadow alone, under a text-bg-primary bar that opens and closes it; below 992px it's a card with a .chill header instead.
--bs-box-shadow-sm
Resting surfaces: header, search, cards (all of them, from app.css), the editor toolbar while stuck
--bs-box-shadow
Floating: dropdowns, autocomplete, toasts, cookie banner, chat, hover lift, the sticky name column of the stats table
--bs-box-shadow-lg
Not used yet
The mobile event page's fixed tickets bar keeps its own upward shadow.

Spacing

One scale for padding, margins and gaps, as --space-* in app.css. Bootstrap's spacing utilities are the same scale without sm and xl, so in markup use the utility and in CSS the token. In app.css, values off the scale mostly size a control to match another (.375rem, btn-sm's padding, the nav crumbs' .625rem), line up with a fixed layout dimension (the header, the sidebar, search results' image column) or are px hairlines. The page-specific stylesheets are on the scale too, except for the same kinds of value: the docs select's room for its caret and the search icon's inset, and marketing sections' 3.5rem and 5rem, which are layout rather than spacing.
--space-2xs
· *-1
Label to field, dropdown and autocomplete items, sidebar sub-menu, badges in a row (set in app.css, or gap-1)
--space-xs
· *-2
Nav links, dropdown headers, search tabs, filter form fields, cookie banner buttons
--space-sm
· —
Grid gutter, table cells, card headers, alerts down, notification items, mobile sidebar links
--space-base
· *-3
Between fields (mb-3), mobile --content-padding, dropdown and list items across, alerts across
--space-lg
· *-4
--content-padding, below a carousel · Dandelion Daily: between stories, and either side of the features' column rules
--space-xl
· —
Dandelion Daily: between text columns · docs h2s above · comparison cards (marketing) · message bubbles from the far side
--space-2xl
· *-5
Between page sections (mt-5, mb-5) · the Dandelion Daily sheet's padding
Content padding · --content-paddinglg · mobile base
Grid guttersm
Container paddingbase each side
Card bodybase
Card headersm base
Field spacingmb-3 · base
Table cellssm · table-sm xs
Containers540 / 720 / 960 / 1140

Motion

Motion shows where things come from and go, and is otherwise kept out of the way. State changes fade rather than snap: hover, focus, active and open states change over --transition-fast (.15s ease-in-out, Bootstrap's own timing for nav links and buttons). Things that float fade in over --transition-enter (.2s, an ease-out), settling --space-2xs into place: dropdown menus (which shut at once), the confirm popover and flash toasts. Modals keep Bootstrap's entrance: the backdrop and dialog fade over .15s as the dialog slides 50px down over .3s. Sidebar submenus, the teams, activities and local groups in the cards on /accounts/subscriptions, and the search options above event lists open and shut with jQuery's slide over 175ms, and their chevrons turn to point up while open. Beyond that only colours, shadows, masks and opacity change, and things move only by translate and rotate: on mobile the sidebar slides in, the menu button's bars turn into a cross, and the marketing pages' cards lift --space-2xs on hover. Nothing scales, bar two older exceptions, the quick theme colours and the feedback stars, which grow 10% on hover; don't add more. Nothing bounces or reveals on scroll, and nothing else moves more than --space-2xs. Only loading indicators loop, on their own timing: the .bi-spin spinner, the .placeholder-glow pulse, CKEditor's upload bar and the docs page's cycling status words. The one other loop is the signed-out home's typewriter, which types its adjectives into the strapline, pausing .7s on each, with a cursor that holds still while it types and blinks (.7s) while it pauses; it's the hero's one flourish, so don't add more. Under prefers-reduced-motion it shows one word, with no cursor. Tab panes appear at once. From 768px, page loads cross-fade over .15s (a view transition), with the header, sidebar and flash toasts held still. Under prefers-reduced-motion both tokens are 0s and view transitions are off, so state changes snap and the tab scroller's chevrons jump rather than scroll smoothly; the loading indicators keep going, so a wait still shows.
Links, nav links, tabscolour, underline
Dropdown items, message previewsbackground
Tilesshadow, photo lightens
Date and icon tilesfill
Tab and crumb scrollersedge fades, chevron colour
Dropdown menus, confirm popover, flash toastsfade in, settle
ModalsBootstrap fade and slide
Sidebar groups, subscription cards, search optionsslide, chevron turns
Chat panelchevron turns
Signed-out home straplinetypes, cursor blinks
04

Page shell

Fixed dark header (--header-height, 2.8125rem) and a fixed sidebar (--sidebar-width, 14.375rem, minified to --sidebar-width-minified, 3.75rem). Groups of sidebar links can sit under a .nav-header label (--font-size-xs, semibold, --bs-gray-500), as the admin stats links do; minified, it becomes a --bs-gray-800 divider. Sticky panels (.sticky-under-header) sit at --sticky-top, just below the header. Below 768px the sidebar becomes a full-width offcanvas and header icons spread across a second row.

Maya
Home
Dandelion Daily
Eventshover
Organisations
Symbiota All organisations
#content · padding --content-padding
col-lg-8 · newsfeed
col-lg-4 · sticky
05

Components

Buttons

Primary and danger are flat: their variant's 500 over a transparent border, and the 600 on hover. Other fills (progress bars, text-bg-* elements other than badges, like card headers, and p.action) are the same flat 500. btn-warning is styled the same but not used yet. Secondary is quiet: white with a --bs-border-color border and body text, --bs-secondary-bg on hover. It's for dismissive actions like Cancel, Not now and Disconnect. btn-outline-secondary is only for buttons joined to an input (Apply, Remove, Copy) and the donation toggle group. Brand buttons like btn-signal stay flat. Put buttons side by side in d-flex flex-wrap gap-2, not ms-* on the second, so they line up and keep their gap when they wrap on phones.
.btn · .875rem · weight 600 · padding .5rem 1rem (sm .375rem .75rem), the same height as an input · radius .5rem (--bs-border-radius), as inputs

Filter buttons

A row of buttons above a list, each narrowing it to one choice: carousels on an organisation's events page, popular locations on /events. Wrap the row in .filter-buttons.
.filter-buttons .btn: full-size btn (radius .5rem) with tighter padding, --space-2xs × --space-xs · btn-outline-primary, the current choice btn-primary · mb-1 so wrapped rows don't touch

Badges

Bootstrap's .badge with a text-bg-* colour, for counts, statuses, tags and amounts. Sized in em, so a badge scales with the text around it: small in a nav, bigger beside an h4, and at body size (1rem) beside an h1, h2 or h3, where it would otherwise be nearly heading-sized. Pick the colour by what it means.

Full moon gathering Featured Locked

Followers 128 · Tagged breathwork in Weekend pass · Paid SPRING: £5
.badge · .9375em (1rem in h1–h3) · .4em .85em · pill · badges side by side get .25rem (mb-1) below them from app.css, so the rows don't touch when they wrap (flex rows use gap-1 instead) · weight 600 · primary, secondary, warning, danger and yellow are soft: their variant's 100 behind its 800, so they show on grey as well as white (warning and danger: the 200, as orange's 100 is close to yellow's and red's reads pink; secondary: --bs-gray-200 and 800); dark, stripe and payment badges are filled · in a selected menu item, sheet item or pill, a primary or secondary badge turns to its 800 with white text, so it sits in the selection · .badge-overlay, for labels over images (the carousel), is filled with its variant's 500 at 70%, white text, square
ClassMeansAs used
text-bg-primary Good, or the main count
1286/6breathworkKitchen shift
text-bg-secondary Neutral detail
LockedSecretSPRING: £5
text-bg-warning Needs attention, not yet a problem
On hold
text-bg-danger A problem or a hard limit
Sold outSales endedFullRejected, to contact
text-bg-yellow Highlighted or rewarded
FeaturedCo-host 2
text-bg-dark Counts in a gathering's nav
312/20
text-primary border border-primary Outline: a group label, or an unselected choice
AvailableWeekend passLondon
text-bg-stripe Reported by Stripe
£1,240
text-bg-yellow and text-bg-stripe are ours, written like Bootstrap's. Payment badges (badge-payment-<method>) are under Third-party brands. A badge with no colour class has no background, so always give it one (or an inline background, as the hashed organisation and gathering badges do).

Alerts

Your ticket is booked. View order
Only 3 tickets left. Book now
Payment failed. Try again
Inline, in the page. Padding .75rem 1rem (sm base, like card headers), and a 1px border in the variant's 200, like toasts. Links bold and underlined in the alert's link colour. Flashes are toasts, except on the few pages that set @hide_alert (sign in, sign up, subscriptions, the mailer header), which render them inline as alerts with no close button.

Toasts

Your ticket is booked.
Only 3 tickets left.
Payment failed.
Flash messages, fixed at the top centre (_alert.erb). The same tints and 1px border in the variant's 200 as alerts, plus a --bs-box-shadow. Notices fade after 3s, errors and warnings after 6s.

Forms

Email is invalid
Form text is --bs-secondary-color. Every focused control gets the same ring, --focus-ring: --bs-focus-ring-width (2px) of --bs-focus-ring-color (the theme at 60%). Danger buttons ring in red, btn-signal in its blue, and colour swatches add a white gap. Nav links have none.
input .875rem with .5rem vertical padding, the same height as a button (about 2.4rem) · border --bs-gray-400 · labels 700 · errors: lib/form_builder.rb adds is-invalid and an .invalid-feedback message in --bs-danger, the field name first · .field-note, for a note or link under a field (a preview link, a resend link), pulls it up into the field's mb-3 so it reads as the field's own

Navigation crumbs and dropdown

Gathering nav

The nav at the top of a gathering's pages from 768px (gatherings/_nav.erb): Bootstrap's .nav-pills on a dark bar, the gathering's name first. Teams, timetables and shifts open dropdowns. Below 768px it's a crumb that opens a sheet, as for organisations and events.
bg-dark bar, p-1 and rounded · links --font-size-xs (.75rem) in white, padding .5rem .75rem · hover, focus and the active page --bs-black · counts and amounts text-bg-dark badges · wraps onto a second line when a gathering has most features on

Input groups

£
input-group-text for currency symbols (money_symbol) and inline labels · a button on the end for one-field forms (sign in, search, reactions, deleting an account), so the two stay together on phones · d-inline-flex w-auto when the field is narrow · input-group-sm with form-control-sm

File and image inputs

file_block and image_block (lib/form_builder.rb): a plain form-control file input, with the current file above it once saved: a download link, or for images the image at up to 12.5rem high, then a Remove checkbox (and Rotate by for images) · forms.js rejects files over 10MB before upload

Select2

Press comma to add a tag or enter to accept a suggestion. No need for #
select2.css overrides select2-bootstrap-5-theme: as tall as .form-control, --bs-gray-400 border, .form-control's font and radius · focused or open: --theme-500 border and --focus-ring, the box and dropdown glowing as one shape · selected option --theme-500 · tags (_tags_select.erb) sit on the typing row, radius sm

Flatpickr

forms.js wires up .datepicker (altFormat Y-m-d) and .datetimepicker (J F Y, H:i, 24-hour; data-flatpickr-min-date, -max-date and -linked-end) · selected days and range ends --theme-500 · weeks start on Monday · on phones, a native input (.flatpickr-mobile) at full width

Rich text editor

CKEditor 5, from our own build in ckeditor/: change ckeditor/src/ckeditor.js and run rake ckeditor:build (needs Node) to write ext/ckeditor.js. wysiwyg_block renders a textarea.wysiwyg, and wysiwyg.js swaps in the editor. ckeditor.css styles it and the rich text it produces; the build adds three paragraph styles, Lead, Small and Action.

Filter forms

The search forms above lists and tables: orders, donations, members, applications. Bootstrap 5 dropped .form-inline and .form-group, so app.css keeps them for these: .form-inline lays the form out in a row from 576px, and each .form-group is a label and its field side by side. .searchForm spaces them and caps fields at 12em; .submitOnChange submits when a select, date, checkbox or month field changes.
Fields stack below 576px, with the button below them · the checkbox helper (checkbox 'Deleted') is a .form-group holding a .form-check-inline · in grid rows instead (events/_search_form.erb), each .form-group takes a col, with the checkboxes beside the button on a second row, inside a .search panel: white, --bs-box-shadow-sm, its checkbox chips outlined in --bs-border-color like the fields, --theme-500 when checked, the form in a .search-body · where the panel can shut (the map and calendar, and on phones on an organisation's events), its top row is a .search-toggle button with a hairline under it while open, a bi-funnel icon then Filter in weight 600, with a chevron at the far end, which slides the .search-body open or shut · .select-placeholder greys a select until forms.js loads, which then greys any select while its chosen option is a disabled placeholder · popular locations: .filter-buttons (see Filter buttons)

Scope toggle and View as

View as (_view_as.erb), on the left with Sort by on the right, above events, facilitators, organisations and gatherings, switches how a list shows: list, grid, map, calendar, network, blocks. Its buttons say what they are, so it has no visible label, only aria-label="View as". Below 992px it's icons only, with tooltips. The scope toggle, at the start of the search row above a list that a signed-in account can switch to their own: organisations they follow or admin, gatherings they're in (_scope_toggle.erb). Switching shows a different list rather than narrowing this one, so it's a group of links, All then each of theirs, not a checkbox. Joined btn-outline-primary buttons in a .segmented, the chosen one .active: --theme-100 with theme text rather than solid, so it doesn't compete with the Search button, the other with the count of their own. Each of theirs is hidden when they have none, unless it's showing, and the toggle with it when none is left; that list, when empty, says so and links back to all.

Short URL input

https://dandelion.events/o/
shorturl_block (lib/form_builder.rb), for slugs on the build pages: the address's stem as text, then an inline input with only a dotted --bs-gray-400 underline and no focus ring, 15em wide, so it reads as part of the address · a link icon once saved, which copies the address (Click to copy, then Copied!) and hides while the slug is edited · in .slugify, it fills in from the name

Theme colour picker

_quick_theme_colors.erb, beside the colour field: quick_colors steps from the default green round the wheel in thirteenths, keeping nine, at HSL lightness 50% · 1.625rem circles that grow 10% with a --bs-box-shadow on hover · selected or focused: a white gap, then a ring in the swatch's own colour

Add block

organisations/_add.erb and accounts/_add.erb: plus and minus icons under a list of people or organisations, each opening a small form · .add-block hides the field's label, since the placeholder says it · white, --bs-box-shadow-sm, base radius · removing is a select and a btn-danger

Button groups

Add a donation to Dandelion
Joined buttons for a choice of amounts (the purchase form's donation: btn-outline-secondary like the Apply button beside it, the chosen one .active, filled --bs-gray-600) and paired tiers (accounts/_pay.erb) · dropdown_nav in a btn-group is the Actions menu on order and ticket tables: an item with no path is a .dropdown-header, bold --bs-emphasis-color over a --bs-border-color line (none in nav crumbs or the mobile bottom sheet, whose sections have dividers), and nil a divider

Card with .chill header

Your upcoming events

Ecstatic dance on Sat 4 Oct
2 hours ago
Maya followed you
Yesterday
All events →

Table and pagination

Ticket typeSoldPrice
Early bird40/40£15
Standard23/80£22
Solidarity9/10£8
Row colours: table-warning flags a row to look at (low delivery rate), table-danger a problem (a blocked ASN). Both use the warning and danger 50 and 200. text-body-secondary greys a row that's past (a past boost slot)
Lists: the arrows alone · admin lists: page numbers too · Active: --theme-500, like the active pill and dropdown item · Hover: --bs-secondary-bg, like pills and nav links

Key-value table

Email
Website
Location
Hackney, London
.table-hr, for details (accounts/_table.erb, organisations/_table.erb, events/_info_table.erb, activity and local group pages): labels bold in a column only as wide as the longest, rows ruled with a --bs-gray-200 line and no other borders

Striped table

Kitchen
2
Welcome desk
1
Friday
Saturday Sign up
Sunday Sign up
Grids of slots: rotas (rotas/_rota.erb), timetables and the admin dashboard; also some admin lists (members, applications, teams, mailers) · stripes --bs-tertiary-bg · each cell an avatar-sm or a btn-sm to sign up · roles and slots carry their points as text-bg-yellow badges

Tooltips

hover these
data-bs-toggle="tooltip" and a title: app.js has one handler for the whole page, so content loaded later needs no setup. HTML is allowed, and with no title it shows the hidden span after the element (reaction names, timetable descriptions) · .875rem, --bs-emphasis-color at 90%, radius base, max 12.5rem wide · label icons, truncated text and disabled controls, not essential information

Progress and loading

Places filled (events/_progress.erb, _ticket_types_progress.erb), pay and rota progress · --theme-500 on --theme-200 · the ticket count always as a link above (places can outnumber tickets, when a ticket type takes more than one), with places filled in the bar's tooltip, and inside the bar too where it's full width
.bi-spin turns an icon once every 2s: bi-slash-lg, while pagelets, payments, sends and uploads are waited on · .placeholder, a faint grey line that pagelet placeholders show while they load, pulsing inside .placeholder-glow

Confirming

data-confirm (and every destroy link, data-method="post" with a path ending in destroy, asking Are you sure?: only links marked data-method="post" are sent as a POST, so links in user content can't be): unless the main pointer is a finger, confirm.js holds the click and asks in a popover above the element, then lets the click through. On touch screens (pointer: coarse) it uses the browser's confirm(), which is centred with big buttons; call neither yourself · a floating panel like a dropdown menu, at most 18.75rem wide · Cancel (btn-secondary) and a btn-sm named after the action: data-confirm-label, or the element's own text or title when it's 24 characters or fewer, or Confirm. btn-danger for destroy links and danger elements, otherwise btn-primary · focus goes to the action; Escape, Cancel or a click elsewhere closes it · ask a real question with the consequence in it, not Are you sure?

Charts

Chart.js, with white bold value labels from chartjs-plugin-datalabels.
Bar (_chart.erb): monthly counts in --theme-500, with the current month's projection stacked on top in the 500 at 50% (make_projection estimates it from the days so far; here it's fixed)
Doughnut (_stats.erb): chart_colors turns the theme's hue evenly round the wheel at oklch lightness .71, chroma .11, so every segment has the same weight

Tabs

.nav · .875rem · no focus ring · .nav-tabs: weight 600 in --bs-secondary-color on a 1px --bs-border-color rule; the active tab --bs-emphasis-color with a 2px --theme-500 underline, which hover and keyboard focus preview in --bs-gray-400 · always in a .nav-wrapper: tabs never wrap but scroll sideways with no scrollbar, while there's more that way, a --bs-secondary-color chevron at that edge scrolls on by three quarters of a width, the tabs clear under it and fading in over the next 2rem, and the active tab scrolled into view · .tab-content padding 1rem

Modal

37.5rem wide from 768px · no border or shadow

Search page

The /search page (search.erb), and lists of people like birthdays. A large input with a round clear button, a row of tabs, and results between rules.
.search-input: up to 32.5rem, form-control-lg, the clear button a 1.5rem --bs-gray-400 circle, --bs-gray-600 on hover · the tabs are .nav-tabs · .search-result-list: each result base top and bottom with a 2px --bs-border-color rule below, the image floated in a 10rem column (5.625rem on mobile), the name an h2 · people results (.search-result-person, with .search-result-stacked content) are a grid on mobile: the name and buttons (.search-result-header) beside the photo, the details table under both

Header search and autocomplete

  • Breathwork Collective
  • Breath and sound journey
  • Holotropic breathwork
Search: offcanvas-top, 2.8125rem, --bs-gray-800, backdrop at .9 · autocomplete (here and on the messages page): flush under its input, so square at the top with the base radius below, and while it's open (.autocomplete-open) the input's bottom corners are square, with no bottom border or focus ring, like Select2, --hairline border, --bs-box-shadow, items --space-2xs across --space-base, hover --bs-tertiary-bg like dropdown items

Notifications and messages dropdowns

.notification-list: list-group-flush, items --space-sm across --space-base, smaller newsfeed icons · .message-preview: unread --theme-100, hover --bs-tertiary-bg like the notification items

Messages

messages.css, loaded only by /messages: their bubbles --bs-gray-200 with --bs-body-color text, yours --theme-500 with white text and underlined links, each --bs-border-radius-xl round with a tail at the bottom corner, on the left for theirs and the right for yours · their avatar a 1.5rem circle beside the bubble's tail · Read and when, --font-size-xs in --bs-secondary-color, under your last message · the reply box is the ask box; Enter sends, Shift+Enter adds a line

Mobile bottom sheet

Below 768px, nav crumbs scroll sideways like tabs, with the same edge fades and chevrons, and they (dropdown_nav, ul_nav), the notifications and messages dropdowns open in #nav-dropdown-sheet (layouts/application.erb) in place of a dropdown: an offcanvas-bottom as tall as its content up to 80dvh, its top corners --bs-border-radius-xl · sheets.js copies the menu in as a list-group-flush with no borders, headers as .dropdown-header with no line, as in nav crumbs, dividers as an hr · the event stats row opens in it too, as a list of its columns

Date and icon tiles

The event page's date, location and link, as tiles beside the details they stand for. On the site they're CSS (.event-detail in app.css, over event_details.css): white, a --hairline border, --bs-box-shadow-sm, .3em corners, 2.5em square; the month a bold, letter-spaced band of --theme-50 in --theme-800 over a semibold day; icons 1.5em in black at 50%. On hover the tile turns solid theme. Emails can't rely on that CSS, so they show the same tiles as PNGs from rake icons:generate (app/views/icons), at 2.75em with the tile inset to match: the month band green for the default theme, grey for events and organisations with their own colour (the location and camera tiles are the same in both).
Nov 7 Sat 7th Nov 2026
7pm – 10pm EST (UTC -05:00)
 
Hackney, London
 
https://dandelion.events/e/breathwork-and-sound-journey
Site: .event-detail.calendar-date, .event-detail.location (or the camera for online events) and .event-detail.link · hover one
Default theme
Own theme colour
Email: 160px PNGs in app/assets/images/icons/calendar/png and icons/image, picked by emails/_tickets_table.erb

Event block

How events are listed on organisation and search pages (events/_block.erb, with _block_main.erb). From 992px the image sits on the right, filling its half (cropped when the text is taller), and the row is a white card with a --hairline border, base radius and --bs-box-shadow-sm; below that the image stacks on top.

Breathwork and sound journey

Featured Sold out
Name h2 · labels: Featured text-bg-yellow, Sold out or Sales ended text-bg-danger, Locked text-bg-secondary · tags text-bg-primary, up to 5, with bi-stars when AI-tagged · .icon-list at fs-5, each line's icon hung 2em to the left, with a tooltip naming it

Tiles

How organisations, gatherings and activities are listed (_block.erb, from organisations/, gatherings/ and activities/_blocks.erb): two to a row, then four from 992px, each 10rem high.
The whole tile links through the title's .stretched-link; hovered or keyboard focused, it takes --bs-box-shadow and a photo's black eases to 40% · title .block-title: --font-size-xl at every width, clamped to 4 lines, so a line is always left empty at the bottom; long words break · with an image: the photo under black at 50%, white text · without: .block-fill, the lavender wash (lavender-50 to paper) with a --hairline border, dark text · an icon such as the verified tick follows the title after a &nbsp;, so it keeps to the line with its last word · admins get a pencil on hover, and activities a link to their organisation at the bottom right

Ticket purchase form

The Select tickets card on an event page (purchase/_purchase.erb), one row per ticket type from purchase/_ticket_type.erb, then discounts, a donation and the total down the right-hand column. Shown here without purchase.js, so totals don't add up.

Select tickets

Weekend
Standard £22
Solidarity 2 left £8

For those on a low income. No questions asked.

Day passes
Pay what you can £5 - £30
Drag the slider
Early bird £15
Apply a code
Add a donation to Dandelion
£
Total
£
Ticket groups: a .ticket-group-heading row, a bold --font-size-lg (1.2rem) heading at line-height 1.2 with .25rem below it, its group's first divider a 2px rule in --bs-emphasis-color · each type: the name bold at --font-size-md (.ticket-type-name), a lock icon if it's secret or for members, "3 left" or fewer as a text-bg-secondary badge · price at the same --font-size-md (.ticket-type-price), not bold: fixed, a form-range for a range (the price shows once dragged), or a £ input group for pay what you like, which enables the quantity · quantity a full-width form-select, 0 to 99, or to the number left if fewer · the description in --bs-secondary-color, its paragraphs without margin · sold out: Join waitlist (btn-sm btn-outline-primary), which opens the waitlist modal (purchase/_ticket_type_waitlist_modal.erb), or a Sold out badge in text-bg-danger, as in event blocks, the carousel and the places-filled bar · a ticket type whose sales have ended says Sales closed in text-bg-secondary · the right-hand column: Apply a code, a membership discount, the donation (1/5/10% of the total, 1/2.5/5% on large orders, or a set amount for the organisation) and the disabled Total, Credit, Discount and Balance fields from currency_input_row, each a bold label over a 5em input group · below 576px cells take table-sm's .5rem with none at the table's edges, and the range hint says just Drag, so the three columns fit a phone

Comments

Lift shares from London

Maya 2 hours ago
I'm driving down from Hackney on Friday morning, room for three. Message me!
Jonas 1 hour ago
Count me in, thank you @Maya
A post (comments/_post.erb) is a card with a text-bg-primary header, its subject · each comment: an avatar-xs, the name bold, the time in --bs-secondary-color, the body linkified · reactions bordered tiles, your own bordered --theme-500, with the reactors' names in a tooltip

Avatars

Maya Okoro
xs
Jonas Berg
sm
Amara Nwosu
md · private
Maya Okoro
lg
Jonas Berg
xl
Square (accounts/_square.erb), except the 1.75rem header avatar (shown only for an uploaded image) and the 1.5rem chat avatars, which are round: the account's thumbnail or Gravatar, or failing both their initials (lib/generated_avatar.rb), a stencil in the theme colour: the PNG is white at 80% with the letters cut out, and .avatar-initials (Account#avatar_class) puts --theme-500 behind it once it has loaded (.avatar-loaded, app.js; white until then), so the letters are the 500 and the rest a tint close to the 100; a plain tint for names not in Latin letters, which the font lacks, with their name in a tooltip · a size class, from size: (:sm by default): xs 2rem in comments, sm 3.125rem in lists of people (the most common), md 4rem in message threads and map info windows, lg 4.6875rem for team and gathering members, xl 6.25rem in account rows, 2xl 9.375rem beside feedback quotes, fill to take its column's width · img.private at 50%, full on hover · .profile-photo, the large photo at the top of a profile or application: 12.5rem, framed in white

Upcoming birthdays

Upcoming birthdays

Maya Okoro
Maya Okoro
Thu 5th Nov
Today
Jonas Berg
Jonas Berg
Fri 6th Nov
Tomorrow
Amara Nwosu
Amara Nwosu
Wed 11th Nov
in 6 days
All birthdays →
The home page's card (accounts/_birthday.erb): an avatar-xs, the name, and the weekday and date small in --bs-secondary-color · on the right a badge from accounts/_birthday_when.erb, also used on /birthdays and a gathering's members: text-bg-yellow with a cake for today, text-bg-primary for tomorrow, text-bg-secondary for in n days

Ratings and feedback

4.8/5 from 36 ratings across 12 events
4.3
12 ratings
5 6
4 4
3 2
2 0
1 0
.average-rating (event_feedbacks/_average_rating.erb): the average at --font-size-2xl, the count in --bs-secondary-color, a star per point, in facilitator info · public feedback (event_feedbacks/_event_feedbacks.erb) opens with .rating-breakdown (_rating_breakdown.erb): the average at --font-size-3xl, bold, over its stars and count, beside a bar per star level, 5 down to 1, its numbers in tabular figures so the stars line up, in --color-orange-400 on --bs-tertiary-bg, at most 20rem wide · then the list, one .feedback-entry per rating with a translucent hairline between: the stars (event_feedbacks/_stars.erb, all five, the ones not given as .star-off at 40% in --bs-tertiary-color) and the date in --bs-secondary-color, then the public answers at .readable, then who and which event in small text · no profile pictures: the name links to the profile, or for people who aren't publicly visible is abbreviated with no link, and anonymous feedback reads Anonymous · removed feedback at 50% reads Removed in place of its stars · the organisation's reply under its answers as .feedback-reply, a 0.25rem primary rule on the left, headed by a bi-reply-fill and who replied in semibold small text · .read-more (rich_text.js) cuts a reply at its first line break and adds a Read more link at --font-size-xs, which shows the rest · the feedback form (event_feedbacks/build.erb, styled by event_feedbacks.css): five 2.75rem stars on a --bs-tertiary-bg panel, radius lg, --bs-gray-300 until chosen, then --color-orange-400 with .chosen · each star is the label of a visually hidden radio, so a click, a tap or the keyboard (Tab, then the arrow keys) chooses it, with the focus ring on the star

Image wall

.image-wall: groups of people as avatars that wrap, --space-2xs between rows: who chose an option or is interested in an activity, team and gathering members, followers · on options, teams and rotas, a "Show names instead of pictures" link reloads them as names separated by middots

Calendar

An organisation's events by month or as a list (events/_calendar.erb and calendar.js), with FullCalendar. Events are --theme-500, or in embeds the organisation's own colour, so the grid has one colour: today's circle and the events. Each has a tooltip with its location and time.
FullCalendar's Bootstrap 5 theme (events/_fullcalendar.erb), restyled through the calendar-* classes calendarOptions (calendar.js) adds, as its elements have no class names of their own · day numbers and headings --bs-secondary-color · today's number white on a --theme-500 circle, with no day tint · events --theme-500, the theme's eventColor (--bs-primary): a dot and title in the month, filled when all-day, a row in the list · toolbar buttons like .btn-secondary, the active view filled --theme-500 like the active pill · prev and next borderless · the title --font-size-2xl, dropping to --font-size-md on mobile, where Today hides (d-none d-md-inline-block)

Timetable

A gathering's activities (timetables/_tactivity.erb), dragged into a table-striped grid of times by spaces · 8rem tall, 16rem wide in the grid · white with a 2px --bs-body-color border and text · .tactivity-image takes the image from --tactivity-image, darkened by black at 50% under white text

Map markers

People
Gatherings
Events
Organisations
8
cluster
42
cluster
130
cluster
Google Maps (map.js): advanced markers drawn in HTML, so they take the tokens directly · .map-marker pins in --color-green-500 (the default green, whatever the theme) for people and gatherings, --color-red-400 for events and organisations, set through --map-marker-color, each with its white Bootstrap icon · .map-cluster-sm, -md and -lg clusters (under 10, under 100, then more) in --color-green-500, --color-orange-500, then --color-red-500, each with a 40% halo and a white Plus Jakarta Sans count · .map-loading fades either to half while its details load · areas drawn in --color-orange-400, filled at 35% · location fields (places.js placeAutocomplete) suggest places in the jQuery UI autocomplete menu, with Google Maps attribution under it

Map info window

Maya Okoro

Maya Okoro

Hackney, London

breathwork sound
Clicking a marker loads /points/:model/:id into Google's InfoWindow, wrapped in .infowindow: a person (points/_account.erb, as here), an organisation or gathering, or an event as its block's text without the image (events/_block_main.erb as ul.map-event, list items at --font-size-base) · Google draws the white box, its tail and the close button; app.css sets its font to Plus Jakarta Sans, sizes the close button and zeroes .row margins inside

Email previews

.email-preview-iframe-container, on the ticket, reminder and feedback request email pages (events/ticket_email.erb and friends): the email rendered by its own route into a borderless iframe that fills the container, 80vh tall (20rem here)

Empty states

No upcoming events

A short "No …" phrase in --bs-secondary-color, left-aligned and without a full stop, where the list or table would be: grey, since it is the absence of content rather than content

Still at the printers

This edition of the Daily isn't out yet. Check back soon!

Read the latest Daily
.empty-notice, for a page whose whole content is missing (daily.erb's dates with no edition), not a list inside one: centred, up to 28rem wide, --space-2xl above and below · .empty-notice-icon, a 5rem circle with a --hairline border and --bs-box-shadow-sm like the principles, holding a --font-size-3xl icon · .empty-notice-title at --font-size-2xl · a line of text in --bs-secondary-color, in sentences, so with full stops · an optional btn-outline-primary to somewhere with content

Principles

No platform fees, the co-op and open data, on the signed-out home and /donate (_badges.erb). Each is a 7rem white circle with a --hairline border and --bs-box-shadow-sm, like the date and icon tiles, holding its black line drawing (gift-cycle.png, the gift and arrows from gift.png without the hand; coop.png; opendata.png), then its line of text. Laid out by its own width with container queries: three centred columns from 36rem, otherwise a list with the circle beside the text. columns: true keeps the three columns at any width, the circles shrinking to fit and the text at --font-size-xs, as in the donation modal.

No platform fees: we seek to inspire a culture of gifting

Fair source and open data on ATProto

No platform fees: we seek to inspire a culture of gifting

Fair source and open data on ATProto

p.action

A centred call to action for docs and email previews, where a .btn class can't be added. Drawn like btn-primary; emails use their own in email.css.

Onboarding callout

Welcomes and next steps for people who have just signed up, so they read as one journey with the sign-up pages: their lavender wash (--color-lavender-50 to --color-paper at 135°, flat from halfway) with a hairline border, radius lg and padding 1.5rem, a --color-lavender-500 icon, an xl title and a secondary line. The organiser journey (the _onboarding_callout partial: create your organisation and set up payments in organisations/build.erb, then create your first event in events_build/build.erb) ends with its checklist: done steps ticked in lavender, the current one bold in --color-lavender-800, the rest grey; payments is only ticked once the organisation has a payment method. The first-sign-in and "Thanks for your interest" welcomes on the profile page (accounts/edit.erb) have no checklist. Only for onboarding, so it stays distinctive; other notices are alerts.

Set up payments

To accept payments, add details for Stripe or another payment processor.

  1. Create your organisation
  2. Set up payments
  3. Create your first event

Welcome to Dandelion!

Your privacy is important to us. Choose what you share using the toggles on the right.

Stats band

Headline numbers on the signed-out home (_stats_band.erb), under the hero. Shown here with sample figures (Ticket::HOME_PAGE_STATS_SAMPLE); on the home they come from Ticket.home_page_stats, which the daily late:errands task refreshes. On development the home always shows the sample figures, so all three cells show.
204,333
tickets sold
$10.0m+
in ticket sales
0% platform fees
4.6 / 5
average attendee review
One card cut into cells by 1px gaps over --hairline: a --hairline border, radius lg, --bs-box-shadow-sm · laid out by its own width with container queries, so it fits beside the sidebar: two cells to a row, an odd last cell taking the full width, then one row of all of them from 48rem · each cell white, padding 1rem (1.5rem from 36rem), --theme-50 on hover over --transition-fast · the figure in the header font, bold, --font-size-2xl (3xl from 60rem), tabular figures, over a semibold label, a 2rem visual and a small line of context (ticket sales has no context line) in --bs-secondary-color · visuals: an illustrative sparkline, not real figures: a fixed shape rising in uneven steps, different for tickets and events, hidden from screen readers (a 2px --theme-500 line over --theme-100), a --theme-50 pill in --theme-800 on one line, --font-size-xs (base from 36rem) (0% platform fees), or the rating's stars in --color-orange-400 (half from .25, whole from .75) · ticket sales are in the visitor's currency, in m or k · a cell whose figure is zero is left out · figures count up (countUp in utils.js) as the band scrolls into view, not under prefers-reduced-motion

AI well and ask box

The AI well is the only sheen, so the gradient itself marks AI: 135°, --ai-well-from flat to halfway, then to --ai-well-to. That's --color-lavender-100 to --color-lavender-400 on the default site, and the theme's 100 to 400 on themed pages (theme.css). Padding 1rem, radius lg.
Suggested description
A gentle evening of breathwork and sound for all levels.
06

Theming

Organisations and events set one colour. theme_css_variables sets --theme-500, app.css derives the ramp from it, and every primary button, link, focus ring, crumb, check, progress bar, stats chart, alert and toast tint and date tile month follows. theme.css is only variables: as well as the theme colour, it switches the AI well to the theme ramp (see AI well and ask box), while the default site keeps it lavender as its own accent.

DEFAULT ·
Book
Saved. Text is --theme-800
AI well: lavender
THEMED · #1a73e8
Book
Saved. Text is --theme-800
AI well: theme tint
THEMED · #ffc107 → #e6ac00 (clamped)
Book
Saved. Text is --theme-800
AI well: theme tint
Each themed panel sets the same variables on itself that /theme.css sets on :root, from theme_css_variables(clamp_color(colour)), and has the theme-scope class so the ramp and focus ring are recomputed from its --theme-500.