Make it work on iPhone
The iPhone app opens the same pop as the Mac. It is in TestFlight on iOS 26+ (see availability), so a pop you build for the Mac already opens on a phone. This guide makes it comfortable there.
What the iPhone host gives you
Section titled “What the iPhone host gives you”- The app is portrait-only. Plan for widths of about 320 to 430 CSS pixels.
- The editor runs edge to edge, under the status bar and the home indicator. Paint your
background to the edges and pad your root by
--pop-safe-topand--pop-safe-bottom(see fill the screen). - The keyboard covers the page, like Messages or Notes. The page keeps its size, and the host brings a focused field above the keyboard with no CSS from you (see above the keyboard).
- Your heading shares the top row with a native glass actions menu. Leave its upper-right corner free.
- The keyboard’s own toolbar moves between fields, and its ✓ dismisses the keyboard without submitting a form.
- Swipe from the leading edge to return to the library, or choose Back to Library in document actions.
- Page zoom and zoom-on-focus are off. Buttons, links and
role="button"elements usetouch-action: manipulation, and tap highlights are hidden. - Number inputs get a numeric keypad unless they set
inputmode.
You still design the layout, the touch targets and the haptics.
Know when you are on a phone
Section titled “Know when you are on a phone”Call getEnvironment() while a component initializes, the same way you call
getDocument. Read current inside reactive code:
<script lang="ts"> import { getEnvironment } from "@hitslop/pop/svelte"; const environment = getEnvironment(); const compact = $derived(environment.current.layout === "compact");</script>
{#if compact} <p>A composition for a narrow window.</p>{/if}layout is "compact" when the editor is under 640 CSS pixels wide and "regular"
otherwise. It follows the real window, so a narrow Mac window is compact too. platform
("ios", "macos", "unknown", …) and device ("phone", "tablet", "desktop",
"unknown") describe the host. Browser copies and older hosts report "unknown". Treat
them as presentation hints, not as permission checks.
The same values reach CSS as attributes on <html>:
html[data-pop-layout="compact"] .controls { flex-direction: column;}html[data-pop-platform="ios"] .tip { display: none;}data-pop-platform, data-pop-device and data-pop-layout exist in the live
editor only. Preview, icon, export and widget views never receive them and cannot call
getEnvironment(). Write base styles that work without the attributes; ordinary media
and container queries work too.
Put your heading beside the native menu
Section titled “Put your heading beside the native menu”Reserve the menu’s corner in your heading row:
.app-header { min-height: var(--pop-host-actions-height, 0px); padding-inline-end: calc(12px + var(--pop-host-actions-width, 0px));}The reservation is 64px wide and 44px high in the iPhone editor, and zero elsewhere.
Let long titles wrap.
Fixed controls and full-height sheets must also leave that corner free. A centered
dialog can use max-height: calc(100dvh - var(--pop-safe-top, 0px) - var(--pop-safe-bottom, 0px) - 2 * (var(--pop-host-actions-height, 0px) + 24px));
a bottom sheet can use calc(100dvh - var(--pop-safe-top, 0px) - var(--pop-host-actions-height, 0px) - 24px).
The iPhone preset exposes the reservation and simulates a notched phone’s safe areas
(47px top, 34px bottom), but native notch, swipe and keyboard behavior still need review
in the app. The preset has no keyboard, so --pop-keyboard-inset
stays 0px there. Preview, export, icon and widget output have no native
menu reservation.
Fill the screen
Section titled “Fill the screen”The iPhone editor runs edge to edge: your page extends under the status bar (and notch)
at the top and the home indicator at the bottom. Paint the background on the element that
fills the page, or on html and body, so those strips match your app. Then pad that
element by --pop-safe-top and --pop-safe-bottom, the heights of those areas. They are
0px everywhere except the iPhone editor, and the iPhone preset simulates them.
html[data-pop-platform="ios"][data-pop-device="phone"]:not([data-pop-capture]) .app { padding-top: calc(var(--pop-safe-top, 0px) + 12px); padding-bottom: var(--pop-safe-bottom, 0px);}Give fixed bars the same padding on the edge they touch. The bottom inset already leaves room around the home indicator, so a bottom bar sits right on it.
Lay it out for a thumb
Section titled “Lay it out for a thumb”- Fluid widths. Use grid, flex and
minmax(0, 1fr)rather than fixed desktop sizes. Let text wrap. Check 320, 390 and 430 pixels. - Fill the editor, scroll inside it. Use
height: 100%withmin-height: 0and give long content its own scrolling pane. - Keep the main action reachable. Put submit and cancel in a footer that sticks to
the bottom of its scroller, and lift anything pinned to the page’s bottom edge by
--pop-keyboard-inset. - Bound your sheets. Cap a dialog with
max-height: calc(100dvh - 24px)and let its content scroll. - Make targets 44px. The icon can be smaller than the hit area. Prefer visible buttons to hover-only controls.
- Use 16px text in inputs. Set
inputmode,enterkeyhint,autocapitalizeandautocompleteto match each field. - Keep focus visible. After a layout change, make sure the focused control is still on screen. Repeated-entry tools can keep focus after Add; a destructive or unrelated action should not raise the keyboard.
- Leave touch alone. Do not cancel touch events or text selection globally.
Keep bottom-pinned controls above the keyboard
Section titled “Keep bottom-pinned controls above the keyboard”The keyboard covers the bottom of the page and the page keeps its size, so headers do not squash and content does not reflow. You do not need any CSS for fields: when one gains focus, the host scrolls its container until the field sits above the keyboard, adds room at the end of that container when it is too short, and only if nothing can scroll shifts the page up just enough. It undoes all of this when the keyboard hides, and never moves a field you are typing in.
What the host cannot know is which of your controls are pinned to the bottom of the
screen. For those it publishes --pop-keyboard-inset on the root: the height, in CSS
pixels, of the bottom edge the keyboard and its toolbar cover. It is 0px while the
keyboard is hidden and in every other presentation, including previews, exports and Mac,
so always give it a fallback. Lift bottom sheets, fixed bars and centered dialogs that hold
fields by it. Never add it to the page or a layout container: that shrinks the layout
exactly like a resize. The SDK notice lifts automatically.
html[data-pop-platform="ios"][data-pop-device="phone"]:not([data-pop-capture]) { .sheet { bottom: var(--pop-keyboard-inset, 0px); max-height: calc(100dvh - var(--pop-host-actions-height, 0px) - var(--pop-keyboard-inset, 0px) - 24px); }}A sticky footer inside a scrolling sheet, from Little Days:
@media (max-width: 639px) { .bj-dialog input:not([type="file"]), .bj-dialog textarea { font-size: 16px; } .bj-form-footer { position: sticky; bottom: -16px; background: var(--pop-surface); padding-block: 12px 16px; z-index: 2; }}Sheets and menus
Section titled “Sheets and menus”surfaceTransition, overlayTransition and popoverTransition from @hitslop/pop/svelte
are Svelte transitions that honor reduced motion. Use surfaceTransition with
kind: "sheet" for a bottom sheet (the default kind is "dialog"),
overlayTransition for the dimming layer and popoverTransition for menus:
<Dialog.Overlay forceMount> {#snippet child({ props, open })} {#if open}<div {...props} class="overlay" transition:overlayTransition></div>{/if} {/snippet}</Dialog.Overlay><Dialog.Content forceMount> {#snippet child({ props, open })} {#if open} <div {...props} class="sheet" transition:surfaceTransition={{ kind: compact ? "sheet" : "dialog" }}> ... </div> {/if} {/snippet}</Dialog.Content>Use Bits UI dialogs and let them manage focus. When keyboard entry is optional, focus the sheet itself rather than a text field, so opening it does not raise the keyboard.
Haptics
Section titled “Haptics”haptics plays a short native tap for a deliberate action:
<script lang="ts"> import { haptics } from "@hitslop/pop/svelte";
async function save() { try { await doc.change(tx => addEntry(tx, input)); haptics.notification("success"); } catch { haptics.notification("error"); } }</script>| Call | Use it for |
|---|---|
haptics.selection() |
A changed choice: a tab, a segment, a menu pick. |
haptics.impact(style) |
A physical-feeling tap. style is "light" (the default), "medium", "heavy", "soft" or "rigid". |
haptics.notification(kind) |
An outcome: "success", "warning" or "error". |
Play feedback after an accepted write or a deliberate action, next to something visible.
Never call it from a document observer, while typing or while rendering, or from a view
that is not on screen. The calls return void: do not await them, and never make an
edit depend on one.
On Mac, in browsers and on older hosts the calls do nothing, and no capability or
permission is involved. People can turn Haptic Feedback off in the iPhone app’s Settings. A
framework-neutral app can reach the same service as optional ctx.haptics.
Test it
Section titled “Test it”Run pop dev and choose iPhone in the Device menu. That sets a 390 by 844
viewport and reports ios and phone, so you can check your compact layout. Check the
320 and 430 pixel edges with your browser’s responsive design tools. The preview simulates layout and identity only. It has no
native keyboard, navigation bar or haptics, so check text entry, bottom-pinned controls
above the keyboard, sheets and dismissing the keyboard in the TestFlight app. Preview, icon, export and widget views render saved state
and ignore the device menu, so review them separately.
Because a document keeps its own app, test the change on a new document. Updated templates reach new copies only.
Worked examples
Section titled “Worked examples”Quick Checklist keeps its scroll budget for tasks. Below 640px it tightens padding,
gives the composer a 44px add button, sets inputs to 16px, gives menu items 44px rows and
caps menus at 100dvh, and lifts its bottom padding above the keyboard. It
plays haptics.selection() for tab and checkbox changes and
haptics.notification("success") after filing finished tasks. After removing a task it
keeps focus off the keyboard on a phone by returning it to a tab instead of the input.
Little Days uses layout === "compact" to put a two-column set of care actions above
the timeline. Its forms become bounded sheets with scrolling content and a sticky
footer, with one modal holding the overview, entry, profile and caregiver screens. Its sheet
rides above the keyboard, and takes focus without raising it. Haptics confirm a saved entry, flag a
refused one and accompany choices.
See style an app for themes and CSS, where your pop runs for the hosts, and capabilities and permissions for what the phone refuses.