diff --git a/desktop-next/tests/layout-refinements.spec.ts b/desktop-next/tests/layout-refinements.spec.ts
new file mode 100644
index 0000000000..7c01ff93f4
--- /dev/null
+++ b/desktop-next/tests/layout-refinements.spec.ts
@@ -0,0 +1,112 @@
+import { expect, test } from "@playwright/test";
+
+test("table cells align to the reading edge and scroll surfaces use thin bars", async ({
+ page,
+}) => {
+ await page.goto("/design/components");
+ const cells = page.locator("#table th, #table td");
+ for (const cell of await cells.all())
+ await expect(cell).toHaveCSS("text-align", "start");
+ for (const selector of [
+ "html",
+ "textarea",
+ "#table .bui-table-scroll",
+ ".catalog-source pre",
+ ]) {
+ await expect(page.locator(selector).first()).toHaveCSS(
+ "scrollbar-width",
+ "thin",
+ );
+ }
+ const viewport = page.locator("#scroll-area .bui-scroll-viewport");
+ // Custom viewports must continue hiding the native bar, avoiding double scrollbars.
+ await expect(viewport).toHaveCSS("scrollbar-width", "none");
+ await expect(page.locator("#scroll-area .bui-scrollbar")).toHaveCSS(
+ "width",
+ "6px",
+ );
+ await viewport.press("End");
+ await expect
+ .poll(() => viewport.evaluate((node) => node.scrollTop))
+ .toBeGreaterThan(0);
+});
+
+test("resize grip is one rem with a usable pointer and keyboard target", async ({
+ page,
+}) => {
+ await page.goto("/design/components");
+ const handle = page.getByRole("separator", { name: "Resize project list" });
+ await handle.scrollIntoViewIfNeeded();
+ const grip = () =>
+ handle.evaluate((node) => getComputedStyle(node, "::after").height);
+ expect(await grip()).toBe("16px");
+ const bounds = await handle.boundingBox();
+ if (!bounds) throw new Error("Missing resize target");
+ expect(bounds.width).toBeGreaterThanOrEqual(24);
+ expect(bounds.height).toBeGreaterThan(16);
+ const before = await handle.getAttribute("aria-valuenow");
+ await page.mouse.move(
+ bounds.x + bounds.width / 2,
+ bounds.y + bounds.height / 2,
+ );
+ await page.mouse.down();
+ await page.mouse.move(
+ bounds.x + bounds.width / 2 + 40,
+ bounds.y + bounds.height / 2,
+ { steps: 5 },
+ );
+ await page.mouse.up();
+ await expect(handle).not.toHaveAttribute("aria-valuenow", before ?? "");
+ const after = await handle.getAttribute("aria-valuenow");
+ await handle.press("ArrowLeft");
+ await expect(handle).not.toHaveAttribute("aria-valuenow", after ?? "");
+ await page.addStyleTag({ content: "html { font-size: 20px; }" });
+ expect(await grip()).toBe("20px");
+});
+
+test("Glass navigation shares selection motion and keyboard semantics", async ({
+ page,
+}) => {
+ await page.goto("/design/glass");
+ const tabs = page.getByRole("tablist", { name: "Glass navigation preview" });
+ const indicator = tabs.locator(".bui-tab-indicator");
+ await expect(
+ tabs.getByRole("tab", { name: "Messages", exact: true }),
+ ).toHaveAttribute("aria-selected", "true");
+ await page.addStyleTag({
+ content: ".bui-tab-indicator { transition-duration: 2s; }",
+ });
+ await tabs.getByRole("tab", { name: "Projects", exact: true }).click();
+ await expect
+ .poll(() =>
+ indicator.evaluate((node) =>
+ node
+ .getAnimations()
+ .map((a) => (a as CSSTransition).transitionProperty),
+ ),
+ )
+ .toContain("clip-path");
+ await tabs.getByRole("tab", { name: "Me", exact: true }).click();
+ await expect(
+ tabs.getByRole("tab", { name: "Me", exact: true }),
+ ).toHaveAttribute("aria-selected", "true");
+ await tabs.getByRole("tab", { name: "Me", exact: true }).press("ArrowRight");
+ await expect(
+ tabs.getByRole("tab", { name: "Messages", exact: true }),
+ ).toBeFocused();
+ await page.keyboard.press("Enter");
+ await expect(
+ tabs.getByRole("tab", { name: "Messages", exact: true }),
+ ).toHaveAttribute("aria-selected", "true");
+ expect(await indicator.evaluate((node) => node.getAnimations().length)).toBe(
+ 0,
+ );
+ await page.emulateMedia({ reducedMotion: "reduce" });
+ await tabs.getByRole("tab", { name: "Projects", exact: true }).click();
+ await expect(
+ tabs.getByRole("tab", { name: "Projects", exact: true }),
+ ).toHaveAttribute("aria-selected", "true");
+ expect(await indicator.evaluate((node) => node.getAnimations().length)).toBe(
+ 0,
+ );
+});
diff --git a/packages/design-tokens/src/foundations.css b/packages/design-tokens/src/foundations.css
index a238a58687..e696553c30 100644
--- a/packages/design-tokens/src/foundations.css
+++ b/packages/design-tokens/src/foundations.css
@@ -3,6 +3,9 @@
--control-sm: 2rem;
--control-md: 2.5rem;
--control-lg: 3.25rem;
+ --size-scrollbar: 0.375rem;
+ --size-grip-length: 1rem;
+ --size-grip-thickness: 0.25rem;
--space-action-inset: 1.25rem;
--space-tooltip-block: 0.125rem;
--layer-dialog: 80;
diff --git a/packages/design-tokens/src/foundations.ts b/packages/design-tokens/src/foundations.ts
index e7700e93e3..5d9e105456 100644
--- a/packages/design-tokens/src/foundations.ts
+++ b/packages/design-tokens/src/foundations.ts
@@ -7,6 +7,17 @@ export const FOUNDATION_GROUPS = [
["control-sm", "2rem", "Small actions."],
["control-md", "2.5rem", "Standard actions and inputs."],
["control-lg", "3.25rem", "Prominent actions."],
+ [
+ "size-scrollbar",
+ "0.375rem",
+ "Thin custom scrollbars and native scrollbar fallback.",
+ ],
+ [
+ "size-grip-length",
+ "1rem",
+ "Visible resize grip length, independent of its hit area.",
+ ],
+ ["size-grip-thickness", "0.25rem", "Visible resize grip thickness."],
["space-action-inset", "1.25rem", "Standard button horizontal padding."],
["space-tooltip-block", "0.125rem", "Compact tooltip vertical padding."],
["layer-dialog", "80", "Modal surfaces."],
diff --git a/packages/ui/README.md b/packages/ui/README.md
index 0889035f6e..ca6ad03898 100644
--- a/packages/ui/README.md
+++ b/packages/ui/README.md
@@ -53,7 +53,9 @@ refs, controlled state, and events. Styled parts merge both string and state-fun
class names. The underlying libraries own focus management, keyboard navigation,
ARIA semantics, and dismissal. Caller-supplied labels and composition still matter.
-Place `Tabs.Indicator` inside `Tabs.List` for a sliding selection pill. Pointer
+Place `Tabs.Indicator` inside `Tabs.List` for a sliding selection pill.
+Use `Tabs.List variant="glass"` for translucent chrome; it shares the same
+selection behavior and motion as the default solid variant. Pointer
selection retargets the shared 120ms transition; keyboard focus and reduced motion
keep selection immediate. Panels update immediately. Toasts use the shared
180ms entrance and 120ms exit transitions, with no movement under reduced motion.
@@ -72,6 +74,11 @@ or proprietary BlockUI package compatibility is implied.
See `../../desktop-next/OPEN_SOURCE.md` for provenance and `THIRD_PARTY_NOTICES.md`
for upstream notices. New source is covered by the repository's Apache-2.0 license.
+Native scroll surfaces use thin scrollbars; custom ScrollArea bars share the
+`size-scrollbar` token. Resize grips use `size-grip-length` (1rem) for their visible
+length while retaining a larger pointer target. Table cells align to the reading
+edge, including row and column headings.
+
## Generated responses
`GeneratedResponse` accepts unknown input and validates it before rendering. The
diff --git a/packages/ui/src/primitives/tabs.tsx b/packages/ui/src/primitives/tabs.tsx
index 0b773e77e6..3055c3a05a 100644
--- a/packages/ui/src/primitives/tabs.tsx
+++ b/packages/ui/src/primitives/tabs.tsx
@@ -10,11 +10,19 @@ function StyledRoot({
}
function StyledList({
+ variant = "solid",
className,
...props
-}: ComponentProps
) {
+}: ComponentProps & {
+ /** Material of the tab track; behavior and indicator motion remain shared. */
+ variant?: "solid" | "glass";
+}) {
return (
-
+
);
}
diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css
index 0ce4ace87e..01aef3d84a 100644
--- a/packages/ui/src/styles.css
+++ b/packages/ui/src/styles.css
@@ -8,6 +8,7 @@
@import "./styles/overlays.css";
@import "./styles/surfaces.css";
@import "./styles/layout.css";
+@import "./styles/scrollbars.css";
@keyframes bui-spin {
to {
transform: rotate(360deg);
@@ -120,15 +121,31 @@
background: var(--bg-inset);
}
.bui-resize-handle {
- background: var(--border-tertiary);
- border-radius: var(--radius-pill);
- width: 0.375rem;
- margin-inline: var(--space-control-gap);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: var(--space-section-gap);
+ background: transparent;
}
- .bui-resize-handle:hover,
- .bui-resize-handle:focus-visible {
+ .bui-resize-handle::after {
+ content: "";
+ width: var(--size-grip-thickness);
+ height: var(--size-grip-length);
+ border-radius: var(--radius-pill);
background: var(--border-primary);
}
+ .bui-resize-handle[aria-orientation="horizontal"] {
+ width: auto;
+ height: var(--space-section-gap);
+ }
+ .bui-resize-handle[aria-orientation="horizontal"]::after {
+ width: var(--size-grip-length);
+ height: var(--size-grip-thickness);
+ }
+ .bui-resize-handle:hover::after,
+ .bui-resize-handle:focus-visible::after {
+ background: var(--border-interactive);
+ }
.bui-sr-only {
position: absolute;
width: 1px;
diff --git a/packages/ui/src/styles/controls.css b/packages/ui/src/styles/controls.css
index 540b836c1d..01cefa70c9 100644
--- a/packages/ui/src/styles/controls.css
+++ b/packages/ui/src/styles/controls.css
@@ -131,6 +131,9 @@
);
transition: clip-path var(--duration-state) var(--ease-standard);
}
+ .bui-tabs-list[data-variant="glass"] .bui-tab-indicator {
+ background: var(--bg-chrome-selected);
+ }
.bui-tabs-list:has(.bui-tab:focus-visible) .bui-tab-indicator {
transition: none;
}
@@ -166,6 +169,19 @@
.bui-tab-panel {
padding-block: var(--space-control-inset);
}
+ .bui-tabs-list[data-variant="glass"] {
+ background: var(--bg-chrome-glass);
+ backdrop-filter: blur(var(--blur-md));
+ box-shadow:
+ inset 0 1px 0 var(--rim-lit),
+ inset 0 -1px 0 var(--rim-shade);
+ }
+ .bui-tabs-list[data-variant="glass"] .bui-tab {
+ padding-inline: var(--space-action-inset);
+ }
+ .bui-tabs-list[data-variant="glass"] .bui-tab:not([data-active]):hover {
+ background: var(--bg-chrome-glass-hover);
+ }
.bui-accordion {
display: grid;
gap: 0;
diff --git a/packages/ui/src/styles/layout.css b/packages/ui/src/styles/layout.css
index 142441838a..a6e48a8459 100644
--- a/packages/ui/src/styles/layout.css
+++ b/packages/ui/src/styles/layout.css
@@ -45,16 +45,23 @@
}
.bui-scrollbar {
display: flex;
- width: 0.5rem;
+ width: var(--size-scrollbar);
margin: 0.25rem;
border-radius: var(--radius-pill);
- background: var(--bg-inset);
+ background: transparent;
+ }
+ .bui-scrollbar[data-orientation="horizontal"] {
+ width: auto;
+ height: var(--size-scrollbar);
}
.bui-scroll-thumb {
width: 100%;
background: var(--border-primary);
border-radius: inherit;
}
+ .bui-scrollbar[data-orientation="horizontal"] .bui-scroll-thumb {
+ height: 100%;
+ }
.bui-toast-viewport {
position: fixed;
bottom: var(--space-panel-inset);
@@ -133,6 +140,7 @@
}
.bui-table th,
.bui-table td {
+ text-align: start;
padding: var(--space-control-inset);
border-bottom: 1px solid var(--border-tertiary);
white-space: nowrap;
diff --git a/packages/ui/src/styles/scrollbars.css b/packages/ui/src/styles/scrollbars.css
new file mode 100644
index 0000000000..03edc58f94
--- /dev/null
+++ b/packages/ui/src/styles/scrollbars.css
@@ -0,0 +1,26 @@
+/* Apply one thin native treatment to pages, code, tables, inputs, and overlays.
+ * Base UI viewports retain their own native-scrollbar suppression. */
+@layer base {
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: var(--border-primary) transparent;
+ }
+ @supports not (scrollbar-width: thin) {
+ ::-webkit-scrollbar {
+ width: var(--size-scrollbar);
+ height: var(--size-scrollbar);
+ }
+ ::-webkit-scrollbar-thumb {
+ background: var(--border-primary);
+ border-radius: var(--radius-pill);
+ }
+ ::-webkit-scrollbar-track {
+ background: transparent;
+ }
+ }
+ @media (forced-colors: active) {
+ * {
+ scrollbar-color: auto;
+ }
+ }
+}