diff --git a/.changeset/create-resizable-plain-handle.md b/.changeset/create-resizable-plain-handle.md
new file mode 100644
index 0000000000000..d2496785edf24
--- /dev/null
+++ b/.changeset/create-resizable-plain-handle.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] The internal resize factory can drive a plain handle element: it gives the element the separator's role, name, value and tab stop, and answers its pointer drag, keys, Enter and double-click. ResizeHandle and the factory derive the separator's attributes from one helper, with no behavior change.
+
+@vjeux
diff --git a/.changeset/resize-handle-disabled-tab-stop.md b/.changeset/resize-handle-disabled-tab-stop.md
new file mode 100644
index 0000000000000..96752d81bd1b8
--- /dev/null
+++ b/.changeset/resize-handle-disabled-tab-stop.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[fix] A disabled `ResizeHandle` keeps its tab stop. Before, it left the tab order, so keyboard users could not reach it to hear that it is disabled. Now it stays in the tab order, as a disabled control keeps its tab stop, and it still ignores pointer and keyboard input and says so with `aria-disabled`.
+
+@vjeux
diff --git a/docs/architecture/internationalization.md b/docs/architecture/internationalization.md
index 6528a4688b2a4..a1f4c8c692f8d 100644
--- a/docs/architecture/internationalization.md
+++ b/docs/architecture/internationalization.md
@@ -440,7 +440,7 @@ callers remain conformance work and do not relax INV1a, INV8, INV9, or INV10.
| INV1, INV1a, INV7 | hardcoded-string lint, catalog tests, component translation tests with a non-English provider | Astryx-owned text bypasses the catalog, an announcement or count is assembled from fragments or a hand-rolled plural, or non-string output reaches visible/AT attributes |
| INV2, INV13, INV14 | export drift checks, public i18n tests, compatibility review | A parallel/replacement runtime lands without migration, or client hooks claim server behavior |
| INV3–INV5 | resolver and provider rerender tests | no-provider output depends on the host, locale swaps stay stale, fallback order changes, or malformed-locale behavior is described as graceful |
-| INV3a | factory unit tests: a flat option or `labels` key replaces its message, including under a provider, and outside a provider its absence announces or shows shipped English; wrapper tests: under a non-English provider its absence gives the translation. Evidence gap: no factory test covers a flat option or `labels` key yet | a factory announces or shows Astryx text that no option or `labels` key can replace, groups more than one string outside one `labels` object, takes a translator, ignores a given string, or gives something other than the catalog message when it is absent |
+| INV3a | factory unit tests: a flat option or `labels` key replaces its message, including under a provider, and outside a provider its absence announces or shows shipped English; wrapper tests: under a non-English provider its absence gives the translation. Evidence gap: no factory test covers a `labels` key, or a flat option under a provider, yet | a factory announces or shows Astryx text that no option or `labels` key can replace, groups more than one string outside one `labels` object, takes a translator, ignores a given string, or gives something other than the catalog message when it is absent |
| INV6 | `check:i18n-catalog`, key-format lint, and focused mutation tests | stale/malformed keys, malformed ICU, or source/translation runtime-contract drift passes CI |
| INV15 | generator tests, `check:i18n-runtime`, package-export drift check, provider/resolver runtime-catalog tests; per-module English resolution-parity test (every key each Core module names, in every shipped locale’s fallback path, its own map versus the whole catalog); import-cost check (component bundles carry only the English they name; public paths keep the whole catalog) | a generated module carries descriptions or is committed, a malformed entry projects instead of failing, a rich export path changes, or a `RuntimeCatalog` input is rejected or leaks a non-rich context shape, a key a module names resolves differently from the whole catalog, a component bundle carries the whole catalog, or a public path stops resolving a shipped key |
| INV16 | build-plugin unit and real-build tests | a rich or generated locale import fails to resolve through the standard plugin |
diff --git a/docs/specs/components/Resizable/Resizable.spec.md b/docs/specs/components/Resizable/Resizable.spec.md
index dcb33663d87f4..4ef0676b3c069 100644
--- a/docs/specs/components/Resizable/Resizable.spec.md
+++ b/docs/specs/components/Resizable/Resizable.spec.md
@@ -89,9 +89,6 @@ Consumer migration instructions belong in consumer docs and release notes.
does not reflect it, so a theme rule for one direction never matches, where
`architecture:component-theming-surface` INV6 keeps declared state on the
owning target.
-- **Disabled Handle leaves the tab order (FR3, AR1).** A disabled Handle sets
- `tabIndex={-1}`, so keyboard users cannot reach it to hear that it is
- disabled, where `spec:AST-085` FR1 keeps a disabled control's tab stop.
- **Window-splitter gaps (AR1).** The Handle names the pane it resizes with
`aria-controls` only when its caller passes one; an unbounded Handle in a
container that no percentage bound observes carries no `aria-valuemax`; and
@@ -147,7 +144,7 @@ Consumer migration instructions belong in consumer docs and release notes.
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| FR1 | ResizeHandle renders one focusable Handle and one enlarged Grab zone (a `--spacing-4` strip on the resize axis); the default render also contains a Grip pill, and caller `children` replace the pill. The grab zone is centred on the Handle for every `pillPlacement`, and the painted pill is its own grab target. On a pane too short for the full strip, the zone shrinks so the pane keeps at least half of itself. A pill shown only on hover also takes the pointer across the gap between it and the Grab zone, never past the pill's far side. | Current source, docs, tests, and an owner ruling |
| FR2 | The Handle carries `resize-handle`, the Grip pill carries `resize-handle-pill`, and the Grab zone carries no public target. | Current source and docs |
-| FR3 | Pointer drag, keyboard, double-click collapse, snapping, persistence, reversal, and size behavior are owned by ResizeHandle and useResizable; dragging under right-to-left inverts the pointer delta so the divider follows the pointer. A disabled Handle ignores pointer and keyboard input and exposes `aria-disabled`, and keeps its tab stop (`spec:AST-085` FR1). With `autoSaveId`, storage holds the last settled size and receives the final size once when a drag ends, cancels, or unmounts. Implementation gap: a disabled Handle sets `tabIndex={-1}` (Current-state impact). | Current source, docs, and tests |
+| FR3 | Pointer drag, keyboard, double-click collapse, snapping, persistence, reversal, and size behavior are owned by ResizeHandle and useResizable; dragging under right-to-left inverts the pointer delta so the divider follows the pointer. A disabled Handle ignores pointer and keyboard input and exposes `aria-disabled`, and keeps its tab stop (`spec:AST-085` FR1). With `autoSaveId`, storage holds the last settled size and receives the final size once when a drag ends, cancels, or unmounts. | Current source, docs, and tests |
| FR4 | The Handle reflects the documented `direction` visual state on its target (`architecture:component-theming-surface` INV6). Implementation gap: it reflects no `direction` (Current-state impact). | Current source and docs |
| FR5 | A `percent(value, {min: pixel(value)})` or `percent(value, {max: pixel(value)})` resolves against one basis — the supplied container, or the viewport when no `containerRef` is given (`spec:AST-010` FR1, FR2); as a default it selects pixels once, and as a bound it stays live and clamps the selected pixels. A descriptor with both or neither bound is invalid. Selected state, persistence, callbacks, paint, and ARIA are pixels. | `spec:AST-010`; current source and tests |
| FR6 | `minSize` and `maxSize` are the bound props; numeric values are pixels. When resolved bounds conflict, the maximum wins. | `spec:AST-010`; current source and tests |
@@ -175,7 +172,7 @@ Consumer migration instructions belong in consumer docs and release notes.
| Custom handle content | Handle and Grab zone remain; caller content replaces the pill. | Caller content. |
| Horizontal or vertical | Interaction follows the axis; ARIA orientation is the divider's. | Size, reversal, pill placement. |
| Collapsed | `aria-valuenow` at the minimum and "Collapsed" value text. | Pill side. |
-| Disabled | `aria-disabled`; no resize; tab stop kept (FR3 gap). | Disabled paint. |
+| Disabled | `aria-disabled`; no resize; tab stop kept. | Disabled paint. |
### Transformation and precedence order
@@ -258,7 +255,7 @@ Consumer migration instructions belong in consumer docs and release notes.
- `spec:AST-010` owns structured sizing; FR5 and FR6 apply it.
- `spec:AST-020` FR15 adopts the APG Window Splitter for the Handle; AR1
applies it and records its gaps.
-- `spec:AST-085` owns disabled-control behavior; FR3 records the tab-order gap.
+- `spec:AST-085` owns disabled-control behavior.
- `architecture:component-theming-surface` owns target mapping and state
reflection; FR4 records the `direction` gap.
- `architecture:interaction-modality` owns shared modality and focus
@@ -272,7 +269,7 @@ Consumer migration instructions belong in consumer docs and release notes.
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- |
| FR1 | `ResizeHandle.test.tsx`; `grabZone.interaction.browser.spec.ts` for the zone's place and its short-pane limit; stable visual baseline `core-resizable--horizontal-split` | Default, collapsed, both axes | Handle and Grab zone regressions fail focused tests; a changed default handle comes back changed for review. Gap: no test renders `children` to prove they replace the Grip pill. | `audit:Resizable/anatomy` |
| FR2 | `themingTargets.test.ts`; source review | Handle, Grip pill, Grab zone | A removed target fails the inventory. `themingTargets.test.ts` catches only a renamed or undocumented target; no check fails if the Handle or Grip pill stops carrying its target. | `audit:Resizable/theming` |
-| FR3 | `ResizeHandle.test.tsx` and `useResizable.test.ts`; storage timing: `ResizeHandle.test.tsx` ("saves the size once, when a drag ends on …", "saves the dragged size once when the split unmounts mid-drag", "still saves each keyboard step at once") and `useResizable.test.ts` ("writes the size a drag reached once, after … releases it", "writes the final size once when the release comes before the last move renders", "keeps the size a region was dragged to when it unmounts mid-drag", "writes only the re-clamped size when the container shrank mid-drag") | Pointer, keyboard, snap, collapse, persistence; RTL drag; disabled keyboard | A changed transformation, an RTL drag that moves against the pointer, keyboard input that resizes a disabled Handle, or a per-move, stale or missing final storage write fails. Gap: no test covers disabled pointer input or the disabled tab stop. | `audit:Resizable/behavior` |
+| FR3 | `ResizeHandle.test.tsx` and `useResizable.test.ts`; storage timing: `ResizeHandle.test.tsx` ("saves the size once, when a drag ends on …", "saves the dragged size once when the split unmounts mid-drag", "still saves each keyboard step at once") and `useResizable.test.ts` ("writes the size a drag reached once, after … releases it", "writes the final size once when the release comes before the last move renders", "keeps the size a region was dragged to when it unmounts mid-drag", "writes only the re-clamped size when the container shrank mid-drag") | Pointer, keyboard, snap, collapse, persistence; RTL drag; disabled keyboard | A changed transformation, an RTL drag that moves against the pointer, keyboard input that resizes a disabled Handle, or a per-move, stale or missing final storage write fails. Gap: no test covers disabled pointer input. | `audit:Resizable/behavior` |
| FR4 | Source and docs comparison | Horizontal and vertical Handle | Gap: no test detects that the documented `direction` is not reflected. | `audit:Resizable/theming` |
| FR5, ORD1–ORD3 | `useResizable.test.ts`, `utils.test.ts`, typecheck | Floor, ceiling, default; invalid shapes; basis changes; numeric container parity | Wrong resolution, a rescaled default, a stale clamp, an observer leak, or state, paint, storage, and ARIA that disagree fails. | `audit:Resizable/behavior` |
| FR6 | `useResizable.test.ts`; typecheck | Numeric bounds; conflicting bounds | A removed alias that type-checks, a numeric bound that is not pixels, or a minimum that wins a conflict fails. | `audit:Resizable/behavior` |
diff --git a/packages/core/src/Resizable/ResizeHandle.test.tsx b/packages/core/src/Resizable/ResizeHandle.test.tsx
index 3872c76778262..a7aa2d8318ebc 100644
--- a/packages/core/src/Resizable/ResizeHandle.test.tsx
+++ b/packages/core/src/Resizable/ResizeHandle.test.tsx
@@ -2,7 +2,7 @@
/**
* @file ResizeHandle.test.tsx
- * @input Uses vitest, @testing-library/react, react-dom, useResizable, ResizeHandle
+ * @input vitest, testing-library, react-dom, useResizable, ResizeHandle, createResizable
* @output Unit tests for ResizeHandle keyboard operability, ARIA state and hydration
* @position Testing; validates ResizeHandle.tsx implementation
*
@@ -22,6 +22,7 @@ import {hydrateRoot} from 'react-dom/client';
import {renderToString} from 'react-dom/server';
import {ResizeHandle} from './ResizeHandle';
import type {ResizeHandleProps} from './ResizeHandle';
+import {createResizable} from './createResizable';
import {useResizable} from './useResizable';
import type {ResizableProps, UseResizableSingleConfig} from './useResizable';
@@ -895,10 +896,10 @@ describe('ResizeHandle', () => {
// --- Disabled guard ---
- it('ignores keyboard input when disabled', () => {
+ it('ignores keyboard input when disabled, and keeps its tab stop', () => {
render(