macbetter

Navigation

Device Frame

Present an iPhone or iPad layout inside a page.

iOS only

Today

  • Book the offsite venue
  • Send Q3 metrics to Kofi
  • Review the onboarding flow
import { DeviceFrame } from "macbetter/device-frame"

const reminders = ["Book the offsite venue", "Send Q3 metrics to Kofi", "Review the onboarding flow"]

export default function Example() {
  return (
    <DeviceFrame scale={0.78} role="group" aria-label="Reminders on iPhone">
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          paddingTop: "calc(var(--mb-safe-top) + 8px)",
          paddingBottom: "var(--mb-safe-bottom)",
          background: "var(--mb-bg-grouped)",
        }}
      >
        <h2 className="mb-text-large-title" style={{ margin: 0, padding: "0 16px 8px" }}>
          Today
        </h2>
        <ul
          style={{
            listStyle: "none",
            margin: "0 16px",
            padding: 0,
            borderRadius: 10,
            background: "var(--mb-bg-grouped-secondary)",
          }}
        >
          {reminders.map((reminder, index) => (
            <li
              key={reminder}
              className="mb-text-body"
              style={{
                padding: "11px 16px",
                boxShadow: index ? "inset 0 0.5px 0 var(--mb-separator)" : undefined,
              }}
            >
              {reminder}
            </li>
          ))}
        </ul>
      </div>
    </DeviceFrame>
  )
}

When to use it

Use a device frame to show an iPhone or iPad layout on a page people read on a computer: a product tour, a design review, or a live preview beside a settings panel. The screen inside the frame switches to the iOS or iPadOS platform, so every macbetter component in it takes the touch metrics on its own.

The frame is presentation, not emulation. It draws the hardware and publishes its safe areas; it doesn't rotate, scroll or respond to the home indicator. On a real phone, render your layout without a frame: the same safe-area tokens then read the device's own insets.

To present a Mac app, use a window.

Usage

import { DeviceFrame } from "macbetter/device-frame"

iPhone

The default is an iPhone 16: a 393 × 852 point screen with the Dynamic Island, status bar and home indicator. Its child fills the screen. Use scale to fit it into a column; 0.78 fits it into a 390 point preview.

iOS only

Today

  • Book the offsite venue
  • Send Q3 metrics to Kofi
  • Review the onboarding flow
import { DeviceFrame } from "macbetter/device-frame"

const reminders = ["Book the offsite venue", "Send Q3 metrics to Kofi", "Review the onboarding flow"]

export default function Example() {
  return (
    <DeviceFrame scale={0.78} role="group" aria-label="Reminders on iPhone">
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          paddingTop: "calc(var(--mb-safe-top) + 8px)",
          paddingBottom: "var(--mb-safe-bottom)",
          background: "var(--mb-bg-grouped)",
        }}
      >
        <h2 className="mb-text-large-title" style={{ margin: 0, padding: "0 16px 8px" }}>
          Today
        </h2>
        <ul
          style={{
            listStyle: "none",
            margin: "0 16px",
            padding: 0,
            borderRadius: 10,
            background: "var(--mb-bg-grouped-secondary)",
          }}
        >
          {reminders.map((reminder, index) => (
            <li
              key={reminder}
              className="mb-text-body"
              style={{
                padding: "11px 16px",
                boxShadow: index ? "inset 0 0.5px 0 var(--mb-separator)" : undefined,
              }}
            >
              {reminder}
            </li>
          ))}
        </ul>
      </div>
    </DeviceFrame>
  )
}

iPad

Set device="ipad" for an 11-inch iPad Air, landscape by default. Its status bar shows the date beside the time. Set orientation="portrait" to turn it.

iPadOS only

Today

3 reminders

import { DeviceFrame } from "macbetter/device-frame"
import { Sidebar, SidebarItem, SidebarSection } from "macbetter/sidebar"
import { CalendarIcon, InboxIcon, StarIcon, SunIcon } from "lucide-react"

