Pricing

SidePanel handbook

v1.4.3 · @snowui-design-system/components

License: Before reading component documentation, source, or npm usage instructions for the first time, read the full LICENSE. Reuse that review for the same task and version. Public API configuration and internal implementation changes have different license implications. All components use the unified runtime package; source rights remain governed by the license.

Overview

Show details or supporting content at the right edge, in floating or inline mode, while preserving context from the main page.

When to use it

Use it for closable detail or auxiliary panels. Persistent notification, activity, or contact columns are page content even when they sit on the right; they do not automatically need SidePanel.

  • Control visibility with bind:open; placement chooses floating or inline. Bind width and constrain resizing with minimum and maximum widths.

  • children provides scrollable content. For inline mode, inlineMotionOwner assigns panel movement to one owner so the page rail and component do not move it twice.

  • When title is omitted, the toolbar title and panel accessible name follow LocaleProvider. An explicit title, including an empty string, and business content remain unchanged.

Quick start

This example shows the recommended basic composition. See the sections below for more cases and composition limits.

<script lang="ts">	import { Badge, Button, Frame, Input, Text, Textarea } from '@snowui-design-system/components';	import { SidePanel, createSidePanelLayoutMotionStyle, type SidePanelPlacement } from '@snowui-design-system/components';	type ComponentRecord = { name: string; category: 'Basic' | 'Composite'; packageName: string; description: string; notes: string; };	let componentRecords = $state<ComponentRecord[]>([		{ name: 'DataTable', category: 'Composite', packageName: '@snowui-design-system/components', description: 'Sort, select, and manage records.', notes: 'Review responsive columns.' },		{ name: 'SidePanel', category: 'Composite', packageName: '@snowui-design-system/components', description: 'View and edit record details.', notes: 'Check floating and inline layouts.' },		{ name: 'Button', category: 'Basic', packageName: '@snowui-design-system/components', description: 'Actions for SnowUI products.', notes: 'Variants ready for release.' }	]);	let panelOpen = $state(false);	let panelPlacement = $state<SidePanelPlacement>('floating');	let activeIndex = $state(0);	let panelWidth = $state(420);	let activity = $state('');	const activeComponent = $derived(componentRecords[activeIndex]);	const inlineLayoutActive = $derived(panelOpen && panelPlacement === 'inline');	const sidePanelLayoutStyle = $derived(createSidePanelLayoutMotionStyle(inlineLayoutActive, panelWidth));	function openComponent(index: number) { activeIndex = index; panelOpen = true; activity = ''; }	function setPanelOpen(next: boolean) { panelOpen = next; }	function showPrevious() { activeIndex = (activeIndex - 1 + componentRecords.length) % componentRecords.length; activity = ''; }	function showNext() { activeIndex = (activeIndex + 1) % componentRecords.length; activity = ''; }	async function copyActive() { try { await navigator.clipboard?.writeText(activeComponent.name); activity = 'Copied ' + activeComponent.name + '.'; } catch { activity = 'Copy unavailable.'; } }	function deleteActive() { if (componentRecords.length === 1) { activity = 'Keep one component.'; return; } const removed = activeComponent.name; componentRecords = componentRecords.filter((_, index) => index !== activeIndex); activeIndex = Math.min(activeIndex, componentRecords.length - 1); activity = 'Removed ' + removed + '.'; }</script> <div data-side-panel-layout style={sidePanelLayoutStyle}>	<div data-side-panel-main style="display: flex; flex-direction: column; align-items: stretch; gap: var(--gap-3); width: min(100%, 28rem); margin-inline: auto;">		<Text style="color: var(--black-40);">Components</Text>		<div style="display: grid; gap: var(--gap-2);">			{#each componentRecords as component, index (component.name)}				<Button variant={index === activeIndex ? 'gray' : 'outline'} onclick={() => openComponent(index)}>{component.name} · {component.category}</Button>			{/each}		</div>	</div>	<div data-side-panel-layer aria-hidden={inlineLayoutActive ? undefined : 'true'}>		<div data-side-panel-rail>			<SidePanel				bind:open={panelOpen}				bind:placement={panelPlacement}				bind:width={panelWidth}				inlineMotionOwner="layout"				minWidth={320}				maxWidth={560}				title={activeComponent.name}				editable				onOpenChange={setPanelOpen}				onPrevious={showPrevious}				onNext={showNext}				onCopy={copyActive}				onDelete={deleteActive}				style={panelPlacement === 'inline' ? 'height: 100%;' : undefined}			>				<Frame column gap={4} style="min-width: 0; width: 100%;">					<Frame singleLine><Text lines={1} size={20} weight={600}>{activeComponent.name}</Text><Badge as="trailing" count={activeComponent.category} color={activeComponent.category === 'Composite' ? 'Indigo' : 'Blue'} /></Frame>					<Text style="color: var(--black-40);">{activeComponent.description}</Text>					<Input value={activeComponent.packageName} aria-label="Package name" />					<Textarea value={activeComponent.notes} aria-label="Release note" style="height: 8rem;" />				</Frame>			</SidePanel>		</div>	</div></div> <style>	[data-side-panel-layout] { position: relative; box-sizing: border-box; width: 100%; min-width: 0; padding-right: var(--snowui-side-panel-layout-space, 0px); transition: padding-right var(--snowui-side-panel-layout-motion-duration, 500ms) var(--snowui-side-panel-layout-motion-easing, ease-out); }	[data-side-panel-main] { min-width: 0; }	[data-side-panel-layer] { position: fixed; inset: 0; overflow: hidden; pointer-events: none; z-index: 40; }	[data-side-panel-rail] { position: absolute; top: var(--padding-4); right: var(--padding-4); bottom: var(--padding-4); box-sizing: border-box; width: var(--snowui-side-panel-layout-panel-width, 420px); transform: translateX(var(--snowui-side-panel-layout-rail-translate-x, 436px)); transition: transform var(--snowui-side-panel-layout-motion-duration, 500ms) var(--snowui-side-panel-layout-motion-easing, ease-out); pointer-events: auto; }	@media (prefers-reduced-motion: reduce) { [data-side-panel-layout], [data-side-panel-rail] { transition: none; } }</style>

API

Keyword Type Default Description
open boolean false Show the panel; supports bind:open.
placement SidePanelPlacement "floating" Choose floating or right-side inline placement; supports bind:placement.
width number 460 Panel width; supports bind:width.
minWidth number 320 Minimum panel width during drag resize.
maxWidth number 720 Maximum panel width during drag resize.
inlineMotionOwner "component" | "layout" "component" Owner of inline panel movement. Default is component. For the documented floating-dock layout, pass layout and use the layout helper with a persistent rail.
showTopToolbar boolean true Show the fixed-content top toolbar; its controls cannot be replaced.
editable boolean false Whether content is editable; true adds default toolbar actions.
showBottomToolbar boolean — Explicitly hide the bottom bar. By default it appears when editable or customized.
bottomLeading Snippet — Replace the entire left group of the bottom bar; passing it shows the bar.
bottomTrailing Snippet — Replace the entire right group of the bottom bar; passing it shows the bar.
bottomLeadingActions SidePanelToolbarAction[] — Built-in left actions when no bottomLeading is supplied; editable mode defaults to copy/delete.
bottomTrailingActions SidePanelToolbarAction[] — Built-in right actions when no bottomTrailing is supplied; editable mode defaults to cancel/save.
title string — Top-toolbar title and panel accessible name. When omitted, sidePanel.title provides the localized title (“Details” in en-US). An explicit empty string is preserved.
showTitle boolean false Show the toolbar title; hiding it retains the accessible name and toolbar buttons.
backdrop DialogBackdrop | false false Floating backdrop using shared Dialog mask; ignored inline.
closeOnEscape boolean true Close on Escape.
closeOnOutside boolean true Close when clicking outside the floating SidePanel.
onOpenChange (open: boolean) => void — Called after open state changes.
onPlacementChange (placement: SidePanelPlacement) => void — Called after switching floating/inline placement.
onPrevious () => void | Promise<void> — Called on Previous in the top toolbar.
onNext () => void | Promise<void> — Called on Next in the top toolbar.
onCopy () => void | Promise<void> — Called on the default copy action.
onDelete () => void | Promise<void> — Called on the default delete action.
onClose () => void | Promise<void> — Called before closing.
children Snippet — Caller-supplied details or editable content in the panel scroller.
parts ComponentPartsOf<{ Button: typeof Button; Indicator: typeof Indicator; }> — Configure generated toolbar Buttons and the full-edge resize hit area. Full part Props such as class/style apply to generated instances; explicit children or bottom Snippets are configured directly.

See the base component documentation for inherited capabilities not redeclared here. This table lists only named public Props declared by this component.

Usage boundaries and details

Replacing bottom toolbar groups

Priority: bottomLeading/bottomTrailing Snippet > corresponding actions array > generated defaults.

<script lang="ts">  import { Frame, Button } from '@snowui-design-system/components';  import { SidePanel } from '@snowui-design-system/components';  let open = $state(true);function download() { /* Connect application download */ }</script>  <SidePanel bind:open title="Subscription">  <Frame column gap={3}>…</Frame></SidePanel>  <SidePanel bind:open editable title="Subscription">  <Frame column gap={3}>…</Frame></SidePanel>  <SidePanel  bind:open  bottomLeadingActions={[{ role: "custom", icon: "arrow-line-down", ariaLabel: "Download", onClick: download }]}/>  <SidePanel bind:open editable>  {#snippet bottomLeading()}    <Frame row gap={2}>      <Button bare iconOnly emphasis="muted" size="sm" aria-label="Download" onclick={download}>        Download      </Button>    </Frame>  {/snippet}  …</SidePanel>

The top toolbar (preview toggle, previous/next item, title, close) cannot be replaced through Props or Snippets.

Inline layout effect

The documented placement="inline" behavior follows SidePanel-Demo.svelte (playground/demo/SidePanel-Demo.svelte). SidePanel stays in a floating dock rather than ordinary document flow:

  1. A fixed layer and rail carry SidePanel above other page content.

  2. The page adds padding-right to selected content, reducing its available width.

  3. Areas outside that reserved space keep their layout and may be covered by the panel.

  4. One inlineActive state and one helper update panel movement and content contraction together, keeping visual timing aligned.

“Inline” means the panel participates in the page’s available-space allocation, not that its visual layer enters normal document flow. The page explicitly chooses what content contracts; SidePanel still floats above it.

Responsibility split

Element Owner Default rule
Panel content, presence, close lifecycle SidePanel Component; unmount after exit completes.
Panel appearance, width, radius, border, and scrolling SidePanel Inline defaults to 460px width, 20px radius, real border, and independent content scrolling.
Horizontal panel travel within the rail Page and layout helper Pass inlineMotionOwner="layout"; persistent rail owns transform.
Which elements contract? Page Only layout content consuming --snowui-side-panel-layout-space contracts.
Which elements may be covered? Page Areas outside the layout container keep their width and may sit below SidePanel.
Viewport top, right, and bottom spacing Page rail Defaults to 16px.
Final panel height Page rail and SidePanel Rail sets available height; SidePanel fills it with height: 100%.
Z-index, page scroller, header offset Page SidePanel cannot infer page structure; the page decides.

Choosing contracted and covered content

  • Contract all main content: place header, Table, pagination, and related content in data-side-panel-layout and consume padding-right together.

  • Contract only Table: only its wrapper consumes --snowui-side-panel-layout-space; header or left navigation outside can be covered.

  • Allow panel to cover a floating control: the control consumes no layout space and uses a z-index below data-side-panel-layer.

  • Keep a control above the panel: the control consumes no layout space, and the page gives it a z-index above the panel layer.

SidePanel does not query Table, main, or header, or rewrite sibling widths. Page structure and CSS explicitly define the affected layout scope.

Coordinated motion

RareUI’s reference moves panel x with the same state as main-content paddingRight. SnowUI follows that synchronization with its own SIDE_PANEL_APPEAR_MOTION/SIDE_PANEL_DISAPPEAR_MOTION: critically damped spring 300 / 34.6410161514 / 1, 500ms trajectory window, no rebound, and enough distance to exit fully.

Default inline integration requires:

  1. Set inlineActive to open && placement === "inline".

  2. Call createSidePanelLayoutMotionStyle(inlineActive, panelWidth, 16) once and apply its result to the coordination container.

  3. Content padding-right and rail transform consume helper variables directly, without separate duration or easing.

  4. Pass inlineMotionOwner="layout" so component and rail transforms do not stack.

  5. Keep layer and rail mounted; SidePanel follows its presence lifecycle and unmounts after exit.

  6. Reduced-motion preference disables both content contraction and rail travel transitions.

The helper exposes page-level bridge variables:

Variable Effect
--snowui-side-panel-layout-space panelWidth + gap when open; 0px when closed.
--snowui-side-panel-layout-panel-width Live width shared by rail and panel.
--snowui-side-panel-layout-rail-translate-x 0px when open; panelWidth + gap when closed.
--snowui-side-panel-layout-motion-duration Duration from the active SidePanel appear/disappear preset.
--snowui-side-panel-layout-motion-easing Spring linear(...) easing from that preset.

Complete default example

<script lang="ts">  import {    SidePanel,    createSidePanelLayoutMotionStyle,  } from "@snowui-design-system/components";   let open = $state(false);  let placement = $state<"floating" | "inline">("floating");  let panelWidth = $state(460);   const inlineActive = $derived(open && placement === "inline");  const sidePanelLayoutStyle = $derived(    createSidePanelLayoutMotionStyle(inlineActive, panelWidth, 16),  );</script>  <div data-side-panel-layout style={sidePanelLayoutStyle}>  <main data-side-panel-main>   </main>    <div data-side-panel-layer aria-hidden={inlineActive ? undefined : "true"}>    <div data-side-panel-rail>      <SidePanel        bind:open        bind:placement        bind:width={panelWidth}        inlineMotionOwner="layout"        style={placement === "inline" ? "height: 100%;" : undefined}      >       </SidePanel>    </div>  </div></div>  <footer>...</footer> <style>  [data-side-panel-layout] {    position: relative;    box-sizing: border-box;    width: 100%;    min-width: 0;    padding-right: var(--snowui-side-panel-layout-space, 0px);    transition: padding-right      var(--snowui-side-panel-layout-motion-duration, 500ms)      var(--snowui-side-panel-layout-motion-easing, ease-out);  }   [data-side-panel-main] {    min-width: 0;  }   [data-side-panel-layer] {    position: fixed;    inset: 0;    overflow: hidden;    pointer-events: none;    z-index: 40;  }   [data-side-panel-rail] {    position: absolute;    top: 16px;    right: 16px;    bottom: 16px;    box-sizing: border-box;    width: var(--snowui-side-panel-layout-panel-width, 460px);    transform: translateX(      var(--snowui-side-panel-layout-rail-translate-x, 476px)    );    transition: transform      var(--snowui-side-panel-layout-motion-duration, 500ms)      var(--snowui-side-panel-layout-motion-easing, ease-out);    pointer-events: auto;  }   @media (prefers-reduced-motion: reduce) {    [data-side-panel-layout],    [data-side-panel-rail] {      transition: none;    }  }</style>

Height and viewport spacing

SidePanel can fill a height, but the page chooses the coordinate space:

Scene Recommended approach Result
Default inline dock Rail uses top/right/bottom: 16px; SidePanel uses height: 100%. Panel height is viewport height minus 32px, regardless of document scroll.
Standalone inline mode without page rail Do not override height. Use component default calc(100dvh - 32px).
Page with fixed header Page moves rail top, e.g. top: calc(var(--header-height) + 16px). Panel clears the header; page still sets bottom.
Panel scrolls with document Page uses normal-flow or sticky rail and sets height explicitly. Height and scroll bounds follow the page container, not fixed viewport spacing.
Dock within a container Container establishes positioning context; rail becomes absolute with explicit top/right/bottom. Container determines panel height.

Do not combine the component’s default 100dvh - 32px height with rail top/bottom constraints. Let rail set height and pass height: 100% to SidePanel so there is one source of truth.

Styling inline mode

Target Public override
Width width / bind:width
Height Root style/class or --snowui-side-panel-height-override.
Rounded corners Root border-radius or --snowui-side-panel-radius-override.
Border / shadow Inline uses a real border; floating mode uses the quick-action shadow.

Layout model

Main layout model: composite management layout. See LayoutProtocol §12.3 (specs/maintainers/LayoutProtocol.md).

Example

<script lang="ts">  import { Frame, Text } from '@snowui-design-system/components';  import { SidePanel } from '@snowui-design-system/components';  let open = $state(true);function download() { /* Connect application download */ }</script> <SidePanel bind:open title="Subscription">  <Frame column gap={3}>    <Text>Content is supplied explicitly by the caller.</Text>  </Frame></SidePanel>

Floating backdrop

No backdrop by default; the previous backdrop selection is retained for mode switching. backdrop="opaque" adds a 10% black mask, "blur" adds var(--size-5) (20px) blur to the same mask, and "transparent" only blocks interaction. Shared .snowui-backdrop supplies visuals; Dialog motion tokens fade it with SidePanel presence. Inline mode shows no mask. Clicking it follows closeOnOutside; close button and Escape are unchanged.

The shared Backdrop (components/src/lib/backdrop.md) fades from transparent and follows the same reduced-motion rules as Dialog and ImageViewer.

SidePanel style overlay

Read the full LICENSE first; reuse that review for the same task, version, and scope.

First use public component Props to match the desired appearance, preserving the existing look where possible. If changes are necessary, follow the style selection and override order: use your own class (including Tailwind already in the project) or an instance style. The names below are public overrides actually consumed by source; check their scope and do not modify private DOM. Choose the correct SnowUI token family. External apps need not install SnowUI website utilities.

Variable Purpose and default reference
--snowui-scroll-mask-bottom-size-override Universal scrolling mask with height (default 16px; 56px when there is a bottom toolbar)
--snowui-scroll-mask-bottom-solid-ratio-override The proportion of solids on the lower side of the universal scrolling mask (default 0.8)
--snowui-scroll-mask-top-size-override Universal scrolling mask with height on the mask (follow the height of the top toolbar by default)
--snowui-scroll-mask-top-solid-ratio-override The proportion of solid on the upper side of the universal scrolling mask (default 0.8)
--snowui-side-panel-font-size-override Font-size (default var(--text-14) = 14px)
--snowui-side-panel-height-override Embedded mode height only (default 100dvh - 32px)
--snowui-side-panel-line-height-override Line-height (default var(--leading-14))
--snowui-side-panel-radius-override Rounded corners (default 20px = var(--radius-5))
--snowui-side-panel-scroll-mask-bottom-size-override Lower mask with height (default 56px; solid-ratio 0.8)
--snowui-side-panel-scroll-mask-top-size-override Upper mask with height (default 48px; solid-ratio 0.8)
--snowui-side-panel-z-index-override Floating layer z-index (default 50)

Design Guide · Token · Combination boundary

SidePanel type and status

Required Props: no named required Props.

Bidirectional binding: bind:open, bind:placement, bind:width.

Inheritance statement: Omit<FrameProps, "children" | "width">. The inheritance ability is subject to the public document of the base class; the fields in Omit have been excluded and cannot be used as they are according to the base class.

Share Frame API · Return API

The following type declarations are actually referenced by public Props and come from source. A Svelte Snippet denotes a content fragment; ComponentProps denotes the Props of the corresponding component.

SidePanelPlacement

Source: components/src/lib/side-panel.ts

export type SidePanelPlacement = "floating" | "inline";

SidePanelToolbarAction

Source: components/src/lib/side-panel.ts

export interface SidePanelToolbarAction {    id?: string;    role?: SidePanelActionRole;    label?: string;    icon?: string;    ariaLabel?: string;    variant?: "gray" | "filled" | "outline" | "borderless";    bare?: boolean;    disabled?: boolean;    hidden?: boolean;    onClick?: (event: MouseEvent) => void | Promise<void>;}

SidePanelActionRole

Source: components/src/lib/side-panel.ts

export type SidePanelActionRole =    | "placement"    | "previous"    | "next"    | "copy"    | "delete"    | "close"    | "custom";

DialogBackdrop

Source: components/src/logic/dialog.svelte.ts

export type DialogBackdrop = import("../lib/backdrop").BackdropVariant;

ComponentPartsOf

Source: components/src/logic/component-parts.svelte.ts

export type ComponentPartsOf<    T extends Record<string, Component<any>>,> = {    [K in keyof T]?: ConfigurableComponentPartProps<T[K]>;};

ConfigurableComponentPartProps

Source: components/src/logic/component-parts.svelte.ts

export type ConfigurableComponentPartProps<C extends Component<any>> = Partial<    Omit<ComponentProps<C>, ComponentPartControlledKey>>;

ComponentPartControlledKey

Source: components/src/logic/component-parts.svelte.ts

export type ComponentPartControlledKey =    | "children"    | "as"    | "value"    | "defaultValue"    | "open"    | "selected"    | "checked"    | "disabled"    | "readonly"    | "id"    | `on${string}`    | `aria-${string}`;