npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lily-design-system-html-text-size-picker

v0.1.1

Published

Lily Design System - HTML custom element text size picker

Readme

<text-size-picker> (HTML helper)

A reusable, headless vanilla HTML/JS text-size picker, packaged as the <text-size-picker> custom element. Renders an icon button that opens a dropdown listbox (WAI-ARIA APG listbox pattern) of text-size slugs and, on every change, sets data-text-size="{slug}" on a target element (default document.documentElement), optionally persisting the choice to localStorage.

It is structurally identical to its theme-picker and locale-picker siblings — all three helpers in this catalog are the same shape.

The single source of truth is spec/index.md. This file is the comprehensive user guide.

Why this exists

Letting users resize text — beyond browser zoom — is a WCAG 2.2 commitment (1.4.4 Resize Text, 1.4.12 Text Spacing). This helper owns the selection + persistence + DOM application of a text-size preference; the consumer owns the typography itself via CSS keyed on [data-text-size="{slug}"]:

: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%;
}

The element is a direct port of the Svelte canonical lily-design-system-svelte-text-size-picker. APIs and behaviour match; only the framework idioms differ.

Install

// One side-effect import registers <text-size-picker> globally:
import "./lily-design-system-html-text-size-picker";

// Or grab the class, helpers, and types:
import {
  TextSizePicker,
  sizeName, // "x-large" → "X Large"
  nextTextSizePickerId,
  LATIN_CAPITAL_LETTER_A, // the default "A" glyph
  type TextSizePickerProps,
  type TextSizePickerChangeDetail,
} from "./lily-design-system-html-text-size-picker";

The barrel guards registration with customElements.get("text-size-picker") so re-imports and SSR contexts don't throw.

Quick start

<script type="module" src="/dist/text-size-picker.js"></script>

<text-size-picker
  label="Text size"
  sizes="small,medium,large,x-large"
  storage-key="lily-text-size"
></text-size-picker>

<p class="text-size-picker-status" aria-live="polite">Text size: Medium</p>

When the user picks large, the element:

  • sets data-text-size="large" on <html>,
  • writes "large" to localStorage["lily-text-size"],
  • dispatches new CustomEvent("textsizechange", { detail: { size: "large" }, bubbles: true, composed: true }).

The element does not style anything — your CSS maps each slug to a real font scale. The closed button shows only the "A" glyph, so the status region above is the default pattern: it is the only thing that tells a user which size is active. Wire textsizechange (or read el.value) to keep it current:

const picker = document.querySelector("text-size-picker")!;
const status = document.querySelector(".text-size-picker-status")!;

picker.addEventListener("textsizechange", (e) => {
  const { size } = (e as CustomEvent<{ size: string }>).detail;
  status.textContent = `Text size: ${picker.labelFor(size)}`;
});

Because the list is a plain flow element, give it positioning — the helper ships no CSS at all:

.text-size-picker {
  position: relative;
}
.text-size-picker-list {
  position: absolute;
  z-index: 10;
}

Default size

The default slug is "medium" whenever "medium" appears in your sizes list. The full resolution order on first connectedCallback:

  1. value attribute (if non-empty)
  2. localStorage[storage-key] (if storage-key is set and readable)
  3. default-value attribute
  4. "medium" (if present in sizes)
  5. sizes[0]
  6. "" — nothing is applied; the picker waits for user interaction

Attributes

The complete table is in spec/index.md §4.1. Highlights:

| Attribute | Type | Required | Notes | | --------------- | ------------- | -------- | ----------------------------------------------- | | label | string | yes | aria-label on the button and the list. | | sizes | string (CSV) | yes | Available slugs. | | value | string | no | Current slug. | | default-value | string | no | Initial when nothing else applies. | | storage-key | string | no | localStorage persistence. | | name | string | no | Defaults to "text-size"; on the hidden input. | | size-labels | string (JSON) | no | Per-slug label overrides. | | class | string | no | Extra class on the root <div>. |

There is no detection attribute. Unlike theme-picker (detect-from-system) and locale-picker (detect-from-navigator), the platform exposes no preferred-text-size signal to detect.

Class hooks