export default function Example() {
  return (
    <DeviceFrame device="ipad" scale={0.56} role="group" aria-label="Reminders on iPad">
      <div style={{ display: "flex", minHeight: 0 }}>
        <Sidebar
          aria-label="Lists"
          style={{ width: 320, paddingTop: "calc(var(--mb-safe-top) + 16px)", paddingBottom: "var(--mb-safe-bottom)" }}
        >
          <SidebarSection title="Reminders">
            <SidebarItem href="#today" icon={<SunIcon />} badge={3} current>
              Today
            </SidebarItem>
            <SidebarItem href="#scheduled" icon={<CalendarIcon />} badge={7}>
              Scheduled
            </SidebarItem>
            <SidebarItem href="#flagged" icon={<StarIcon />} tint="var(--mb-orange)">
              Flagged
            </SidebarItem>
            <SidebarItem href="#all" icon={<InboxIcon />} badge={24}>
              All
            </SidebarItem>
          </SidebarSection>
        </Sidebar>
        <div style={{ flex: 1, padding: "calc(var(--mb-safe-top) + 16px) 32px var(--mb-safe-bottom)" }}>
          <h2 className="mb-text-large-title" style={{ margin: 0 }}>
            Today
          </h2>
          <p className="mb-text-body mb-label-secondary">3 reminders</p>
        </div>
      </div>
    </DeviceFrame>
  )
}

Safe areas

The screen publishes how much of each edge the hardware covers, as the same --mb-safe-* tokens that read env(safe-area-inset-*) on a real device. Pad your content with them and it clears the status bar and home indicator both inside a frame and on a phone:

.screen-header {
  padding-top: calc(var(--mb-safe-top) + 8px);
}

.screen-footer {
  padding-bottom: var(--mb-safe-bottom);
}

macbetter's bars do this for you, and leave insets of their own for the content beneath them:

VariableSet byRead by
--mb-safe-top, --mb-safe-bottom, --mb-safe-left, --mb-safe-righttokens.css (from env()), overridden by the device frame's screenNavigation bar, tab bar, sheets, your own layouts
--mb-content-inset-topNavigationStackList section headers, which stick below the bar instead of under it
--mb-content-inset-bottomTabBarNavigation stack and scrolling content, which leave room for the bar

The screen also sets --mb-safe-area-top and --mb-safe-area-bottom to the same values for code written before the shared tokens. Use --mb-safe-* in new code.

Platform behavior

  • iPhone: the screen nests data-platform="ios". The top inset is 59 points (the status bar and Dynamic Island), the bottom 34 (the home indicator).
  • iPad: the screen nests data-platform="ipados". The top inset is 24 points and the bottom 20.
  • The frame draws the same on every page platform, so an iPhone preview can sit inside a Mac page with the right type sizes, touch targets and controls.

Accessibility

  • The status bar, Dynamic Island and home indicator are hidden from assistive technology.
  • Give the frame role="group" and an aria-label such as "Mail on iPhone", so people know the content inside is a preview. It matters most when a page has several.
  • scale uses CSS zoom, so text inside stays crisp and hit targets scale with it.

API

DeviceFrame

PropTypeDefaultDescription
device"iphone" | "ipad""iphone"Which device to draw. Its screen nests the iOS or iPadOS platform.
orientation"portrait" | "landscape"portrait on iPhone, landscape on iPadThe iPad's orientation. iPhone frames are portrait.
scalenumber1Scales the whole device. 1 is actual size in points.
timestring"9:41"The status bar's clock.
datestring"Mon Jun 9"The date beside the clock on iPad.
statusBarbooleantrueShows the time, signal, Wi-Fi and battery.
homeIndicatorbooleantrueShows the home indicator.

It also takes the props of a div.

Data attributes and CSS variables

NameOnDescription
data-device.mb-device-frameiphone or ipad.
data-orientation.mb-device-frameportrait or landscape.
--mb-device-scale.mb-device-frameSet by scale.
--mb-device-screen-width, --mb-device-screen-height.mb-device-frameThe screen's size in points. Override both to draw another model.
--mb-device-screen-radius.mb-device-frameThe screen's corner radius.
--mb-device-safe-top, --mb-device-safe-bottom.mb-device-frameThe hardware insets the screen publishes.
--mb-safe-top, --mb-safe-bottom, --mb-safe-left, --mb-safe-right.mb-device-frame-screenThe shared safe-area tokens, set from the device.