Skip to content

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.

  • 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-top and --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 use touch-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.

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.

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.

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.

  • 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% with min-height: 0 and 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, autocapitalize and autocomplete to 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;
}
}

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 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.

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.

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.