jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

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.

install — native-scroll-area

$npx jixoai-ui add native-scroll-area

https://ui.jixoai.com/r/native-scroll-area.json

zero drawn chrome stable gutter stage-scoped color-scheme scrollbar-width tiers overscroll containment

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.

  1. item 1 — the platform bar, themed by tokens
  2. item 2 — the platform bar, themed by tokens
  3. item 3 — the platform bar, themed by tokens
  4. item 4 — the platform bar, themed by tokens
  5. item 5 — the platform bar, themed by tokens
  6. item 6 — the platform bar, themed by tokens
  7. item 7 — the platform bar, themed by tokens
  8. item 8 — the platform bar, themed by tokens
  9. item 9 — the platform bar, themed by tokens
  10. item 10 — the platform bar, themed by tokens
  11. item 11 — the platform bar, themed by tokens
  12. item 12 — the platform bar, themed by tokens
native-scroll-area-usage.svelte svelte
<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

scrollbar-gutter: stable

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).

  1. row 1
  2. row 2
  3. row 3
  4. row 4
  5. row 5
  6. row 6
  7. row 7
  8. row 8
  9. row 9
  10. row 10
scoped color-scheme

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.

  1. row 1
  2. row 2
  3. row 3
  4. row 4
  5. row 5
  6. row 6
  7. row 7
  8. row 8
  9. row 9
  10. row 10
scrollbar-width tiers

auto | thin | none — thin is the theme's global law (the default, no channel at all); the tiers are the consumer's dial.

row 1

row 2

row 3

row 4

row 5

row 6

row 1

row 2

row 3

row 4

row 5

row 6

row 1

row 2

row 3

row 4

row 5

row 6

overscroll containment

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.

row 1

row 2

row 3

row 4

row 5

row 6

native-capability.css css
@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.

basic
<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>
width tiers
<!-- 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>
kit boundary
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

KeyAction
TabMoves focus onto the scrollable region
↑ ↓ ← → / Home / End / PgUp / PgDnNative scrollport scrolling once the region is focused
platform barThe OS scrollbar remains directly operable — dragging, clicking, its own keyboard path

ARIA

AttributeValueDescription
aria-labellabel propAccessible name for the region (default "scrollable content")
roleregionPlus tabindex=0 — the WAI scrollable-region pattern; the ONLY a11y surface here
role="scrollbar"ABSENTNo 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.

PropertyTypeDefaultDescription
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.

the eight axes on native-scroll-area
<!-- 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

size auto — the ambient scale

row 1

row 2

row 3

row 4

row 5

row 6

row 7

row 8

size={14} — the root moves 16 → 14px; declared voices win (measured)

row 1

row 2

row 3

row 4

row 5

row 6

row 7

row 8

light — the ambient scheme

row 1

row 2

row 3

row 4

row 5

row 6

row 7

row 8

theme="dark" — the stamp IS a scope: data-scheme flips, the bar re-schemes (measured)

row 1

row 2

row 3

row 4

row 5

row 6

row 7

row 8

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.

native-scroll-area-axes.svelte svelte
<!-- 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>
one real query() case
<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()

row 1

row 2

row 3

row 4

row 5

row 6

row 7

row 8

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.

native-scroll-area-query-demo.svelte svelte
<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>
TokenDefaultSource
scrollbar-gutterstable both-edgescomponent
color-schemestage-scoped (data-scheme)component
scrollbar-widthauto | thin | none (data-width)component
overscroll-behaviorcontaincomponent
--scrollbar-thumb / -hover / -activecurrentColor stepscomponent
--ringraw 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

PropertyTypeDefaultDescription
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 | nullexportThe scrollport element — the family's ToC pairing works here too.
scrollTo() (options?: ScrollToOptions) => voidexportThin passthrough to the native scrollport scrollTo.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h