| Class | Element | | -------------------------- | ---------------------------------- | | text-size-picker | root <div> | | text-size-picker-button | the icon <button> | | text-size-picker-icon | the <span> holding the "A" glyph | | text-size-picker-list | the <ul role="listbox"> | | text-size-picker-option | each <li role="option"> |

Style [data-active] (keyboard-highlighted) differently from [aria-selected="true"] (chosen) — they mean different things.

JS properties

Every observed attribute mirrors a camelCase JS property:

const picker = document.querySelector("text-size-picker") as TextSizePicker;

picker.sizes = ["small", "medium", "large", "x-large"]; // CSV-encoded in attribute
picker.sizeLabels = { small: "Compact", large: "Comfortable" };
picker.target = document.querySelector("section.panel") as HTMLElement;

el.target accepts HTMLElement | null and has no attribute form.

Events

| Event | Detail | Bubbles | Composed | | ---------------- | ------------------ | ------- | -------- | | textsizechange | { size: string } | yes | yes |

Labels

size-labels overrides the default rendering per slug; otherwise the slug is title-cased per hyphen-word (x-largeX Large).

Persistence

Pass storage-key to persist the active slug to localStorage. On a fresh mount the picker reads back the stored slug as part of the initial-value resolution. Storage errors are silently swallowed.

Custom rendering

Subclass and override renderButtonContent() to replace the glyph. It is the HTML-helper stand-in for the children snippet the Svelte / React / Vue siblings take, and this.value, this.open, and this.labelFor(...) are all readable inside it:

class MyTextSizePicker extends TextSizePicker {
  renderButtonContent(): Node {
    const span = document.createElement("span");
    span.textContent = `A — ${this.labelFor(this.value)}`;
    return span;
  }
}
customElements.define("my-text-size-picker", MyTextSizePicker);

The base class still builds the button and listbox, so the aria wiring and the whole keyboard contract keep working.

Accessibility

  • The control is an icon button (aria-haspopup="listbox", aria-expanded, aria-controls) opening a <ul role="listbox">. aria-label={label} names both.
  • The keyboard contract is implemented in JS, not inherited from the platform: ArrowDown/Enter/Space open, ArrowUp opens on the last option, arrows clamp, Home/End jump, PageUp/PageDown move by ten (clamped), Enter/Space select, Escape closes unchanged, Tab moves focus to the button first and then closes, so the browser's default Tab proceeds from the picker's position. Printable characters run a 500 ms typeahead: a repeated character cycles through its matches, a buffer of differing characters refines the match from the active option.
  • Focus sits on the <ul> while open; the highlighted option is conveyed by aria-activedescendant. Style .text-size-picker-list:focus-visible and .text-size-picker-option[data-active], or keyboard users get no feedback.
  • Directly supports WCAG 2.2 — 1.4.4 (Resize Text), 1.4.10 (Reflow), and 1.4.12 (Text Spacing).
  • Three known tradeoffs (icon-only naming, a custom listbox being weaker than a native <select>, font-dependent glyph rendering) are documented honestly in docs/accessibility.md.

SSR and static-site generation

The element compiles cleanly under static-site generators. On the server no lifecycle hook runs; the SSG emits the literal <text-size-picker> tag, and the browser upgrades it after the JS loads. For zero-flicker first paint, pre-render <html data-text-size="…"> and the matching <text-size-picker value="…"> host.

Testing

pnpm test

Exercises every numbered acceptance criterion in spec/index.md §7.

Files in this directory

| File | Purpose | | --------------------------- | ------------------------------------------------ | | spec/index.md | Single source of truth. | | AGENTS.md | Fast-index pointer; loads the AGENTS bundle. | | CLAUDE.md | @AGENTS.md. | | text-size-picker.ts | The custom-element class. | | text-size-picker.test.ts | vitest suite. | | index.ts | Barrel + side-effectful customElements.define. | | index.md | This file. | | docs/accessibility.md | Roles, keyboard contract, known tradeoffs. |

License

MIT or Apache-2.0 or GPL-2.0 or GPL-3.0 or BSD-3-Clause. Contact [email protected] for other terms.


Lily™ and Lily Design System™ are trademarks.