Preference helpers
Seven small packages for each framework, each one own one user job from start to finish, plus bar wey put five of dem together. You don meet theme-picker; here na locale-picker, text-size-picker and motion-picker (preferences), share-picker and search-picker (actions), date-time-picker (form value), and picker-bar.
Wetin you go build
Settings panel where users go pick dia language and text size — choices wey apply to di whole document, wey dey stay between visits and wey cost you two components and few lines of CSS. About 15 minutes.
Before you start
Finish di framework tutorial for your stack. Di samples below na Svelte; di React, Vue, Angular, HTML, Nunjucks, and Blazor helpers match contract-for-contract.
Di shape wey dem share
- Headless icon button wey open WAI-ARIA APG listbox (or, for date-time-picker, date-picker dialog) — no native
<select>, so every framework go get identical keyboard behaviour and styling hooks. - One class hook for each package (
.locale-picker,.text-size-picker,.share-picker,.date-time-picker), zero CSS shipped. - SSR-safe: DOM writes dey happen only inside di mount/effect lifecycle.
- i18n-clean: every user-facing string na prop.
- E dey available for Svelte (canonical), React, Vue, Angular, HTML custom element, Nunjucks, and Blazor — di same contract for each one.
theme-picker, locale-picker, and text-size-picker each one own one user
preference from start to finish (selection, DOM application, optional
localStorage persistence storageKey
through prop). share-picker and date-time-picker different for kind: share-picker own action — e no apply anything to di document and e no persist anything — and date-time-picker own
form value, because date inside form na data, no be preference.
locale-picker — language and direction
E let di user pick locale, then e write lang and
dir to di document root so your i18n library, your CSS, and assistive technology go follow. RTL dey detected automatically for Arabic, Hebrew, and other right-to-left scripts. E focus only on signalling — your i18n library go handle di 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 dey labelled from built-in table of locale names (you fit override am through localeLabels), each option carry im own
lang attribute so screen readers go pronounce “Français” for French, and underscore codes like en_US
dey round-trip losslessly to BCP 47 en-US. Wire
onChange (or di bound value) enter i18next, Paraglide, vue-i18n, or anyone wey you dey use.
text-size-picker — reader-controlled sizing
Accessibility win wey take minutes: di helper dey set
data-text-size="{slug}" for di document root and your CSS dey map 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 inside rem and di whole app go scale with di user choice. Labels dey title-cased from di slugs (“X Large”) or dem override am with sizeLabels.
Check your work: pick “Large” from di picker, and di page text go grow. Reload — e still large: di choice persist to
localStorage. Switch your language picker to Arabic and
dir="rtl" go land for di document automatically.
share-picker — send dis page
Single-glyph button wey open di device native share sheet where one dey, and if no dey, e fall back to list of destinations wey you supply, plus copy-di-URL action. E no ship any social-network URLs — which networks belong inside your product na your call, no be di design system own.
<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 default to location.hrefso di common case no need any wiring. Force path with strategy="list" or
strategy="native" when you need consistent behaviour across platforms for testing or screenshots.
date-time-picker — date, time, or both
Text field wey you fit type inside, plus icon button wey open 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 dey come from Intl — and di value na always 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",
}}
/>
Di ISO value dey identical to wetin <input type="date">
post, so you fit swap di native control in or out without to touch your backend.
motion-picker — respect reduced motion
Di helper dey set data-motion="{slug}" for di document root. Im first value dey come from di visitor operating-system
prefers-reduced-motion setting — always, without opt-in flag, because motion na real accessibility need (WCAG 2.3.3). Your CSS and scripts go decide wetin dem go suppress.
<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 — search box wey no dey disturb
Icon button wey open search field. To press Return, or di ⏎ button beside di field, go navigate with plain GET request to
/?<text> (di text dey trimmed and URI-encoded; empty query no dey go anywhere). Im three accessible names dey required, because di helper no ship 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 dey change di path (/search?foo), and
navigate dey keep di navigation inside di app; without am di helper dey call location.assign.
picker-bar — di whole header row at once
picker-bar dey render search, theme, locale, text-size and share for one row, with di full set of reference themes and di seven-step text scale already wired. Every accessible name come inside 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 dey show im label inside tooltip when di pointer rest on im button or di button get keyboard focus. Di tooltip dey stay as di pointer move enter am, Escape dey dismiss am wherever focus dey, and e no dey ever show while di picker popup open. E carry di same text as di button accessible name, so nothing dey to configure and nothing dey to translate twice; di reference themes dey style am.
Settings panel for one place
Di four preference helpers dey 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: di locale-picker, text-size-picker, share-picker, date-time-picker and combined-panel snippets dem compile individually against di published 0.1.1 packages through
svelte/server (2026-08-29); di motion-picker, search-picker and picker-bar snippets dem render against di packages own components on 2026-10-06.
Where to get dem
# 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 get im own spec, tests wey map to di spec clauses, runnable examples (including SSR cookie patterns), and build + publish pipeline. Di Svelte packages na canonical; when doubt dey, dia contracts go win.
Next steps
- Component catalog — di full 571-component reference.
- Help — troubleshooting and FAQ.
- Why Lily — and how to support di project.
You get questions as you dey go? Di help page full of answers — and if any step fit clearer, tell us and we go gladly improve di tutorial.