Skip to content

TextSizeControl

LG · 3300B gzip budget

In-page text scaling for low-vision readers, persisted and announced.

Category
Accessibility & preferences
Budget
lg, composes several components
External runtime
React / React DOM only
axe fixtureSSR rendersource scanbundle budget

Preview

Live fixture
Preview locale

Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/text-size-control.json

Copies the source into your project. Pulls in 6 shared registry items: cn, announce, segmented-control, use-locale, locale, use-stored-value.

New here? Set up Tailwind and import aliases first →

Source & props

Edit on GitHub ↗

Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.

"use client";

import { useEffect } from "react";
import { announce } from "../lib/announce";
import { cn } from "../lib/cn";
import { useStoredValue } from "../lib/use-stored-value";
import { SegmentedControl } from "./segmented-control";

const STORAGE_KEY = "gear5-ui:text-size";

export interface TextSizeControlLabels {
  legend: string;
  small: string;
  normal: string;
  large: string;
  larger: string;
  /** `{size}` is replaced with the chosen label. */
  announcement: string;
}

const DEFAULT_LABELS: TextSizeControlLabels = {
  legend: "Text size",
  small: "Small",
  normal: "Normal",
  large: "Large",
  larger: "Larger",
  announcement: "Text size: {size}",
};

const SCALES: Record<string, string> = {
  small: "87.5%",
  normal: "100%",
  large: "112.5%",
  larger: "125%",
};

export interface TextSizeControlProps {
  labels?: Partial<TextSizeControlLabels>;
  className?: string;
}

/**
 * An in-page text size control.
 *
 * Browser zoom scales everything including layout; this scales only type, by
 * setting the root font size — which is why every length in a component
 * library should be in `rem`. Readers with low vision, and anyone on a cheap
 * phone with a dense screen, adjust this far more often than analytics
 * suggest, because most sites do not offer it and they gave up asking.
 *
 * The choice persists, and is announced on change so it is confirmable
 * without seeing the result.
 */
export function TextSizeControl({ labels: labelOverrides, className }: TextSizeControlProps) {
  const labels = { ...DEFAULT_LABELS, ...labelOverrides };
  const [stored, setStored] = useStoredValue(STORAGE_KEY, "normal");
  const size = stored ?? "normal";

  // A real side effect on an external system — the document element outside
  // this component — which is what useEffect is for.
  useEffect(() => {
    document.documentElement.style.fontSize = SCALES[size] ?? SCALES.normal;
  }, [size]);

  const options = [
    { value: "small", label: labels.small },
    { value: "normal", label: labels.normal },
    { value: "large", label: labels.large },
    { value: "larger", label: labels.larger },
  ];

  return (
    <SegmentedControl
      label={labels.legend}
      options={options}
      value={size}
      onChange={(next) => {
        setStored(next);
        const chosen = options.find((option) => option.value === next);
        announce(labels.announcement.replace("{size}", chosen?.label ?? next), "polite");
      }}
      className={cn(className)}
    />
  );
}

What CI checks

Quality contract
  • Bundled, minified and gzipped against its tier budget
  • Audited by axe in the state previewed above
  • Rendered through react-dom/server with no browser globals
  • Scanned for network calls, dangerous sinks, and unguarded animation