Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
How to build mobile-native experiences with Vanilla Breeze: safe areas, app shells, bottom sheets, touch targets, forms, and horizontal scroll.
Most CSS frameworks ignore mobile-specific concerns, leaving you to bolt on safe areas, touch targets, and viewport fixes. VB takes the opposite approach: mobile support ships inside the core.
100dvh body height, smooth scrolling, and responsive layout collapse. No attributes required.viewport-fit=cover to unlock safe-area tokens, use data-page-layout="app-shell" for bottom-nav shells, and use <dialog data-position="bottom"> for action sheets.inputmode, autocomplete, and enterkeyhint.Because VB’s design tokens drive all sizing and spacing, the @media (pointer: coarse) query only needs to set --size-touch-min: 2.75rem — every button, input, and select automatically adapts.
Modern phones have notches, Dynamic Islands, and home indicators that occupy real pixels. To respect these areas, add viewport-fit=cover to your viewport meta tag:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
This activates VB’s safe-area tokens, which are defined automatically in the token layer:
/* VB exposes these tokens automatically */:root { --safe-top: env(safe-area-inset-top, 0px); --safe-right: env(safe-area-inset-right, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); --safe-left: env(safe-area-inset-left, 0px);} /* Use them on edge-touching elements */.my-fixed-bar { padding-block-end: calc(var(--size-m) + var(--safe-bottom));}
| Element | Token used |
|---|---|
dialog[data-position="bottom"] |
--safe-bottom (padding) |
data-page-layout="app-shell" nav (mobile) |
--safe-bottom (padding) |
For custom fixed bars or sticky footers, apply safe-area tokens manually using calc().
These mobile features are built into VB’s core — no extra attributes or configuration needed.
| Concern | VB solution |
|---|---|
| Touch targets | @media (pointer: coarse) enforces 44px min on buttons, inputs, selects |
| Full-height pages | body { min-block-size: 100dvh } |
| Tap highlight | -webkit-tap-highlight-color: transparent in reset |
| iOS text zoom | text-size-adjust: none in reset |
| Smooth scrolling | scroll-behavior: smooth (respects reduced-motion) |
| Sidebar collapse | data-layout="sidebar" wraps naturally (intrinsic CSS, no media query) |
| Column ↔ row flip | data-layout="switcher" flips at configurable threshold |
| Split stack | data-layout="split" stacks at < 48rem |
| Hover-only motion | @media (hover: none) disables hover lift/scale |
| Scroll snap | Built into data-layout="reel" and <carousel-wc> |
| Scroll containment | overscroll-behavior: contain on dialogs and sticky nav/aside |
The data-page-layout="app-shell" layout creates a header/main/nav grid. On screens narrower than 48rem, the nav automatically moves below main — becoming a bottom navigation bar. Safe-area bottom padding is applied automatically.
<body data-page-layout="app-shell" data-layout-gap="none"> <header data-layout="cluster" data-layout-justify="between" data-layout-sticky> <brand-mark>AppName</brand-mark> <button class="icon-only ghost" aria-label="Notifications"> <icon-wc name="bell"></icon-wc> </button> </header> <main data-layout="stack" data-layout-gap="l"> <!-- scrollable content --> </main> <nav aria-label="Main navigation"> <ul> <li><a href="#" aria-current="page"><icon-wc name="home"></icon-wc> Home</a></li> <li><a href="#"><icon-wc name="search"></icon-wc> Search</a></li> <li><a href="#"><icon-wc name="folder"></icon-wc> Projects</a></li> <li><a href="#"><icon-wc name="user"></icon-wc> Profile</a></li> </ul> </nav></body>
See also: Application Shells pattern
VB’s native <dialog> styling includes a bottom drawer variant. It slides up from the bottom, has rounded top corners, respects safe areas, and prevents scroll chaining — all with zero JavaScript beyond the native showModal() API.
<button commandfor="my-sheet" command="show-modal"> Show Options</button> <dialog id="my-sheet" data-position="bottom"> <header data-layout="cluster" data-layout-justify="between"> <h2>Options</h2> <button commandfor="my-sheet" command="close" class="ghost icon-only" aria-label="Close"> <icon-wc name="x"></icon-wc> </button> </header> <section> <nav aria-label="Actions"> <ul> <li><a href="#"><icon-wc name="share"></icon-wc> Share</a></li> <li><a href="#"><icon-wc name="bookmark"></icon-wc> Save</a></li> </ul> </nav> </section></dialog>
The commandfor / command attributes use the Invokers API to open and close the dialog declaratively. In browsers that don’t support Invokers yet, fall back to document.getElementById('my-sheet').showModal().
Add data-gesture="dismiss-down" to enable swipe-down-to-close on mobile. The gesture library tracks the drag, applies opacity falloff, and closes the sheet when the swipe threshold is met. Buttons and links inside remain interactive.
<dialog id="my-sheet" data-position="bottom" data-gesture="dismiss-down"> ...</dialog>
See also: Dialog element docs — Bottom Sheet snippet
The single biggest mobile form improvement is using the right HTML attributes. These are native HTML — not VB features — but VB’s <form-field> component makes them easy to use correctly.
| Attribute | What it does on mobile |
|---|---|
type="email" |
Shows keyboard with @ key prominent |
type="tel" |
Shows numeric dialpad |
inputmode="numeric" |
Numeric keyboard without spinners (use for credit cards, ZIP codes) |
autocomplete="email" |
Enables one-tap autofill from saved contacts |
autocomplete="cc-number" |
iOS shows camera to scan physical card; Android offers Google Pay |
autocomplete="one-time-code" |
iOS shows SMS suggestion bar above keyboard |
enterkeyhint="next" |
Shows “Next” on the Enter key (guides user through the form) |
enterkeyhint="send" |
Shows “Send” on the last field |
<form class="stacked" novalidate> <form-field> <label for="email">Email</label> <input type="email" id="email" required autocomplete="email" enterkeyhint="next"> </form-field> <form-field> <label for="phone">Phone</label> <input type="tel" id="phone" autocomplete="tel" inputmode="tel" enterkeyhint="next"> </form-field> <form-field> <label for="zip">ZIP Code</label> <input type="text" id="zip" autocomplete="postal-code" inputmode="numeric" pattern="[0-9]{5}" enterkeyhint="send"> </form-field> <button type="submit" class="full-width">Submit</button></form>
See also: autocomplete reference, inputmode reference, enterkeyhint reference
VB’s cover layout supports three viewport height units:
| Value | Unit | Use when |
|---|---|---|
data-layout-min="100svh" |
Small viewport height | Hero sections — always fits when browser chrome is visible |
data-layout-min="100dvh" |
Dynamic viewport height | Full-screen app panels — updates as chrome hides on scroll |
data-layout-min="100vh" |
Legacy viewport height | Fallback — broken on mobile (ignores browser chrome) |
<!-- Hero that fits in one mobile screen (browser chrome visible) --><section data-layout="cover" data-layout-min="100svh" data-layout-centered> <h1>Big Headline</h1> <p>Fits without scrolling.</p></section> <!-- Full-height app panel (dynamic, updates as chrome hides) --><section data-layout="cover" data-layout-min="100dvh"> ...</section>
The data-layout="reel" provides a horizontal scroll-snap container. It includes scroll-snap-type: x mandatory, momentum scrolling, and hidden scrollbars. Use it for card feeds, feature showcases, and image galleries.
<section data-layout="reel" data-layout-gap="m" data-layout-item-width="m"> <layout-card>Card 1</layout-card> <layout-card>Card 2</layout-card> <layout-card>Card 3</layout-card> <layout-card>Card 4</layout-card></section>
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
env(safe-area-inset-*) |
69+ | 65+ | 11.2+ | 79+ |
dvh / svh |
108+ | 101+ | 15.4+ | 108+ |
overscroll-behavior |
63+ | 59+ | 16+ | 18+ |
scroll-snap-type |
69+ | 68+ | 11+ | 79+ |
@media (pointer: coarse) |
41+ | 64+ | 9+ | 12+ |
Invokers API (commandfor) |
135+ | No | No | 135+ |
All core features have 95%+ support on current mobile browsers. The Invokers API is the newest; for broader support, use element.showModal() as a JavaScript fallback.
Elements can reveal as they enter the viewport using CSS scroll-driven animations. No JavaScript required — the browser handles timing, throttling, and compositing.
<section data-animate="fade-up"> <h2>This fades up as it scrolls into view</h2></section>
| Value | Effect |
|---|---|
fade-up | Fades in while translating up from 2rem |
fade-in | Opacity fade only |
slide-left | Fades in while translating from 2rem right |
scale-up | Fades in from 95% scale |
Uses animation-timeline: view() (Chrome 115+, Safari 18+). Unsupported browsers show elements statically — no content is hidden. Animations are disabled when prefers-reduced-motion: reduce is active.
See this in action across the corporate site demo:
Headers can hide on scroll-down and reappear on scroll-up — the same pattern as Instagram and Medium.
<header data-scroll-hide> <nav>...</nav></header>
The CSS adds a smooth translateY(-100%) transition. The JS (auto-loaded when the attribute is present) toggles a data-hidden attribute based on scroll direction.
Contrast with data-scroll-shrink, which shrinks the header but keeps it visible.
Two patterns for keeping a call-to-action always reachable:
<footer data-sticky-cta> <p>Start free — no credit card needed</p> <a href="/signup" class="btn primary">Get started</a></footer>
<button class="fab" aria-label="Get started">+</button>
Both respect safe-area bottom insets automatically.
Phase 3 of the mobile strategy adds keyboard-aware scrolling and optimized form layout for checkout-grade flows.
Add data-keyboard-aware to a form and inputs will auto-scroll into view when the virtual keyboard opens:
<form class="mobile" data-keyboard-aware novalidate> <form-field> <label for="email">Email</label> <input type="email" id="email" autocomplete="email" enterkeyhint="next"> </form-field> <footer class="sticky"> <button type="submit">Submit</button> </footer></form>
The form.mobile class forces single-column stacking below 48rem and full-width submit buttons on touch devices. The footer.sticky sticks above the virtual keyboard using env(keyboard-inset-height).
Combine form.mobile with data-wizard for step-by-step flows:
See the side-by-side comparison of common form mistakes vs best practices:
Every mobile demo rendered inside realistic device mockups — iPhone, Pixel, Galaxy, and iPad.