@samebase/sidebars is the unstyled React sidebar layout that samebase.com
uses for its workspace layout. It supports optional left and right panes and owns pane geometry,
mobile scroll snapping, resizing, merge state, accessibility, SSR markup, and prehydration. The
consumer owns visible styling, text, and durable state storage.
This repository is an automated export of
packages/sidebarsfrom the Samebase monorepo. The monorepo is the source of truth. Do not edit exported files directly. Use GitHub Issues to report a problem or request a change.
The public API follows Base UI conventions. Components render canonical DOM with stable part and state attributes. The package does not depend on Base UI.
pnpm add @samebase/sidebarsimport { SidebarLayout } from "@samebase/sidebars/SidebarLayout";
import { PaneFrame } from "@samebase/sidebars/PaneFrame";
import {
SidebarRuntimeProvider,
useSidebarLayoutPresentation,
useSidebarActions,
} from "@samebase/sidebars/SidebarRuntime";
function MobilePaneEscape() {
const { isMobile, mobilePane } = useSidebarLayoutPresentation();
const { setMobilePane } = useSidebarActions();
if (!isMobile || mobilePane === "main") return null;
return (
<button type="button" onClick={() => setMobilePane("main")}>
Show main content
</button>
);
}
<SidebarRuntimeProvider controller={paneController}>
<SidebarLayout
addressChrome={
<>
<AddressBar />
<MobilePaneEscape />
</>
}
left={<PaneFrame content={<FileTree />} />}
main={<PaneFrame content={<Editor />} />}
resizeHandleLabels={{
left: "Resize navigation pane",
right: "Resize details pane",
}}
right={<PaneFrame content={<Inspector />} />}
formatResizeHandleValueText={({ widthPx }) => `${widthPx} pixels wide`}
/>
</SidebarRuntimeProvider>;Both resize labels are required because the resize separators have no visible text. The value-text
formatter is optional. Without it, the package omits aria-valuetext and keeps the numeric ARIA
value.
Keep a visible action that returns to the main pane on mobile. Put it in persistent chrome or in each side pane so it stays reachable when a side pane covers the main pane.
useSidebarActions exposes only setMobilePane, toggleLeftPane, and toggleRightPane.
useSidebarLayoutPresentation provides read-only presentation state: isMobile, mobilePane,
leftDesktopOpen, rightDesktopOpen, mobilePaneScrollProgress, and mobileMergeProgress. Merge
progress is available during a resize and after a merge settles. The desktop open fields let
consumer-owned toggle buttons expose their current state.
SidebarLayout imports its required structural CSS. Build tools can also resolve the same file
through @samebase/sidebars/structure.css.
Do not replace the structural rules. Runtime geometry and prehydration depend on the canonical pane order, overflow, scroll snap, separator position, and resize hit areas.
PaneFrame makes its header and content scrollport anonymous inline-size query containers. Make
content inside these regions respond to the pane width. Opening or resizing a sidebar can change the
pane width without changing the window width.
Use native container queries and container-relative width units:
.product-grid {
display: grid;
gap: 1rem;
}
.product-title {
font-size: clamp(1.5rem, 5cqw, 3rem);
}
@container (min-width: 42rem) {
.product-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}If the consumer uses Tailwind CSS, its container variants read the same pane container:
<div className="grid gap-4 @2xl:grid-cols-2">...</div>Do not use viewport variants such as sm: or md:, or viewport width units such as vw, for a
pane-content layout decision. Keep viewport rules for behavior that depends on the browser viewport.
For example, useSidebarLayoutPresentation().isMobile reports the mobile or desktop shell mode. It
does not report the available width of one pane.
If a component needs an internal query container, name that container and target it by name. An
anonymous nested container becomes the nearest container for unnamed descendant queries. If
JavaScript must match a pane-width rule, observe a pane-filling element with ResizeObserver. Do
not use a window media query for that decision.
This query-container contract covers the PaneFrame header and content. It does not cover
addressChrome or the PaneFrame footer.
The structural stylesheet contains no consumer theme colors, shadows, radii, typography, or
resize-grip artwork. It uses the system Highlight color only as a keyboard-focus fallback. Style
the package with its parts, states, and focus-outline variable.
[data-sidebar-layout-part="root"],
[data-sidebar-layout-part="pane"],
[data-sidebar-layout-part="pane-surface"],
[data-sidebar-layout-part="pane-frame"] {
background: var(--app-background);
}
[data-sidebar-layout-part="root"] {
--sidebar-layout-focus-outline: 1px solid var(--app-focus-ring);
}
[data-sidebar-layout-part="pane"][data-pane-side="left"],
[data-sidebar-layout-part="pane"][data-pane-side="right"] {
border-color: var(--app-border);
}
[data-sidebar-layout-part="pane"]::before,
[data-sidebar-layout-part="pane"]::after {
background: var(--app-border);
}
[data-sidebar-layout-part="resize-grip-indicator"] {
color: color-mix(in srgb, var(--app-foreground) 70%, transparent);
}
[data-sidebar-layout-part="resize-grip-indicator"]::before {
width: 3px;
height: 3px;
border-radius: 9999px;
background: currentColor;
box-shadow:
0 -7px 0 currentColor,
0 7px 0 currentColor;
content: "";
}
[data-sidebar-layout-part="resize-handle"][data-resizing]
[data-sidebar-layout-part="resize-grip-indicator"] {
border: 1px solid var(--app-border);
background: var(--app-background);
}Stable parts are root, address-chrome, viewport, carousel, pane, pane-surface,
resize-track, resize-handle, resize-grip-indicator, pane-frame, pane-header,
pane-scrollport, pane-content, and pane-footer.
Stable state attributes are data-mobile-pane, data-mobile-min-resize-behavior,
data-sidebar-layout-mobile-merged-side, data-pane-side, data-desktop-open,
data-active-resize, data-transition-enabled, data-resize-side, data-resize-mode,
data-resizing, data-viewport-measured, and data-carousel-scroll-locked.
The package uses 2px solid Highlight as the default resize-handle focus outline. Consumers can
theme it with --sidebar-layout-focus-outline on the layout root.
The runtime writes these read-only geometry variables:
--sidebar-layout-left-mobile-width--sidebar-layout-main-mobile-width--sidebar-layout-right-mobile-width--sidebar-layout-left-desktop-width--sidebar-layout-right-desktop-width
The runtime accepts a SidebarLayoutStateController. A consumer can keep this state in memory,
browser storage, a server store, or a combination. The package does not select storage. It marks
width writes as width_deferred and all other durable state changes as immediate.
Prehydration builders accept a self-contained readState(): SidebarLayoutPrehydrationState | null
function. The package serializes this function into the generated script. The function must not
close over module variables. It can read and validate the consumer's selected storage format. The
package does not know the storage key, envelope, or version.
Render the desktop script and then the mobile script immediately after the matching layout markup.
Give each concurrently rendered layout a unique desktopStyleElementId. The desktop script measures
and scopes that layout only. Call clearSidebarLayoutDesktopPrehydrationStyle after the client
state takes ownership.
Use Node 24.11.0 or later and pnpm 11.18.0 or later.
pnpm install --frozen-lockfile
pnpm checkThe package check formats and lints the source, checks TypeScript, runs the package tests, builds the ESM and declaration outputs, and installs the packed tarball in a temporary consumer.