registry:ui · scroll-area family
native-scroll-area — the platform path, packaged best practices
The 2026-09-15 family rework (Owner ruling): the styled component hand-draws always; this sibling ships the platform scrollbar under the site's scrollbar-token law — zero drawn chrome, zero custom scrollbar ARIA (the platform bar IS the accessibility contract) — with the native best practices packaged as capability styles from the shared scroll-area-kit kernel.
$npx jixoai-ui add native-scroll-area
https://ui.jixoai.com/r/native-scroll-area.json
overview
Overview
The platform path, packaged: zero drawn chrome, the native best practices shipped as capability styles, and the platform scrollbar itself as the accessibility contract.
NativeScrollArea renders the scroll-area family's PLATFORM sibling: an outer region
root (where the §11 carriers stamp) around a scrollport whose scrollable anatomy is
the platform's own — overflow mapped from the orientation prop, scrollbar-gutter: stable both-edges so
overflow arriving never shifts layout, scrollbar-width tiers
(auto/thin/none — thin is the theme's global law), and overscroll-behavior: contain so a boxed region's momentum stops at its
edge. The bar's paint is the theme's global scrollbar-token law (the currentColor
chain); this component never touches scrollbar-color.
Two state machines are the component's own. The stage-scope scheme: a
MutationObserver resolves the nearest [data-theme]/.dark/ .jx-light scope on the ancestor chain (self included) into data-scheme on the viewport — color-scheme follows the
STAGE, not just the OS, and re-resolves live on scope flips without a re-mount; the
theme lane's own .dark stamp is itself a scope the observer sees. The
WAI scrollable-region pattern: role="region" + name + tabindex="0" makes the scrollport a keyboard scroll surface — and NO custom scrollbar ARIA mounts
anywhere (a scrollbar role on a nonexistent thumb would be a violation, not a
feature — the Gate-1 r1 ruling, probe-asserted absent).
The eight axes resolve on the outer root all no-own (the native-wrapper batch A rule: the family owns only the axis names it destructures; the consumer style stays the viewport's own channel, the scrollbar tiers ride their own data-width hook, and no native size-like attribute collides). Size reaches the content through inheritance; the rest stamp-and-supply. The composed story is kit-shaped: the scroll-area-kit core is shared with the hand-drawn sibling — the siblings share the core, never each other's halves. Per-axis below; the shared grammar lives on the universal props page.
native-scroll-area
the platform scrollbar, the theme's currentColor token law, and the packaged capability styles — flip the stage theme in the dock head and watch the bar follow the stage scope, not just the OS.
<NativeScrollArea
label="config demo"
orientation="vertical"
scrollbarWidth="thin"
class="h-40"
>
…scrolling content…
</NativeScrollArea>capabilities
The packaged best practices
Four capabilities ship as the kit's native-capability.css — consumed by this sibling only.
native-scroll-area · capability law
overflow arriving never shifts layout — the gutter is reserved on vertical-capable axes (a horizontal-only port never grows a block-axis bar; stable would reserve dead space forever, the scrollbar law's carve-out).
the bar follows the STAGE scope, not just the OS: the nearest [data-theme]/.dark/.jx-light resolves live (a class+attribute observer on the ancestor chain) — flip the stage theme
in this canvas's dock head and the bar re-schemes without a re-mount.
auto | thin | none — thin is the theme's global law
(the default, no channel at all); the tiers are the consumer's dial.
a boxed region's momentum stops at its edge — overscroll-behavior: contain, the family's standing
posture; the scroll chaining stays inside by explicit choice.
@layer properties, theme, base, components;
/* jixoai scroll-area-kit — THE PLATFORM CAPABILITY STYLES
* (registry/files/lib/scroll-area-kit/native-capability.css, Owner
* 2026-09-15). PART (c) of the kit's concern split — consumed ONLY by
* the platform sibling (native-scroll-area). NEVER by the hand-drawn
* component (it hides the platform bar and draws its own capsule).
*
* The PACKAGED best practices — the platform path's cross-consumer
* law, keyed on the kit's own hook (.jx-native-scroll) and the
* sibling's data channels:
* - scrollbar-gutter: stable both-edges on vertical-capable axes —
* overflow arriving never shifts layout (a horizontal-only port
* can never grow a block-axis bar; stable would reserve dead
* space forever — the scrollbar law's carve-out);
* - theme-scope-aligned color-scheme — the bar follows the STAGE
* scope (data-scheme, resolved from the nearest theme scope by
* the kit core's resolveThemeScope, live), not just the OS;
* - scrollbar-width tiers auto | thin | none (data-width) — the
* theme's global thin law stays the default, the tiers are the
* consumer's dial;
* - overscroll-behavior containment — a boxed region's momentum
* stops at its edge (the scroll chaining stays INSIDE by
* explicit choice, the standing family posture).
*
* ZERO drawn chrome: no thumb, no track, no pseudo-element paint —
* the platform scrollbar under the theme's scrollbar-token law (the
* global * rule in jixoai-theme paints it currentColor) IS the look
* and IS the accessibility contract. */
/* the packaged gutter law — vertical-capable axes only */
:where(.jx-native-scroll[data-orientation='vertical']),
:where(.jx-native-scroll[data-orientation='both']) {
scrollbar-gutter: stable both-edges;
}
/* the width tiers (default: the theme's inherited thin — no data
* attribute, no override, the global law stands) */
:where(.jx-native-scroll[data-width='auto']) {
scrollbar-width: auto;
}
:where(.jx-native-scroll[data-width='thin']) {
scrollbar-width: thin;
}
:where(.jx-native-scroll[data-width='none']) {
scrollbar-width: none;
}
/* the scheme follows the stage scope, not just the OS — the W1
* lesson applied to the platform bar (the sibling's runtime resolves
* the nearest scope and re-resolves on live scope flips) */
:where(.jx-native-scroll[data-scheme='light']) {
color-scheme: light;
}
:where(.jx-native-scroll[data-scheme='dark']) {
color-scheme: dark;
}
/* overscroll containment — the family's standing posture */
:where(.jx-native-scroll) {
overscroll-behavior: contain;
}
@media (prefers-reduced-motion: reduce) {
/* nothing animated lives here — the platform bar's own hit fades
* are the OS's; the sheet merely keeps its guarantees static */
:where(.jx-native-scroll) {
scroll-behavior: auto;
}
}
usage
Usage
Give it a height and a label; the platform does the rest under the token law.
<script lang="ts">
import NativeScrollArea from '@ui/native-scroll-area.svelte';
</script>
<!-- the platform scrollbar under the theme's scrollbar-token law -->
<NativeScrollArea class="h-72" label="release notes">
{#each notes as note (note.id)}
<article>…</article>
{/each}
</NativeScrollArea><!-- the width tiers: thin (default, the theme law) | auto | none -->
<NativeScrollArea scrollbarWidth="none" class="h-56" label="clip lane">
<!-- still fully scrollable — keyboard, wheel, touch — just no visible bar -->
</NativeScrollArea>import { resolveThemeScope } from '@lib/scroll-area-kit/core';
import '$lib/scroll-area-kit/native-capability.css';
// the capability styles are a kit part consumed ONLY by this sibling;
// the styled component (scroll-area) consumes the hand-drawn adapter
// instead — the siblings share the core, never each other's halves.a11y
Accessibility
The platform scrollbar IS the accessibility contract — no custom scrollbar ARIA mounts anywhere inside (probe-asserted absent).
Keyboard
| Key | Action |
|---|---|
| Tab | Moves focus onto the scrollable region |
| ↑ ↓ ← → / Home / End / PgUp / PgDn | Native scrollport scrolling once the region is focused |
| platform bar | The OS scrollbar remains directly operable — dragging, clicking, its own keyboard path |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-label | label prop | Accessible name for the region (default "scrollable content") |
| role | region | Plus tabindex=0 — the WAI scrollable-region pattern; the ONLY a11y surface here |
| role="scrollbar" | ABSENT | No drawn thumb exists — a scrollbar role on a nonexistent thumb would be a violation, not a feature (the Gate-1 r1 ruling; probe-asserted) |
axes
The eight axes on native-scroll-area
The native-wrapper batch A rule realized: the family owns only the axis names it destructures on the OUTER root, the carriers stamp there (never the scrollport — the consumer style stays the viewport's own channel), and all eight lanes read zero family css (grep receipt across ui/native-scroll-area/ and the kit's capability sheet). The exceptions are structural, not painted: size reaches the content through inheritance, and theme is CONSUMED BY THE OBSERVER — the lane's own .dark stamp is a stage scope the family's scheme resolver sees, so the platform bar re-schemes live. Census: the D5 hole round (the sweep's 13 unlisted families).
Reading the table: Property is the axis, Type is the real carrier or consumption it drives on THIS family, Default is the lane default — the named steps, number unit, and consumption are in each description.
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | CONSUMED through inheritance, scoped by the declaration law — the §11 stamp sets the OUTER root's font-size (measured 16 → 14px) and unstyled flow inherits it; content that declares its OWN type (the demo rows' 12px voice) wins over the stamp. The scrollport's chrome is the platform bar, sized by the UA. Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' | SUPPLY-ONLY — stamps --jx-shape-effective + --jx-radius-factor-effective; no family css reads them (grep receipt: zero carrier reads in ui/native-scroll-area/ and the capability sheet). The scrollport is square by the kit. Number unit: none. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-radius-effective; no family css reads it (grep receipt: zero readers). The region root is the §3 concentric ANCHOR for composed children — the supply is the point, the anchor is in reach. Number unit: px. |
| density | 'small' | 'medium' | 'large' | 'auto' | number (+ the five legacy spellings) | 'auto' | SUPPLY-ONLY — the named rung stamps data-density on the outer root; the family reads none of the re-based channels (grep receipt: the capability sheet and the atoms read no density channels — the region's rhythm is the platform's). The stamp supplies composed content. Number unit: coefficient. |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' | SUPPLY-ONLY — stamps --jx-color-effective; no family css reads it (grep receipt: zero readers). The bar's paint is the theme's global scrollbar-token law (the currentColor chain) — this component never touches scrollbar-color. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' | CONSUMED BY THE OBSERVER — the axis story this family makes strange. The lane's stamp (.dark on the outer root) IS a stage scope: the family's own scheme observer resolves the nearest [data-theme]/.dark/.jx-light scope (self included) into data-scheme on the viewport, and color-scheme follows — the PLATFORM BAR re-schemes live, no re-mount (measured). The family's one raw css read — the focus ring (--ring) — flips under any dark scope on the chain. Everything else is theme-free by contract: the atoms carry no theme slots at all. light and system stamp nothing — the observer falls through to the OS scheme (the honest fallback). |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-elevation-effective; no family css reads it (grep receipt: zero shadow declarations — zero drawn chrome by contract). Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-motion-effective; no family css reads it. The kit's reduced-motion block forces scroll-behavior: auto (the platform's smooth scrolling and bar fades are the OS's own); scrollTo() is a passthrough. Number unit: coefficient. |
Deviations, cited: the adoption is the census D5 hole round (native-scroll-area was unlisted until the sweep — openspec/changes/explicit-props/research/ migration-census.md). The §1 collision rule, native-wrapper form: no native size-like attribute exists on a region — the scrollbar tiers ride their own data-width hook, the consumer style stays the viewport's channel, and every axis name is the family's own destructured lane.
<!-- size: CONSUMED through inheritance — the outer root's
font-size is the stamp and the scrolling content inherits it -->
<NativeScrollArea label="axes" size={18}>…content…</NativeScrollArea>
<!-- theme: the scheme observer treats the lane's own .dark stamp as a
stage scope — the viewport resolves data-scheme and the PLATFORM
BAR re-schemes live, no re-mount -->
<NativeScrollArea label="dark island" theme="dark">…content…</NativeScrollArea>native-scroll-area · the inherited stamp and the scheme observer
The size pair is the consumed lane, measured: the stamped region's root font-size moves 16 → 14px exactly — and the demo rows' own 12px type voice WINS over the stamp, the declaration law live: inheritance reaches only unstyled flow. The theme pair is the observer story, measured: the dark region's viewport resolves data-scheme="dark" (color-scheme: dark — the platform bar re-schemes in place) because the lane's own stamp is a scope the ancestor-chain observer reads; the light panels resolve the page's ambient light scope. No scrollbar ARIA mounts anywhere in the page's raw DOM — the Gate-1 r1 ruling holds.
<!-- size: CONSUMED through inheritance — the outer root's
font-size is the stamp and the scrolling content inherits it -->
<NativeScrollArea label="axes" size={18}>…content…</NativeScrollArea>
<!-- theme: the scheme observer treats the lane's own .dark stamp as a
stage scope — the viewport resolves data-scheme and the PLATFORM
BAR re-schemes live, no re-mount -->
<NativeScrollArea label="dark island" theme="dark">…content…</NativeScrollArea><script lang="ts">
import NativeScrollArea from '@ui/native-scroll-area.svelte';
import { query } from '@lib/universal-props-query.svelte';
</script>
<!-- below 48rem the base (14px) applies; at 48rem+ the md case (18px)
wins — the scrolling content inherits the stamp -->
<NativeScrollArea label="responsive region" class="h-40" size={query({ md: 18 }, 14)}>
…scrolling content…
</NativeScrollArea>native-scroll-area · query()
Media keys are min-width: below 48rem the base applies — 14px; at 48rem and wider the md case wins — 18px, the content inheriting the stamp. The number lane goes bare (results infer); string lanes take both generics. Resize across 48rem.
<script lang="ts">
import NativeScrollArea from '@ui/native-scroll-area.svelte';
import { query } from '@lib/universal-props-query.svelte';
</script>
<!-- below 48rem the base (14px) applies; at 48rem+ the md case (18px)
wins — the scrolling content inherits the stamp -->
<NativeScrollArea label="responsive region" class="h-40" size={query({ md: 18 }, 14)}>
…scrolling content…
</NativeScrollArea>| Token | Default | Source |
|---|---|---|
| scrollbar-gutter | stable both-edges | component |
| color-scheme | stage-scoped (data-scheme) | component |
| scrollbar-width | auto | thin | none (data-width) | component |
| overscroll-behavior | contain | component |
| --scrollbar-thumb / -hover / -active | currentColor steps | component |
| --ring | raw theme token (the one read) | structural |
api
API
Props from the NativeScrollArea Props interface (克制原则 — the API stays small); getViewport()/scrollTo() are the imperative exports. Every other attribute rides restProps through to the scrollport.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| orientation | 'vertical' | 'horizontal' | 'both' | 'vertical' | Which axes scroll: overflow-y/x mapping (vertical hides overflow-x, horizontal hides overflow-y, both shows both bars). |
| scrollbarWidth | 'auto' | 'thin' | 'none' | 'thin' | The scrollbar-width tier — thin rides the theme's global law (the default ships no data-width attribute at all); auto restores the platform-wide bar; none hides it (content still scrolls). |
| label | string | 'scrollable content' | The a11y name for the scrollable region (role=region + aria-label + tabindex — the WAI scrollable-region pattern). |
| class | string | '' | Class passthrough (the height utility lands here — give the region a block size). |
| style | string | — | Style passthrough — the VIEWPORT's own channel (the §11 carriers stamp the outer root, never the scrollport). |
| onscroll | (event: ViewportScrollEvent) => void | — | Scroll callback from the scrollport. |
| children * | Snippet | — | The scrolling content — stretched to fill the scrollport. |
| getViewport() | () => HTMLDivElement | null | export | The scrollport element — the family's ToC pairing works here too. |
| scrollTo() | (options?: ScrollToOptions) => void | export | Thin passthrough to the native scrollport scrollTo. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |