Preference helpers
Seven small packages per framework, each owning one user job end to end, plus a bar that assembles five of them. You've met theme-picker; here are locale-picker, text-size-picker and motion-picker (preferences), share-picker and search-picker (actions), date-time-picker (a form value), and picker-bar.
What you'll build
A settings panel where users pick their language and text size — choices that apply to the whole document, persist across visits, and cost you two components and a few lines of CSS. About 15 minutes.
Before you start
Finish the framework tutorial for your stack. The samples below are Svelte; the React, Vue, Angular, HTML, Nunjucks, and Blazor helpers match contract-for-contract.
The shape they share
- A headless icon button that opens a WAI-ARIA APG listbox (or, for date-time-picker, a date-picker dialog) — no native
<select>, so every framework gets identical keyboard behaviour and styling hooks. - One class hook per package (
.locale-picker,.text-size-picker,.share-picker,.date-time-picker), zero CSS shipped. - SSR-safe: DOM writes happen only in the mount/effect lifecycle.
- i18n-clean: every user-facing string is a prop.
- Available for Svelte (canonical), React, Vue, Angular, HTML custom element, Nunjucks, and Blazor — same contract in each.
theme-picker, locale-picker, and text-size-picker each own one user preference end to end (selection, DOM application, optional localStorage persistence via a storageKey prop). share-picker and date-time-picker are different in kind:
share-picker owns an action — it applies nothing to the
document and persists nothing — and date-time-picker owns a form value, since a date in a form is data rather than a
preference.
locale-picker — language and direction
Lets the user pick a locale, then writes lang and dir to the document root so your i18n library, your CSS,
and assistive technology all follow. RTL is detected automatically for
Arabic, Hebrew, and other right-to-left scripts. It focuses purely on
signalling — your i18n library handles the translation.
<script>
import LocalePicker from "@lilydesignsystem/svelte-locale-picker";
let locale = $state("en-US");
</script>
<LocalePicker
label="Language"
locales={["en-US", "fr", "ar", "he", "ja"]}
bind:value={locale}
storageKey="my-app-locale"
/> Options are labelled from a built-in table of locale names (overridable
via localeLabels), each option carries its own lang attribute so screen readers pronounce
"Français" in French, and underscore codes like en_US round-trip losslessly to BCP 47 en-US. Wire onChange (or the bound value) into i18next, Paraglide,
vue-i18n, or whatever you use.
text-size-picker — reader-controlled sizing
An accessibility win that takes minutes: the helper sets data-text-size="{slug}" on the document root and your
CSS maps each value to sizing.
<script>
import TextSizePicker from "@lilydesignsystem/svelte-text-size-picker";
</script>
<TextSizePicker
label="Text size"
sizes={["small", "medium", "large", "x-large"]}
storageKey="my-app-text-size"
/> :root[data-text-size="small"] { font-size: 87.5%; }
:root[data-text-size="medium"] { font-size: 100%; }
:root[data-text-size="large"] { font-size: 112.5%; }
:root[data-text-size="x-large"] { font-size: 125%; } Size your layout in rem and the whole app scales with the
user's choice. Labels are title-cased from the slugs
("X Large") or overridden with sizeLabels.
Check your work: pick "Large" from the picker, and the
page text grows. Reload — it's still large: the choice persisted to localStorage. Switch your language picker to Arabic and dir="rtl" lands on the document automatically.
share-picker — send this page
A single-glyph button that opens the device's native share sheet where one exists, and otherwise falls back to a list of destinations you supply, plus a copy-the-URL action. It ships no social-network URLs — which networks belong in your product is your call, not the design system's.
<script>
import SharePicker from "@lilydesignsystem/svelte-share-picker";
const targets = [
{ id: "mastodon", label: "Mastodon",
href: (url, title) => `https://mastodon.social/share?text=${encodeURIComponent(title)}%20${encodeURIComponent(url)}` },
{ id: "email", label: "Email",
href: (url, title) => `mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(url)}`, newTab: false },
];
</script>
<SharePicker
label="Share this page"
title="An article worth reading"
{targets}
copyLabel="Copy link"
copiedLabel="Link copied"
/> url defaults to location.href, so the common
case needs no wiring. Force a path with strategy="list" or strategy="native" when you need consistent behaviour across
platforms for testing or screenshots.
date-time-picker — a date, a time, or both
A text field you can type into, plus an icon button that opens a
WAI-ARIA APG date-picker dialog. Locale-correct by construction — month
names, weekday names, first day of week, and 12- vs 24-hour clock all
come from Intl — and the value is always a sortable,
timezone-free ISO string: "2026-03-15" for mode="date", "09:30" for mode="time", "2026-03-15T09:30" for mode="datetime".
<script>
import DateTimePicker from "@lilydesignsystem/svelte-date-time-picker";
let appointment = $state("");
</script>
<DateTimePicker
inputId="appointment"
name="appointment"
label="Choose an appointment date"
locale="en-GB"
bind:value={appointment}
labels={{
previousYear: "Previous year",
previousMonth: "Previous month",
nextMonth: "Next month",
nextYear: "Next year",
confirm: "OK",
cancel: "Cancel",
}}
/> The ISO value is identical to what <input type="date"> posts, so you can swap the native control in or out without touching
your backend.
motion-picker — respect reduced motion
The helper sets data-motion="{slug}" on the document
root. Its first value comes from the visitor's operating-system prefers-reduced-motion setting — always, with no opt-in flag,
because motion is a real accessibility need (WCAG 2.3.3). Your CSS and
scripts decide what gets suppressed.
<script>
import MotionPicker from "@lilydesignsystem/svelte-motion-picker";
</script>
<MotionPicker
label="Motion"
motions={["no-preference", "reduce"]}
storageKey="my-app-motion"
/> :root[data-motion="reduce"] *,
:root[data-motion="reduce"] *::before,
:root[data-motion="reduce"] *::after {
animation: none !important;
transition: none !important;
scroll-behavior: auto !important;
} search-picker — a search box that stays out of the way
An icon button that opens a search field. Pressing Return, or the
⏎ button beside the field, navigates with a plain GET request to /?<text> (the text trimmed and URI-encoded; an empty
query goes nowhere). Its three accessible names are required, because the
helper ships no English.
<script>
import SearchPicker from "@lilydesignsystem/svelte-search-picker";
import { goto } from "$app/navigation";
</script>
<SearchPicker
label="Search this site"
inputLabel="Search terms"
submitLabel="Search"
placeholder="Search…"
action="/search"
navigate={(href) => goto(href)}
/> action changes the path (/search?foo), and navigate keeps the navigation in-app; without it the helper
calls location.assign.
picker-bar — the whole header row at once
picker-bar renders search, theme, locale, text-size and share
in one row, with the full set of reference themes and the seven-step text
scale pre-wired. Every accessible name arrives in one required labels object.
<script>
import PickerBar from "@lilydesignsystem/svelte-picker-bar";
</script>
<PickerBar
labels={{
search: "Search", searchInput: "Search terms", searchSubmit: "Search",
theme: "Theme", locale: "Language", textSize: "Text size", share: "Share",
}}
themesUrl="/themes/"
locales={["en", "fr", "es"]}
shareTargets={targets}
/> Reach any wrapped picker with searchProps, themeProps, localeProps, textSizeProps and shareProps.
Tooltips come built in
Every picker shows its label in a tooltip when the pointer rests on its button or the button has keyboard focus. The tooltip stays while the pointer moves onto it, Escape dismisses it wherever focus is, and it never shows while the picker's popup is open. It carries the same text as the button's accessible name, so there is nothing to configure and nothing to translate twice; the reference themes style it.
A settings panel in one place
The four preference helpers compose naturally:
<fieldset class="fieldset">
<legend>Preferences</legend>
<ThemePicker label="Theme" themesUrl="/themes/" themes={themes} storageKey="pref-theme" />
<LocalePicker label="Language" locales={locales} storageKey="pref-locale" />
<TextSizePicker label="Text size" sizes={sizes} storageKey="pref-text-size" />
<MotionPicker label="Motion" motions={motions} storageKey="pref-motion" />
</fieldset> Verified: the locale-picker, text-size-picker,
share-picker, date-time-picker and combined-panel snippets were compiled
individually against the published 0.1.1 packages through svelte/server (2026-08-29); the motion-picker, search-picker
and picker-bar snippets were rendered against the packages' own
components on 2026-10-06.
Where to get them
# one directory per package in the monorepo, e.g.
git clone https://github.com/LilyDesignSystem/lily-design-system
cd lily-design-system/lily-design-system-svelte-theme-picker
# or lily-design-system-react-theme-picker, -vue- / -angular- / -html- / -web-components- / -nunjucks- / -blazor- Each package has its own spec, tests mapped to the spec clauses, runnable examples (including SSR cookie patterns), and a build + publish pipeline. The Svelte packages are canonical; when in doubt, their contracts win.
Next steps
- Component catalog — the full 571-component reference.
- Help — troubleshooting and FAQ.
- Why Lily — and how to support the project.
Questions along the way? The help page is full of answers — and if a step could be clearer, tell us and we'll gladly improve the tutorial.