Framework-agnostic sheets, drawers, dialogs, scroll helpers, and overlay primitives for Capacitor apps.
This package is inspired by the public Silk feature surface, but it is not a wrapper around Silk and does not include Silk source code. It uses platform web APIs and custom elements so the same primitives work in React, Vue, Angular, Svelte, Solid, or plain TypeScript.
Each usecase has its own animated WebP demo. These match the public Silk-style example set: long sheets, detents, sidebars, bottom/top sheets, keyboard-aware sheets, toasts, detached sheets, full pages, stacking, depth, parallax, lightboxes, persistent detents, and cards.
Each framework example opens the same 16-usecase gallery, so StackBlitz is useful for testing the full surface instead of a single smoke-test sheet:
The repo includes a Capacitor app that wraps the React gallery so the sheets can be tested on real iOS and Android devices. The app id is app.capgo.capacitor.sheets.
Capacitor 8 requires Node.js 22 or newer for local mobile commands.
npm run build:mobile
npx cap run ios
npx cap run androidCapgo OTA uploads use the same built gallery:
npx @capgo/cli@latest app add app.capgo.capacitor.sheets --name "Capgo Sheets"
npm run deploy:capgo:dev
npm run deploy:capgoCreate the Capgo app once before the first upload. GitHub Actions also include a Capgo deploy workflow. Tag builds deploy to production, alpha tags deploy to dev, and manual runs can choose either channel. Set CAPGO_TOKEN in repository secrets before running it.
- Framework agnostic custom elements: no React runtime dependency in the core package.
- Bottom sheets, side drawers, top sheets, and centered dialogs through
content-placement. - Detents with
em,rem,dvh,lvh,svh,calc(), and other modern CSS lengths. - Touch, pointer, wheel, and trackpad gestures with dismissal, overshoot, and trap controls.
- Modal behavior: focus trap, focus restore, Escape dismissal, outside-click dismissal, and inert outside content.
- Capacitor safe-area defaults using
env(safe-area-inset-*)plus Capacitor SystemBars--safe-area-inset-*fallbacks. - Keyboard-aware layout using
visualViewportresize/scroll events andpreventScrollfocus behavior. - Overlay compatibility through
cap-islandandcap-external-overlay. - Stacking and outlet animation hooks for depth effects and coordinated page motion.
- Scroll primitives with progress/distance helpers.
- Theme color dimming for Capacitor WebViews and mobile browsers.
- Modern CSS defaults authored with
em-based sizing, with no design-system lock-in.
| Usecase | Capgo Sheets pattern |
|---|---|
| Long Sheet | cap-sheet + cap-scroll or naturally scrolling cap-sheet-content |
| Sheet with Detent | detents="18em 32em" and stepTo() / cap-sheet-trigger action="step" |
| Sidebar | content-placement="left" or content-placement="right" |
| Bottom Sheet | default content-placement="bottom" |
| Sheet with Keyboard | visual viewport offset support via native-focus-scroll-prevention |
| Toast | inert-outside="false", close-on-outside-click="false", focus-trap="false" |
| Detached Sheet | rounded content with margins and cap-sheet-special-wrapper composition |
| Page from Bottom | full-height bottom sheet content |
| Top Sheet | content-placement="top" |
| Sheet with Stacking | cap-sheet-stack, stack depth variables, and stacking animation hooks |
| Sheet with Depth | cap-sheet-outlet progress variables and travelAnimation |
| Parallax Page | cap-sheet-outlet + cap-scroll progress composition |
| Page | full-viewport sheet content or side-entering content-placement |
| Lightbox | content-placement="center" plus backdrop and media content |
| Persistent Sheet with Detent | default-presented, swipe-dismissal="false", inert-outside="false", close-on-outside-click="false" |
| Card | content-placement="center" with compact content |
npm install @capgo/capacitor-sheets
npx cap syncAdd viewport-fit=cover so iOS exposes safe-area insets to CSS:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />Safe areas are enabled by default. cap-sheet-view reads standard env(safe-area-inset-*) values and Capacitor SystemBars fallback variables such as --safe-area-inset-bottom, then applies them to the sheet viewport in em-based layout. You can opt out or choose edges per sheet:
<cap-sheet safe-area="none"></cap-sheet>
<cap-sheet safe-area="bottom left right"></cap-sheet>
For Capacitor apps with overlay status/system bars, keep the platform plugins responsible for exposing correct insets:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
plugins: {
StatusBar: {
overlaysWebView: true,
},
Keyboard: {
resize: 'body',
resizeOnFullScreen: true,
},
SystemBars: {
insetsHandling: 'css',
},
},
};
export default config;Keyboard handling is on by default through native-focus-scroll-prevention. It listens to visualViewport changes, keeps focused controls visible above the keyboard, and restores the sheet offset when the keyboard closes.
<script type="module">
import '@capgo/capacitor-sheets';
</script>
<cap-sheet-trigger for="booking-sheet" action="present">Open</cap-sheet-trigger>
<cap-sheet id="booking-sheet" detents="18em 32em" content-placement="bottom">
<cap-sheet-portal>
<cap-sheet-view>
<cap-sheet-backdrop></cap-sheet-backdrop>
<cap-sheet-content class="sheet">
<cap-sheet-bleeding-background></cap-sheet-bleeding-background>
<cap-sheet-handle></cap-sheet-handle>
<cap-sheet-title>Evening route</cap-sheet-title>
<cap-sheet-description>Choose a route and confirm pickup.</cap-sheet-description>
<cap-sheet-trigger action="dismiss">Done</cap-sheet-trigger>
</cap-sheet-content>
</cap-sheet-view>
</cap-sheet-portal>
</cap-sheet>.sheet {
width: min(100%, 34em);
padding: 0 1.25em 1.25em;
}Full runnable examples live in:
examples/react-appexamples/vue-appexamples/angular-appexamples/svelte-appexamples/solid-app
All five apps mount the shared usecase gallery from examples/shared/usecase-gallery.ts. That gallery covers Long Sheet, Sheet with Detent, Sidebar, Bottom Sheet, Sheet with Keyboard, Toast, Detached Sheet, Page from Bottom, Top Sheet, Sheet with Stacking, Sheet with Depth, Parallax Page, Page, Lightbox, Persistent Sheet with Detent, and Card.
import { setupSheet } from '@capgo/capacitor-sheets/react';
import { useEffect, useRef } from 'react';
import '@capgo/capacitor-sheets';
import { mountUsecaseGallery } from '../../shared/usecase-gallery';
export function App() {
const galleryRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!galleryRef.current) return;
return mountUsecaseGallery(galleryRef.current, {
framework: 'React',
setupSheet,
});
}, []);
return <div ref={galleryRef} />;
}<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { setupSheet } from '@capgo/capacitor-sheets/vue';
import '@capgo/capacitor-sheets';
import { mountUsecaseGallery } from '../../shared/usecase-gallery';
const galleryRef = ref<HTMLElement | null>(null);
let cleanup: (() => void) | undefined;
onMounted(() => {
if (galleryRef.value) {
cleanup = mountUsecaseGallery(galleryRef.value, {
framework: 'Vue',
setupSheet,
});
}
});
onUnmounted(() => cleanup?.());
</script>
<template>
<div ref="galleryRef"></div>
</template>import type { AfterViewInit, ElementRef, OnDestroy } from '@angular/core';
import { Component, CUSTOM_ELEMENTS_SCHEMA, ViewChild } from '@angular/core';
import { setupSheet } from '@capgo/capacitor-sheets/angular';
import '@capgo/capacitor-sheets';
import { mountUsecaseGallery } from '../../../shared/usecase-gallery';
@Component({
selector: 'app-root',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `<div #gallery></div>`,
})
export class AppComponent implements AfterViewInit, OnDestroy {
@ViewChild('gallery', { static: true }) gallery?: ElementRef<HTMLElement>;
private cleanup?: () => void;
ngAfterViewInit(): void {
if (this.gallery?.nativeElement) {
this.cleanup = mountUsecaseGallery(this.gallery.nativeElement, {
framework: 'Angular',
setupSheet,
});
}
}
ngOnDestroy(): void {
this.cleanup?.();
}
}<script lang="ts">
import { onMount } from 'svelte'
import { setupSheet } from '@capgo/capacitor-sheets/svelte'
import '@capgo/capacitor-sheets'
import { mountUsecaseGallery } from '../../shared/usecase-gallery'
let gallery: HTMLDivElement
onMount(() =>
mountUsecaseGallery(gallery, {
framework: 'Svelte',
setupSheet,
}),
)
</script>
<div bind:this={gallery}></div>import { onCleanup, onMount } from 'solid-js';
import { setupSheet } from '@capgo/capacitor-sheets/solid';
import '@capgo/capacitor-sheets';
import { mountUsecaseGallery } from '../../shared/usecase-gallery';
export function App() {
let galleryEl!: HTMLDivElement;
onMount(() => {
const cleanup = mountUsecaseGallery(galleryEl, {
framework: 'Solid',
setupSheet,
});
onCleanup(cleanup);
});
return <div ref={galleryEl} />;
}Core elements:
cap-sheet: state, detents, placement, gestures, modal behavior, lifecycle events.cap-sheet-trigger: declarative present, dismiss, toggle, and step actions.cap-sheet-portal: optional body portal for overlay layering.cap-sheet-view: viewport overlay host.cap-sheet-backdrop: progress-synced modal backdrop.cap-sheet-content: accessible sheet surface.cap-sheet-bleeding-background: background extension for rounded edge sheets.cap-sheet-handle: draggable and keyboard-accessible detent handle.cap-sheet-titleandcap-sheet-description: accessible naming.cap-sheet-special-wrapperandcap-sheet-special-wrapper-content: composition hooks for detached sheets, cards, lightboxes, and custom page layouts.cap-sheet-stack: groups sheets for stacked depth.cap-sheet-outlet: receives sheet progress variables and optionaltravelAnimation.cap-scrollandcap-scroll-content: scroll progress helpers.cap-fixed,cap-island,cap-external-overlay,cap-visually-hidden,cap-auto-focus-target: composition and accessibility helpers.
Important events:
cap-sheet-presented-changecap-sheet-active-detent-changecap-sheet-travelcap-sheet-travel-status-changecap-sheet-travel-range-changecap-sheet-drag-startcap-sheet-drag-endcap-scroll
Imperative methods on cap-sheet:
const sheet = document.querySelector('cap-sheet')!;
await sheet.present();
await sheet.dismiss();
await sheet.toggle();
await sheet.stepTo(2);
await sheet.step('down');The public TypeScript API is documented with TSDoc in src/core/types.ts, and generated declarations are emitted during npm run build.
npm install
npm run render:demos
npm run build
npm run dev:examples -- react vue svelte angular solidIn the Capgo monorepo, use bun and bunx for local commands.















