Autumn Collection
Doors open at 10:00. Coffee is on the house until the first talk starts.
An edit surface beside a live preview, each scrolling on its own, collapsing to a tab switch when the panel is too narrow to hold both. The split is a container query on the panel's own width, not a viewport breakpoint.
Two slots, edit and preview. The examples on this page run in fill mode at a fixed height so they stay inside the docs; a real page usually leaves mode at sticky and lets the document scroll.
Import the parts you need from the component's folder.
import {
PreviewPanel,
PreviewPanelPane,
PreviewPanelSurface,
PreviewPanelSwitcher,
} from "@/components/ui/preview-panel";The admin sidebar is 18rem open and 3rem collapsed, a 240px swing that is wider than a Tailwind breakpoint step. A viewport query cannot see it, so at 1440px the same page would be comfortable with the sidebar collapsed and cramped with it open. The threshold is measured on the panel instead: 5xl (1024px) by default, with 4xl (896px) and 6xl (1152px) available. 4xl would split at roughly 448px per pane with the sidebar open, too narrow for a form beside a preview; 6xl lands on exactly 1440px with the sidebar expanded, the most common laptop width here, so rounding and zoom would make it flap.
<PreviewPanel threshold="6xl" ratio="3:2" side="left">
<template #edit>…</template>
<template #preview>…</template>
</PreviewPanel>Two ways the panel gets its height, and the choice decides which pane scrolls.
The panel never references a navbar token. The page passes its own offsets in, which is what lets the same component serve three repos with three different headers.
<PreviewPanel
offset="calc(var(--navbar-height-desktop) + 1rem)"
inset-bottom="1rem"
>In sticky mode, whatever follows the panel in the document must be no taller than insetBottom. A sticky element stops sticking once its containing block runs out, so with pb-16 on the page and a 1rem insetBottom, scrolling to the very bottom shoves the pinned preview 3rem up and its top disappears under the page header. Pad the inside of the edit slot instead: that height belongs to the containing block and costs nothing. There is no fixing this from inside the component, because the offending padding lives on an ancestor it cannot see.
- <div class="pb-16">
- <PreviewPanel>…</PreviewPanel>
- </div>
+ <div>
+ <PreviewPanel>
+ <template #edit><div class="pb-16">…</div></template>
+ </PreviewPanel>
+ </div>The preview is framed in a card with an optional chrome row: a title, a reload button, and an expand button. Reload remounts the preview subtree for a tick, because a :key bump on a wrapper is not enough when the slot's vnodes belong to the consumer.
surface=false drops the border, background and rounding and renders the slot straight into the scroller, for a preview that draws its own page.
Below the threshold a floating pill switches panes. It is plain position: fixed, like every other pane switcher in the app, and the container query that hides it lives on a static wrapper rather than the pill itself. Use PreviewPanelSwitcher on its own inside any Tabs when a page wants the same control somewhere else, and pass hideSwitcher to the panel so there are not two.
container-type: inline-size does NOT make the panel a containing block for position: fixed descendants. That was assumed here at first and measured to be wrong: a probe anchored to the viewport and the computed contain read none. Only an explicit contain: layout or paint would capture them. Two consequences are baked into this folder. The floating switcher is plain fixed. And the expanded preview teleports to body rather than switching to fixed, so component state survives the move even though the inner scroll position does not.
Import all parts and piece them together.
<script setup>
import {
PreviewPanel,
PreviewPanelPane (edit),
PreviewPanelPane (preview),
PreviewPanelSurface,
PreviewPanelSwitcher,
} from "@/components/ui/preview-panel";
</script>
<template>
<PreviewPanel>
<PreviewPanelPane (edit) />
<PreviewPanelPane (preview)>
<PreviewPanelSurface />
</PreviewPanelPane (preview)>
<PreviewPanelSwitcher />
</PreviewPanel>
</template>Keyboard shortcuts and ARIA behavior.