Live demos
Every demo runs the real built package. Each panel is one small source file, shown whole below its preview. The basics live here; stacks, rewrites and popovers are on Complex examples.
Anatomy
One sheet with every structural part colour-coded, so you can see the pieces before wiring one up.
Vanilla
The framework-agnostic core on plain DOM — no React, no framework at all.
Basic
import {createSheetCore} from 'sheet-view'
const sheets = createSheetCore()
function openBasic() {
sheets.open({
title: 'Basic sheet',
content: () => {
const body = document.createElement('div')
body.className = 'demo-body'
body.textContent = 'The core takes plain DOM nodes — no framework, no JSX.'
return body
},
})
}
export function mount(root) {
const button = document.createElement('button')
button.className = 'demo-btn'
button.textContent = 'Open a sheet'
button.addEventListener('click', openBasic)
root.append(button)
return () => sheets.closeAll()
}Themed
Per-instance theming: style sets --sheet-* tokens on the root, so they reach every part — including the backdrop, which a card class can't reach.
import {createSheetCore} from 'sheet-view'
const sheets = createSheetCore()
// A crisp vector × for the close button — inherits currentColor, centered by the
// button's flexbox. Replaces the default glyph.
function closeIcon() {
const NS = 'http://www.w3.org/2000/svg'
const svg = document.createElementNS(NS, 'svg')
svg.setAttribute('viewBox', '0 0 16 16')
svg.setAttribute('width', '15')
svg.setAttribute('height', '15')
svg.setAttribute('aria-hidden', 'true')
const path = document.createElementNS(NS, 'path')
path.setAttribute('d', 'M4 4 L12 12 M12 4 L4 12')
path.setAttribute('stroke', 'currentColor')
path.setAttribute('stroke-width', '1.6')
path.setAttribute('stroke-linecap', 'round')
svg.append(path)
return svg
}
function openThemed() {
sheets.open({
title: 'Themed',
// `style` sets --sheet-* tokens on the root dialog, so they reach every part —
// including the backdrop, which a card class can't touch.
style: {
'--sheet-surface': '#14121c',
'--sheet-text': '#f4f0ff',
'--sheet-handle': 'rgba(255, 255, 255, 0.3)',
'--sheet-backdrop': 'rgb(20 6 40 / 0.6)',
},
closeIcon: closeIcon(),
content: () => {
const body = document.createElement('div')
body.className = 'demo-body'
body.textContent =
'This one sheet is re-themed through the style prop — while the rest of the page stays light.'
return body
},
})
}
export function mount(root) {
const button = document.createElement('button')
button.className = 'demo-btn'
button.textContent = 'Open a themed sheet'
button.addEventListener('click', openThemed)
root.append(button)
return () => sheets.closeAll()
}React
The sheet-view/react adapter — createSheets() plus <SheetHost/>, slots widened to ReactNode.
Basic
import {createSheets, SheetHost} from 'sheet-view/react'
const sheets = createSheets()
export function Demo() {
return (
<>
<button
className="demo-btn"
onClick={() =>
sheets.open({
title: 'Basic sheet',
size: 'md',
content: (
<div className="demo-body">
{Array.from({length: 10}, (_, i) => (
<p key={i}>
Paragraph {i + 1}. A React body portaled into the core's DOM — the
content area scrolls on its own.
</p>
))}
</div>
),
})
}
>
Open a sheet
</button>
<SheetHost instance={sheets} />
</>
)
}Footer
A pinned footer over a scrolling body, plus both default-header glyphs supplied as plain JSX — icon before the title and closeIcon inside the close button. The button itself stays library-owned, so its label, its disabled state and its 44×44 hit target come for free.
import type {ReactElement} from 'react'
import {createSheets, SheetHost} from 'sheet-view/react'
const sheets = createSheets()
// Both header glyphs as plain JSX, the shape every icon library ships. They fill
// library-owned nodes, so the close button keeps its label, its disabled state and
// its 44×44 hit target. Nothing here replaces the header row.
const Sparkle = (): ReactElement => (
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M8 1.5 9.6 6.4 14.5 8 9.6 9.6 8 14.5 6.4 9.6 1.5 8 6.4 6.4Z" fill="currentColor" />
</svg>
)
const Cross = (): ReactElement => (
<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true">
<path
d="M4 4 L12 12 M12 4 L4 12"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
</svg>
)
export function Demo() {
return (
<>
<button
className="demo-btn"
onClick={() =>
sheets.open({
title: 'With a footer',
size: 'md',
icon: <Sparkle />,
closeIcon: <Cross />,
content: (
<div className="demo-body">
<p>The footer stays pinned while the body scrolls.</p>
</div>
),
footer: ({close}) => (
<div className="demo-actions">
<button className="demo-btn" onClick={close}>
Apply & close
</button>
</div>
),
})
}
>
Open
</button>
<SheetHost instance={sheets} />
</>
)
}Form & native controls
A real <form> inside the modal sheet, and the library is not involved at any point. autocomplete attributes let password managers offer autofill; focusOnOpen keeps the entrance keyboard-safe on mobile while the email field autofocuses. The <select> and date popups are OS-rendered, so the modal top layer doesn't apply to them — no portalling, no positioning code. Built-in browser autofill works, and so does an extension's dropdown (iCloud Passwords in Chrome) — see Notes & limitations.
import type {FormEvent} from 'react'
import {createSheets, SheetHost} from 'sheet-view/react'
const sheets = createSheets()
// The select and date popups are rendered by the browser, not by the page, so
// the modal top layer does not apply to them: no portalling, no positioning code.
function BookingForm({close}: {close: () => void}) {
const submit = (e: FormEvent): void => {
e.preventDefault()
close()
}
return (
<form className="demo-form" onSubmit={submit}>
<label>
Email
<input name="email" type="email" autoComplete="username" autoFocus required />
</label>
<label>
Password
<input name="password" type="password" autoComplete="current-password" required />
</label>
<label className="demo-label">
Country
<select className="demo-input" defaultValue="th">
<option value="th">Thailand</option>
<option value="vn">Vietnam</option>
<option value="id">Indonesia</option>
<option value="ph">Philippines</option>
</select>
</label>
<label className="demo-label">
Departure
<input className="demo-input" type="date" />
</label>
<button className="demo-btn" type="submit">
Book
</button>
</form>
)
}
export function Demo() {
return (
<>
<button
className="demo-btn"
onClick={() =>
sheets.open({
title: 'Book a trip',
size: 'md',
focusOnOpen: true,
content: ({close}) => <BookingForm close={close} />,
})
}
>
Open a booking sheet
</button>
<SheetHost instance={sheets} />
</>
)
}Everything past the basics — Complex examples: stacked sheets, keyed re-opens, in-place rewrites, anchored dropdowns, and a toast above the modal.