{"slug":"page-control","name":"PageControl","packageName":"@liquidify/react","category":"controls","maturity":"stable","summary":"is the bare dot indicator for a paged collection — the row of neutral dots that shows how many pages exist, which one is current, and lets the user jump between them.","importPath":"@liquidify/react/page-control","sourcePath":"packages/@liquidify/react/src/components/page-control/page-control.tsx","specPath":"packages/@liquidify/react/src/components/page-control/spec.md","docsPath":"../liquidify-page/content/component-source/page-control.md","documentationStatus":"stable","storyIds":["controls-page-control--default"],"additionalExports":["PageControlSize","SizedControlProps"],"anatomy":[],"props":[{"name":"allowsContinuousInteraction","type":"boolean | undefined","required":false,"description":"Enables press-drag scrubbing across the dot row (`UIPageControl`'s continuous interaction): the pressed pointer commits the dot it settles over, dot-to-dot, until release. When `false`, only a discrete dot tap (and the keyboard) can change the page, and `backgroundDisplay=\"interacting\"` never reveals its plate — there is no scrub to reveal it for.","defaultValue":"`true`","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"aria-label","type":"string | undefined","required":false,"description":"Required accessible name (the tablist renders no visible label).","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"aria-labelledby","type":"string | undefined","required":false,"description":"Required accessible name via reference (alternative to `aria-label`).","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"backgroundDisplay","type":"\"automatic\" | \"always\" | \"never\" | \"interacting\" | undefined","required":false,"description":"Whether/when the frosted backing plate renders. `\"always\"` keeps it mounted; `\"interacting\"` mounts it only while the user is scrubbing (a held pointer that has travelled — a plain dot tap never reveals it); `\"automatic\"` / `\"never\"` never mount it. `\"interacting\"` therefore has no effect under `allowsContinuousInteraction={false}`, where no scrub exists. Maps SwiftUI `PageIndexViewStyle.BackgroundDisplayMode.interactive` (the plate reveals only during a live dot interaction) to the React value `\"interacting\"` — a deliberately different spelling from the removed `GlassMaterial` tier of the same era (D6, ADR-0010), which `material_scale.py` bans from `packages/*` source on an unrelated axis (the glass-surface material, not this presentational display enum). See spec.md `## Variants` for the full SwiftUI → React naming rationale.","defaultValue":"`\"automatic\"`","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"className","type":"string | undefined","required":false,"description":"Optional additional CSS class merged into the glass surface class. The supported per-instance customisation seam: override any `--lq-*` custom property from this class's CSS — the token cascade the engine reads (ADR-0004). No raw inline style is involved, so this stays ADR-0003 clean.","declaredIn":"packages/@liquidify/react/src/liquid-component-props.ts"},{"name":"count","type":"number","required":true,"description":"Total number of pages (`UIPageControl.numberOfPages`). Required.","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"defaultValue","type":"number | undefined","required":false,"description":"Uncontrolled seed for the current page index.","defaultValue":"`0`","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"disabled","type":"boolean | undefined","required":false,"description":"Disables the whole control.","defaultValue":"`false`","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"hidesForSinglePage","type":"boolean | undefined","required":false,"description":"Hide the row when `count === 1` under `indexDisplay=\"automatic\"`.","defaultValue":"`true`","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"id","type":"string | undefined","required":false,"description":"Base id for the deterministic per-dot ids (`${id}-tab-${i}`).","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"indexDisplay","type":"\"automatic\" | \"always\" | \"never\" | undefined","required":false,"description":"Whether the dot row renders.","defaultValue":"`\"automatic\"`","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"onChange","type":"((index: number) => void) | undefined","required":false,"description":"Fires with the next 0-based index whenever the page changes.","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"ref","type":"Ref<HTMLDivElement> | undefined","required":false,"description":"Forwarded ref to the rendered element — the component's native root (or the merged child element on a component that opts into {@link SlottableProps}). Generic over `E` (charter `docs/api-conventions.md` §4 rule 1, default `HTMLElement`) so a component with a polymorphic root (Link's navigation / action mode union, Text's `as`) can instantiate the concrete element per variant instead of leaving every consumer with a widened `Ref<HTMLElement>`. A component that does not narrow simply inherits the `HTMLElement` default — source-compatible with every pre-existing non-generic `extends ControlProps`.","declaredIn":"packages/@liquidify/react/src/liquid-component-props.ts"},{"name":"size","type":"PageControlSize | undefined","required":false,"description":"Size of the dot row — the `regular · large` subset of the canonical {@link SizedControlProps.sizesize scale} (ADR-0018). Resolved to geometry via `data-size` attribute selectors in `styles.css`; the root stamps `data-size` **unconditionally** (the Button/Toggle pattern), so both values are always addressable in CSS and tests.","defaultValue":"`\"regular\"`","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"},{"name":"value","type":"number | undefined","required":false,"description":"Controlled current page index (0-based).","declaredIn":"packages/@liquidify/react/src/components/page-control/page-control.tsx"}],"swiftUIMap":[{"swiftUI":"`UIPageControl.numberOfPages`","react":"`count: number` (required)","notes":"Total dots; drives the row length.","relationship":"direct"},{"swiftUI":"`UIPageControl.currentPage`","react":"`value` / `defaultValue` (0-based)","notes":"Controlled triad (ADR-0015/ADR-0017 — the library-wide value-bearing naming convention, chosen over a domain-specific `page`/`defaultPage` pair so `controlled_props` recognises the triad); `value` controlled, `defaultValue` uncontrolled seed (default `0`).","relationship":"direct"},{"swiftUI":"page change (tap / scrub / keyboard)","react":"`onChange?: (index: number) => void`","notes":"Fires the new 0-based index on activation.","relationship":"direct"},{"swiftUI":"`PageTabViewStyle.IndexDisplayMode` `.always`/`.automatic`/`.never`","react":"`indexDisplay?: \"always\" | \"automatic\" | \"never\"`","notes":"Whether the dot row renders; default `\"automatic\"`.","relationship":"direct"},{"swiftUI":"`PageIndexViewStyle.BackgroundDisplayMode` `.always`/`.interactive`/`.automatic`/`.never` (≈ `backgroundStyle` `.prominent`/`.automatic`/`.minimal`)","react":"`backgroundDisplay?: \"always\" | \"interacting\" | \"automatic\" | \"never\"`","notes":"Frosted plate visibility; default `\"automatic\"` (= no plate here). React spells the SwiftUI `.interactive` case `\"interacting\"` — `material_scale.py` bans the former as the removed `GlassMaterial` tier.","relationship":"direct"},{"swiftUI":"`UIPageControl.hidesForSinglePage`","react":"`hidesForSinglePage?: boolean` (default `true`)","notes":"Hide the row when `count === 1`. **Deliberate divergence:** UIKit applies it unconditionally, React folds it into `indexDisplay=\"automatic\"` only — `\"always\"` shows a lone dot, `\"never\"` hides regardless (resolves open question 7).","relationship":"direct"},{"swiftUI":"`UIPageControl.allowsContinuousInteraction`","react":"`allowsContinuousInteraction?: boolean` (default `true`)","notes":"Press-drag scrubs the row dot-to-dot, committing each dot the pointer settles over (resolves open question 1). `false` leaves the discrete dot tap + keyboard, and makes `backgroundDisplay=\"interacting\"` inert (no scrub to reveal the plate for).","relationship":"direct"},{"swiftUI":"`UIPageControl.direction` `.natural`/`.leftToRight`/`.rightToLeft`","react":"(follow CSS `dir`; no prop)","notes":"LTR/RTL from the effective document direction; the pointer→index mapping measures the painted dot rects, so it mirrors for free.","relationship":"direct"},{"swiftUI":"`UIPageControl.direction` `.topToBottom`/`.bottomToTop`","react":"(none)","notes":"Vertical page controls are out of scope for v1; the root is `aria-orientation=\"horizontal\"` (see Out of scope).","relationship":"direct"},{"swiftUI":"`UIPageControl.preferredIndicatorImage` / `setIndicatorImage(_:forPage:)`","react":"(none)","notes":"Per-page symbol indicators are out of scope; every dot is the same neutral circle (see Out of scope).","relationship":"direct"},{"swiftUI":"(no Apple equivalent)","react":"`size?: PageControlSize` (`\"regular\" | \"large\"`, default `\"regular\"`)","notes":"A web-only geometry axis (ADR-0018): `UIPageControl` ships one dot metric, but a full-bleed web pager needs a legible step up. Not a `.controlSize(_:)` mapping — see Sizes.","relationship":"web-only"},{"swiftUI":"`UIPageControl.progress` (`UIPageControlProgress` / `UIPageControlTimerProgress`)","react":"(none)","notes":"The progress/timer page-control styles are out of scope (see Out of scope).","relationship":"direct"},{"swiftUI":"`label: () -> Label` (accessible name)","react":"`aria-label` / `aria-labelledby` (required)","notes":"The tablist renders no visible label of its own.","relationship":"direct"},{"swiftUI":"`isEnabled` (env)","react":"`disabled?: boolean`","notes":"Standard control convention; dims + removes from tab order.","relationship":"direct"},{"swiftUI":"`currentPageIndicatorTintColor` / `pageIndicatorTintColor`","react":"(no prop — fixed neutral tokens)","notes":"Apple keeps the dots neutral glass; no tint axis (ADR-0019).","relationship":"direct"}],"tokens":["--lq-color-fill-tertiary","--lq-color-label-primary","--lq-color-label-tertiary","--lq-duration-","--lq-ease-","--lq-glass-","--lq-hit-target-touch","--lq-page-control-dot","--lq-page-control-dot-active","--lq-page-control-dot-gap","--lq-page-control-pad","--lq-page-control-plate-pad","--lq-radius-pill","--lq-space-"],"keyboard":[{"key":"Keyboard","action":"Keyboard (WAI-ARIA APG Tabs, horizontal, automatic activation):"}],"variants":[],"variantPages":[]}