From bcb105083a870ceef7a33a07b84647bbb778e890 Mon Sep 17 00:00:00 2001
From: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com>
Date: Wed, 12 Nov 2025 11:51:04 -0800
Subject: [PATCH] Fix scrollbars for all macOS settings
---
stylesheets/_modules.scss | 12 +-
stylesheets/components/CallsTab.scss | 5 +
stylesheets/components/Preferences.scss | 14 +-
stylesheets/components/Stories.scss | 7 +-
ts/axo/AxoDialog.dom.stories.tsx | 225 +++++++++++++++++++++++-
ts/axo/AxoDialog.dom.tsx | 78 +++-----
ts/axo/AxoProvider.dom.tsx | 18 +-
ts/axo/AxoScrollArea.dom.stories.tsx | 18 +-
ts/axo/AxoScrollArea.dom.tsx | 40 +++--
ts/axo/_internal/scrollbars.dom.tsx | 188 +++++++++++++-------
10 files changed, 440 insertions(+), 165 deletions(-)
diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index 9ff9e02c53..7b123d6cc4 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -4856,8 +4856,9 @@ button.module-calling-participants-list__contact {
.module-conversation-list {
$normal-row-height: 72px;
- padding-inline-start: 10px;
- padding-inline-end: 1px; /* leaving room for scrollbar */
+ scrollbar-gutter: stable;
+ padding-inline-start: 11px;
+ padding-inline-end: calc(11px - var(--axo-scrollbar-gutter-thin-vertical));
@include mixins.scrollbar-on-hover;
@@ -4867,13 +4868,6 @@ button.module-calling-participants-list__contact {
padding-inline: 0;
}
- // Center chat list icons in narrow mode by reserving scrollbar space, preventing
- // scrollbar from pushing content
- &--width-narrow {
- padding-inline: 10px 1px;
- scrollbar-gutter: stable;
- }
-
&--has-dialog-padding {
padding-block-start: 8px;
}
diff --git a/stylesheets/components/CallsTab.scss b/stylesheets/components/CallsTab.scss
index 0ca91d1742..cb61293760 100644
--- a/stylesheets/components/CallsTab.scss
+++ b/stylesheets/components/CallsTab.scss
@@ -209,6 +209,9 @@
}
.CallsList__List {
+ scrollbar-gutter: stable;
+ padding-inline-start: 11px;
+ padding-inline-end: calc(11px - var(--axo-scrollbar-gutter-thin-vertical));
@include mixins.scrollbar-on-hover;
}
@@ -310,6 +313,8 @@
// Override .ListTile
.ListTile.CallsList__ItemTile {
padding-block: 10px;
+ border: none;
+ border-radius: 12px;
// Override .ListTile__subtitle with correct font size
.ListTile__subtitle {
diff --git a/stylesheets/components/Preferences.scss b/stylesheets/components/Preferences.scss
index caf01ac3a7..8be7e1e65b 100644
--- a/stylesheets/components/Preferences.scss
+++ b/stylesheets/components/Preferences.scss
@@ -54,6 +54,12 @@ $secondary-text-color: light-dark(
&__scroll-area {
overflow-y: scroll;
max-height: 100%;
+
+ scrollbar-gutter: stable;
+ padding-inline-start: 11px;
+ padding-inline-end: calc(11px - var(--axo-scrollbar-gutter-thin-vertical));
+
+ @include mixins.scrollbar-on-hover;
}
&__padding {
@@ -71,10 +77,6 @@ $secondary-text-color: light-dark(
flex-direction: row;
align-items: center;
- width: calc(100% - 11px);
- margin-inline-start: 10px;
- margin-inline-end: 1px;
-
margin-bottom: 4px;
border-radius: 8px;
@@ -212,12 +214,10 @@ $secondary-text-color: light-dark(
@include mixins.font-body-1;
align-items: center;
display: flex;
+ width: 100%;
height: 40px;
- width: calc(100% - 11px);
padding-block: 14px;
padding-inline: 0;
- margin-inline-start: 10px;
- margin-inline-end: 1px;
border-radius: 10px;
margin-bottom: 4px;
}
diff --git a/stylesheets/components/Stories.scss b/stylesheets/components/Stories.scss
index 9829639931..1ed02cf81f 100644
--- a/stylesheets/components/Stories.scss
+++ b/stylesheets/components/Stories.scss
@@ -105,8 +105,11 @@
flex-direction: column;
flex: 1;
overflow-y: auto;
- padding-inline: 16px;
-
+ scrollbar-gutter: stable;
+ padding-inline-start: 11px;
+ padding-inline-end: calc(
+ 11px - var(--axo-scrollbar-gutter-thin-vertical)
+ );
@include mixins.scrollbar-on-hover;
&--empty {
diff --git a/ts/axo/AxoDialog.dom.stories.tsx b/ts/axo/AxoDialog.dom.stories.tsx
index eb0af60b06..35c0ecc1fc 100644
--- a/ts/axo/AxoDialog.dom.stories.tsx
+++ b/ts/axo/AxoDialog.dom.stories.tsx
@@ -1,12 +1,13 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
-import React, { useState } from 'react';
+import React, { useId, useMemo, useState } from 'react';
import type { Meta } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { AxoDialog } from './AxoDialog.dom.js';
import { AxoButton } from './AxoButton.dom.js';
import { tw } from './tw.dom.js';
+import { AxoCheckbox } from './AxoCheckbox.dom.js';
export default {
title: 'Axo/AxoDialog',
@@ -143,3 +144,225 @@ export function FooterContentLongAndTight(): JSX.Element {
);
}
+
+function Spacer(props: { height: 8 | 12 }) {
+ return
;
+}
+
+function TextInputField(props: { placeholder: string }) {
+ const style = useMemo(() => {
+ const bodyPadding = 24;
+ const inputPadding = 16;
+
+ return { marginInline: inputPadding - bodyPadding };
+ }, []);
+
+ return (
+
+
+
+ );
+}
+
+export function ExampleNicknameAndNoteDialog(): JSX.Element {
+ const [open, setOpen] = useState(true);
+ return (
+
+
+
+ Open Dialog
+
+
+
+
+ Nickname
+
+
+
+
+ Nicknames & notes are stored with Signal and end-to-end
+ encrypted. They are only visible to you.
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Save
+
+
+
+
+
+ );
+}
+
+function CheckboxField(props: { label: string }) {
+ const id = useId();
+ const [checked, setChecked] = useState(false);
+
+ return (
+
+ );
+}
+
+export function ExampleMuteNotificationsDialog(): JSX.Element {
+ const [open, setOpen] = useState(true);
+ return (
+
+
+
+ Open Dialog
+
+
+
+
+ Mute notifications
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Save
+
+
+
+
+
+ );
+}
+
+function ExampleItem(props: { label: string; description: string }) {
+ const labelId = useId();
+ const descriptionId = useId();
+
+ return (
+
+
+ {props.label}
+
+
+ {props.description}
+
+
+ );
+}
+
+export function ExampleLanguageDialog(): JSX.Element {
+ const [open, setOpen] = useState(true);
+ return (
+
+
+
+ Open Dialog
+
+
+
+
+ Language
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Set
+
+
+
+
+
+ );
+}
diff --git a/ts/axo/AxoDialog.dom.tsx b/ts/axo/AxoDialog.dom.tsx
index 45f9286647..dd882dc0ff 100644
--- a/ts/axo/AxoDialog.dom.tsx
+++ b/ts/axo/AxoDialog.dom.tsx
@@ -14,36 +14,12 @@ import { AxoBaseDialog } from './_internal/AxoBaseDialog.dom.js';
import { AxoSymbol } from './AxoSymbol.dom.js';
import { tw } from './tw.dom.js';
import { AxoScrollArea } from './AxoScrollArea.dom.js';
-import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
import { AxoButton } from './AxoButton.dom.js';
const Namespace = 'AxoDialog';
const { useContentEscapeBehavior, useContentSize } = AxoBaseDialog;
-// We want to have 25px of padding on either side of header/body/footer, but
-// it's import that we remain aligned with the vertical scrollbar gutters that
-// we need to measure in the browser to know the value of.
-//
-// Chrome currently renders vertical scrollbars as 11px with
-// `scrollbar-width: thin` but that could change someday or based on some OS
-// settings. So we'll target 24px but we'll tolerate different values.
-const SCROLLBAR_WIDTH_EXPECTED = 11; /* (keep in sync with chromium) */
-const SCROLLBAR_WIDTH_ACTUAL = getScrollbarGutters('thin', 'custom').vertical;
-
-const DIALOG_PADDING_TARGET = 20;
-
-const DIALOG_PADDING_BEFORE_SCROLLBAR_WIDTH =
- DIALOG_PADDING_TARGET - SCROLLBAR_WIDTH_EXPECTED;
-
-const DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH =
- SCROLLBAR_WIDTH_ACTUAL + DIALOG_PADDING_BEFORE_SCROLLBAR_WIDTH;
-
-const DIALOG_HEADER_PADDING_BLOCK = 10;
-
-const DIALOG_HEADER_ICON_BUTTON_MARGIN =
- DIALOG_HEADER_PADDING_BLOCK - DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH;
-
export namespace AxoDialog {
/**
* Component:
@@ -126,19 +102,12 @@ export namespace AxoDialog {
}>;
export const Header: FC = memo(props => {
- const style = useMemo(() => {
- return {
- paddingBlock: DIALOG_HEADER_PADDING_BLOCK,
- paddingInline: DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH,
- };
- }, []);
return (
{props.children}
@@ -193,7 +162,7 @@ export namespace AxoDialog {
return (
;
export const Back: FC = memo(props => {
- const style = useMemo((): CSSProperties => {
- return { marginInlineStart: DIALOG_HEADER_ICON_BUTTON_MARGIN };
- }, []);
return (
-
+
;
export const Close: FC = memo(props => {
- const style = useMemo((): CSSProperties => {
- return { marginInlineEnd: DIALOG_HEADER_ICON_BUTTON_MARGIN };
- }, []);
return (
-
+
@@ -254,6 +217,16 @@ export namespace AxoDialog {
Close.displayName = `${Namespace}.Close`;
+ export type ExperimentalSearchProps = Readonly<{
+ children: ReactNode;
+ }>;
+
+ export const ExperimentalSearch: FC
= memo(props => {
+ return {props.children}
;
+ });
+
+ ExperimentalSearch.displayName = `${Namespace}.ExperimentalSearch`;
+
/**
* Component:
* ---------------------------
@@ -271,12 +244,13 @@ export namespace AxoDialog {
const contentSize = useContentSize();
const contentSizeConfig = AxoBaseDialog.ContentSizes[contentSize];
- const style = useMemo((): CSSProperties => {
+ const style = useMemo((): CSSProperties | undefined => {
+ if (padding === 'only-scrollbar-gutter') {
+ return;
+ }
+
return {
- paddingInline:
- padding === 'normal'
- ? DIALOG_PADDING_BEFORE_SCROLLBAR_WIDTH
- : undefined,
+ paddingInline: 'calc(24px - var(--axo-scrollbar-gutter-thin-vertical))',
};
}, [padding]);
@@ -323,17 +297,8 @@ export namespace AxoDialog {
}>;
export const Footer: FC = memo(props => {
- const style = useMemo((): CSSProperties => {
- return {
- paddingInline: DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH,
- };
- }, []);
-
return (
-
+
{props.children}
);
@@ -354,6 +319,7 @@ export namespace AxoDialog {
return (
;
+let runOnceGlobally = false;
+
export const AxoProvider: FC
= memo(props => {
+ useInsertionEffect(() => {
+ if (runOnceGlobally) {
+ return;
+ }
+ runOnceGlobally = true;
+
+ const unsubscribe = createScrollbarGutterCssProperties();
+
+ return () => {
+ unsubscribe();
+ runOnceGlobally = false;
+ };
+ });
return (
{props.children}
);
diff --git a/ts/axo/AxoScrollArea.dom.stories.tsx b/ts/axo/AxoScrollArea.dom.stories.tsx
index 7ce9a6aa8f..f267d7b8b1 100644
--- a/ts/axo/AxoScrollArea.dom.stories.tsx
+++ b/ts/axo/AxoScrollArea.dom.stories.tsx
@@ -1,11 +1,10 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactNode } from 'react';
-import React, { useMemo } from 'react';
+import React from 'react';
import type { Meta } from '@storybook/react';
import { AxoScrollArea } from './AxoScrollArea.dom.js';
import { tw } from './tw.dom.js';
-import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
import { AxoSymbol } from './AxoSymbol.dom.js';
export default {
@@ -39,18 +38,9 @@ function VerticalTemplate(props: {
hints?: boolean;
mask?: boolean;
}) {
- const paddingInline = useMemo(() => {
- return getScrollbarGutters('thin', 'custom').vertical;
- }, []);
-
return (
-
- Header
-
+
Header
-
- Footer
-
+
Footer
);
}
diff --git a/ts/axo/AxoScrollArea.dom.tsx b/ts/axo/AxoScrollArea.dom.tsx
index 4614961cdc..263eeed11e 100644
--- a/ts/axo/AxoScrollArea.dom.tsx
+++ b/ts/axo/AxoScrollArea.dom.tsx
@@ -5,7 +5,6 @@ import type { CSSProperties, FC, ReactNode } from 'react';
import type { TailwindStyles } from './tw.dom.js';
import { tw } from './tw.dom.js';
import { assert } from './_internal/assert.dom.js';
-import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
const Namespace = 'AxoScrollArea';
@@ -182,6 +181,23 @@ export namespace AxoScrollArea {
smooth: tw('scroll-smooth'),
};
+ type GutterCss = { horizontal: string; vertical: string };
+
+ const ScrollbarWidthToGutterCss: Record = {
+ wide: {
+ vertical: 'var(--axo-scrollbar-gutter-auto-vertical)',
+ horizontal: 'var(--axo-scrollbar-gutter-auto-horizontal)',
+ },
+ thin: {
+ vertical: 'var(--axo-scrollbar-gutter-thin-vertical)',
+ horizontal: 'var(--axo-scrollbar-gutter-thin-horizontal)',
+ },
+ none: {
+ vertical: '0px',
+ horizontal: '0px',
+ },
+ };
+
export type ViewportProps = Readonly<{
children: ReactNode;
}>;
@@ -198,15 +214,15 @@ export namespace AxoScrollArea {
// `scrollbar-gutter: stable both-edges` is broken in Chrome
// See: https://issues.chromium.org/issues/40064879)
// Instead we use padding to polyfill the feature
- let paddingTop: number | undefined;
- let paddingInlineStart: number | undefined;
+ let paddingTop: string | undefined;
+ let paddingInlineStart: string | undefined;
if (scrollbarGutter === 'stable-both-edges') {
- const scrollbarGutters = getScrollbarGutters(scrollbarWidth, 'custom');
if (hasVerticalScrollbar) {
- paddingInlineStart = scrollbarGutters.vertical;
+ paddingInlineStart =
+ ScrollbarWidthToGutterCss[scrollbarWidth].vertical;
}
if (hasHorizontalScrollbar) {
- paddingTop = scrollbarGutters.horizontal;
+ paddingTop = ScrollbarWidthToGutterCss[scrollbarWidth].horizontal;
}
}
@@ -346,19 +362,17 @@ export namespace AxoScrollArea {
const { scrollbarWidth } = useAxoScrollAreaConfig();
const style = useMemo((): CSSProperties => {
- const scrollbarGutters = getScrollbarGutters(scrollbarWidth, 'custom');
-
const isVerticalEdge = edge === 'top' || edge === 'bottom';
const isStartEdge = edge === 'top' || edge === 'inline-start';
return {
insetInlineEnd:
edge !== 'inline-start' && orientation === 'both'
- ? scrollbarGutters.horizontal
+ ? ScrollbarWidthToGutterCss[scrollbarWidth].horizontal
: undefined,
bottom:
edge !== 'top' && orientation === 'both'
- ? scrollbarGutters.vertical
+ ? ScrollbarWidthToGutterCss[scrollbarWidth].vertical
: undefined,
animationTimeline: isVerticalEdge
? AXO_SCROLL_AREA_TIMELINE_VERTICAL
@@ -417,16 +431,14 @@ export namespace AxoScrollArea {
const { scrollbarWidth } = useAxoScrollAreaConfig();
const style = useMemo(() => {
- const scrollbarGutters = getScrollbarGutters(scrollbarWidth, 'custom');
-
const hasVerticalScrollbar = orientation !== 'horizontal';
const hasHorizontalScrollbar = orientation !== 'vertical';
const verticalGutter = hasVerticalScrollbar
- ? `${scrollbarGutters.vertical}px`
+ ? ScrollbarWidthToGutterCss[scrollbarWidth].vertical
: '0px';
const horizontalGutter = hasHorizontalScrollbar
- ? `${scrollbarGutters.horizontal}px`
+ ? ScrollbarWidthToGutterCss[scrollbarWidth].horizontal
: '0px';
return {
diff --git a/ts/axo/_internal/scrollbars.dom.tsx b/ts/axo/_internal/scrollbars.dom.tsx
index 898f6a3cfd..9468b5ef97 100644
--- a/ts/axo/_internal/scrollbars.dom.tsx
+++ b/ts/axo/_internal/scrollbars.dom.tsx
@@ -1,84 +1,152 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
-
import { assert } from './assert.dom.js';
-export type ScrollbarWidth = 'wide' | 'thin' | 'none';
-export type ScrollbarColor = 'native' | 'custom';
-
-const ScrollbarWidths: Record = {
- wide: 'auto',
- thin: 'thin',
- none: 'none',
-};
-
-const ScrollbarColors: Record = {
- native: 'auto',
- custom: 'black transparent',
-};
+export type ScrollbarWidth = 'auto' | 'thin' | 'none';
export type ScrollbarGutters = Readonly<{
vertical: number;
horizontal: number;
}>;
-const SCROLLBAR_GUTTERS_CACHE = new Map();
-
function isValidClientSize(value: number): boolean {
return Number.isInteger(value) && value > 0;
}
-export function getScrollbarGutters(
- scrollbarWidth: ScrollbarWidth,
- scrollbarColor: ScrollbarColor
-): ScrollbarGutters {
- const cacheKey = `${scrollbarWidth}, ${scrollbarColor}`;
- const cached = SCROLLBAR_GUTTERS_CACHE.get(cacheKey);
- if (cached != null) {
- return cached;
+type Listener = () => void;
+type Unsubscribe = () => void;
+
+class ScrollbarGuttersObserver {
+ #scroller: HTMLDivElement;
+ #current: ScrollbarGutters;
+ #observer: ResizeObserver;
+ #listeners = new Set();
+
+ constructor(scrollbarWidth: Exclude) {
+ const container = document.createElement('div');
+ container.dataset.scrollbarGuttersObserver = scrollbarWidth;
+
+ // Insert the element into the DOM to get non-zero measurements
+ document.body.append(container);
+
+ const scroller = document.createElement('div');
+ const content = document.createElement('div');
+
+ // Use `all: initial` to avoid other styles affecting the measurement
+ // This resets elements to their initial value (such as `display: inline`)
+ scroller.style.setProperty('all', 'initial');
+ scroller.style.setProperty('position', 'absolute');
+ scroller.style.setProperty('top', '-9999px');
+ scroller.style.setProperty('left', '-9999px');
+ scroller.style.setProperty('display', 'block');
+ scroller.style.setProperty('visibility', 'hidden');
+ scroller.style.setProperty('overflow', 'auto');
+ scroller.style.setProperty('width', '100px');
+ scroller.style.setProperty('height', '100px');
+ scroller.style.setProperty('scrollbar-width', scrollbarWidth);
+ scroller.style.setProperty('scrollbar-color', 'black transparent');
+
+ content.style.setProperty('all', 'initial');
+ content.style.setProperty('display', 'block');
+ content.style.setProperty('width', '101px');
+ content.style.setProperty('height', '101px');
+
+ scroller.append(content);
+ container.append(scroller);
+
+ this.#scroller = scroller;
+ this.#current = this.#compute();
+ this.#observer = new ResizeObserver(() => this.#update());
+ this.#observer.observe(this.#scroller, { box: 'content-box' });
}
- const outer = document.createElement('div');
- const inner = document.createElement('div');
+ #compute(): ScrollbarGutters {
+ const { offsetWidth, offsetHeight, clientWidth, clientHeight } =
+ this.#scroller;
- // Use `all: initial` to avoid other styles affecting the measurement
- // This resets elements to their initial value (such as `display: inline`)
- outer.style.setProperty('all', 'initial');
- outer.style.setProperty('display', 'block');
- outer.style.setProperty('visibility', 'hidden');
- outer.style.setProperty('overflow', 'auto');
- outer.style.setProperty('width', '100px');
- outer.style.setProperty('height', '100px');
- outer.style.setProperty('scrollbar-width', ScrollbarWidths[scrollbarWidth]);
- outer.style.setProperty('scrollbar-color', ScrollbarColors[scrollbarColor]);
+ assert(offsetWidth === 100, 'offsetWidth must be exactly 100px');
+ assert(offsetHeight === 100, 'offsetHeight must be exactly 100px');
+ assert(
+ isValidClientSize(clientWidth),
+ 'clientWidth must be non-zero positive integer'
+ );
+ assert(
+ isValidClientSize(clientHeight),
+ 'clientHeight must be non-zero positive integer'
+ );
- inner.style.setProperty('all', 'initial');
- inner.style.setProperty('display', 'block');
- inner.style.setProperty('width', '101px');
- inner.style.setProperty('height', '101px');
+ const vertical = offsetWidth - clientWidth;
+ const horizontal = offsetHeight - clientHeight;
- outer.append(inner);
+ return { vertical, horizontal };
+ }
- // Insert the element into the DOM to get non-zero measurements
- document.body.append(outer);
- const { offsetWidth, offsetHeight, clientWidth, clientHeight } = outer;
- outer.remove();
+ #update() {
+ const next = this.#compute();
- assert(offsetWidth === 100, 'offsetWidth must be exactly 100px');
- assert(offsetHeight === 100, 'offsetHeight must be exactly 100px');
- assert(
- isValidClientSize(clientWidth),
- 'clientWidth must be non-zero positive integer'
- );
- assert(
- isValidClientSize(clientHeight),
- 'clientHeight must be non-zero positive integer'
- );
+ if (
+ next.vertical === this.#current.vertical &&
+ next.horizontal === this.#current.horizontal
+ ) {
+ return;
+ }
- const vertical = offsetWidth - clientWidth;
- const horizontal = offsetHeight - clientHeight;
+ this.#current = next;
- const result: ScrollbarGutters = { vertical, horizontal };
- SCROLLBAR_GUTTERS_CACHE.set(cacheKey, result);
- return result;
+ this.#listeners.forEach(listener => {
+ listener();
+ });
+ }
+
+ current(): ScrollbarGutters {
+ return this.#current;
+ }
+
+ subscribe(listener: Listener): Unsubscribe {
+ this.#listeners.add(listener);
+ return () => {
+ this.#listeners.delete(listener);
+ };
+ }
+}
+
+function applyGlobalProperties(
+ observer: ScrollbarGuttersObserver,
+ verticalProperty: `--${string}`,
+ horizontalProperty: `--${string}`
+): Unsubscribe {
+ const root = document.documentElement;
+
+ function update() {
+ const value = observer.current();
+ root.style.setProperty(verticalProperty, `${value.vertical}px`);
+ root.style.setProperty(horizontalProperty, `${value.horizontal}px`);
+ }
+
+ update();
+ const unsubscribe = observer.subscribe(update);
+ return () => {
+ unsubscribe();
+ root.style.removeProperty(verticalProperty);
+ root.style.removeProperty(horizontalProperty);
+ };
+}
+
+export function createScrollbarGutterCssProperties(): Unsubscribe {
+ const autoUnsubscribe = applyGlobalProperties(
+ new ScrollbarGuttersObserver('auto'),
+ '--axo-scrollbar-gutter-auto-vertical',
+ '--axo-scrollbar-gutter-auto-horizontal'
+ );
+
+ const thinUnsubscribe = applyGlobalProperties(
+ new ScrollbarGuttersObserver('thin'),
+ '--axo-scrollbar-gutter-thin-vertical',
+ '--axo-scrollbar-gutter-thin-horizontal'
+ );
+
+ return () => {
+ autoUnsubscribe();
+ thinUnsubscribe();
+ };
}