Containment
Device Mockup
A device drawn around whatever you put on its screen.
import { DeviceMockup } from 'omaris' Examples
Basic
A phone with your own components on the screen. The screen is laid out at the phone's real width — 390 CSS px — however small the picture is drawn, and it scrolls when there is more than fits.
9:41
<script lang="ts"> import { DeviceMockup, Text } from 'omaris'; const TILES = [ { label: 'Orders', value: '128' }, { label: 'Refunds', value: '4' }, { label: 'Visitors', value: '2,904' }, { label: 'Signups', value: '61' } ];</script><DeviceMockup classes={{ screen: 'bg-surface-container-low' }}> <div class="flex flex-col gap-4 px-5 pt-2 pb-6"> <Text variant="headline-md" as="h2">Today</Text> <div class="rounded-shape-lg bg-primary-container p-4 text-primary-container-foreground"> <Text variant="label-md">Revenue</Text> <Text variant="display-sm" tabular>$48,210</Text> </div> <div class="grid grid-cols-2 gap-3"> {#each TILES as tile (tile.label)} <div class="rounded-shape-lg bg-surface-container p-4"> <Text variant="label-md" tone="muted">{tile.label}</Text> <Text variant="title-lg" tabular>{tile.value}</Text> </div> {/each} </div> </div></DeviceMockup> Every device
Six frames, each fitted to its own column with nothing measured by hand. Every one carries the real viewport of the thing it draws, so the same markup wraps like a phone in the phone and like a desktop in the laptop.
9:41
10:09
<script lang="ts"> import { DeviceMockup, DEVICE_IDS, DEVICES, Text } from 'omaris';</script><div class="grid grid-cols-2 items-end gap-x-6 gap-y-10 sm:grid-cols-3"> {#each DEVICE_IDS as id (id)} <figure class="flex flex-col items-center gap-3"> <DeviceMockup device={id} classes={{ screen: 'bg-surface-container-low' }}> {#snippet children({ width, height })} <div class="grid size-full place-items-center p-4 text-center"> <Text variant="title-lg" as="p" tabular>{width} × {height}</Text> </div> {/snippet} </DeviceMockup> <Text as="figcaption" variant="label-md" tone="muted">{DEVICES[id].label}</Text> </figure> {/each}</div> Finishes
Five finishes. A wallpaper runs under the status bar and the island rather than around them, so safeArea={false} puts the clock back over the picture — and a light text-* on the glass turns the clock white with it.
9:41
9:41
9:41
9:41
9:41
<script lang="ts"> import { DeviceMockup, Text, type DeviceFinish } from 'omaris'; const FINISHES: DeviceFinish[] = ['graphite', 'silver', 'gold', 'sky', 'porcelain'];</script><div class="grid grid-cols-2 items-end gap-6 sm:grid-cols-5"> {#each FINISHES as finish (finish)} <figure class="flex flex-col items-center gap-3"> <DeviceMockup {finish} width={180} safeArea={false} classes={{ screen: 'bg-linear-to-br from-info to-tertiary text-white' }} > <div class="grid size-full place-items-center"> <Text variant="display-lg" tabular>9:41</Text> </div> </DeviceMockup> <Text as="figcaption" variant="label-md" tone="muted">{finish}</Text> </figure> {/each}</div> Responsive preview
A responsive preview anyone can drive: pick a device, turn it over. The snippet is told the width it is laid out at, and turning a phone changes the geometry rather than the picture — the viewport becomes 844 × 390, the island moves to the side and the content stays upright.
9:41
<script lang="ts"> import { DeviceMockup, SegmentedButton, Text, type DeviceId, type DeviceOrientation } from 'omaris'; let device = $state<DeviceId>('phone'); let orientation = $state<DeviceOrientation>('portrait');</script><div class="flex flex-col items-center gap-5"> <div class="flex flex-wrap justify-center gap-3"> <SegmentedButton label="Device" mandatory value={device} onchange={(value) => (device = value as DeviceId)} items={[ { value: 'phone', label: 'Phone' }, { value: 'tablet', label: 'Tablet' }, { value: 'browser', label: 'Browser' } ]} /> <SegmentedButton label="Orientation" mandatory value={orientation} onchange={(value) => (orientation = value as DeviceOrientation)} items={[ { value: 'portrait', label: 'Portrait' }, { value: 'landscape', label: 'Landscape', disabled: device === 'browser' } ]} /> </div> <DeviceMockup {device} {orientation} width={orientation === 'landscape' || device === 'browser' ? 560 : 300} classes={{ screen: 'bg-surface-container-low' }} > {#snippet children({ width })} <div class="grid gap-3 p-5 {width < 600 ? 'grid-cols-1' : 'grid-cols-3'}"> <Text variant="headline-sm" as="h2" class="col-span-full">{width} px across</Text> {#each ['Orders', 'Visitors', 'Signups'] as tile (tile)} <div class="rounded-shape-lg bg-surface-container p-4"> <Text variant="label-md" tone="muted">{tile}</Text> <Text variant="title-lg" tabular>128</Text> </div> {/each} </div> {/snippet} </DeviceMockup></div> A website inside it
A real site in a real iframe, at the device's own viewport: scroll it, follow a link. The browser window's address bar reads the site's host.
<script lang="ts"> import { DeviceMockup } from 'omaris';</script><DeviceMockup device="browser" src="/" url="omaris.dev" label="The omaris home page" /> Overridden
Every part is reachable. The frame is a colour of its own, the glass carries its own background, the width is a class — and the screen is inert, because this one is a picture rather than something to use.
<script lang="ts"> import { DeviceMockup, Text } from 'omaris';</script><DeviceMockup device="tablet" inert class="w-80" classes={{ frame: 'bg-primary', button: 'bg-primary', screen: 'bg-primary-container text-primary-container-foreground' }}> {#snippet children({ width, height })} <div class="grid size-full place-items-center text-center"> <div> <Text variant="headline-sm" as="p" tabular>{width} × {height}</Text> <Text variant="label-md">the viewport this snippet is laid out in</Text> </div> </div> {/snippet}</DeviceMockup> When to use it
Use it for
- Showing a screen in the device it runs on: a phone on a marketing page, a tablet in a case study, a laptop above the fold. Pick a
deviceand afinish, and put your own components inside it. - Previewing your own UI at a real device width. The screen is laid out at the device's viewport (390 CSS px for
phone, 834 fortablet, 1440 forlaptop), however small the mockup is drawn. - A responsive preview. Drive
deviceandorientationfrom a Segmented Button; the snippet is told thewidthit is laid out at. - A live site.
srcloads it in an iframe at the device's viewport, so it lays out, scrolls and navigates like it would on the hardware. - A screenshot.
imagefills the glass edge to edge.
Not for
- One screenshot with nothing to say about the hardware → a plain
<img>, or Image Viewer when it should open. - A phone-shaped panel in your app's own layout, like a preview column → Card or Resizable. A drawn frame around real application UI reads as a mistake.
- A gallery of screenshots → Carousel or Masonry, with one mockup as the hero above it.
- Testing a real viewport while you build. Use the browser's device toolbar.
Do
- Let it size itself. Each device has a width it looks right at and it never grows past its container. Set
width(or aw-*/max-w-*class) when the layout needs a particular size. - Give the glass the background:
classes={{ screen: 'bg-surface-container-low' }}. It runs edge to edge behind the status bar and the island, while your content sits between them and scrolls when it is taller than the screen. - Put a light
text-*on the glass over a dark wallpaper. The clock and the battery follow the screen's text colour. - Use
safeArea={false}for a wallpaper or hero picture that should run under the hardware. The insets are on the glass as--device-safe-top(and-right,-bottom,-left) and in the snippet assafe. - Give a site or a screenshot a name with
label: it becomes the iframe's title and the image's alt text. Sandbox a site you do not control withsandbox="allow-scripts allow-same-origin". - Add
inertwhen the mockup is decoration, so it takes no clicks, focus or scrolls.
Don't
- Set a height on it. The height follows from the width and the device; size it by width only.
- Turn a laptop, a browser window or a watch sideways.
orientationonly applies tophone,androidandtablet. - Point
srcat the page the mockup is on. The iframe loads that page, mockup and all. - Load several live sites on one page. Each
srcis a full browsing context. Useimagefor the ones that are only looked at.
Quick reference
finish graphite(default)silvergoldskyporcelain
The material the body is made of.
API
DeviceMockup
A device drawn around whatever you put on its screen.
Six frames — phone, android, tablet, laptop, browser and watch — in five finishes, the handhelds in either orientation. The screen holds your own markup, a screenshot (image) or a live site (src).
The screen is the device's real viewport. The phone's glass is 390 CSS px wide, the tablet's 834, the laptop's 1440 — content is laid out at that size and the whole drawing is then scaled as one picture. A snippet wraps as it would on the hardware and a site in the iframe gets its mobile layout, however small the mockup is drawn.
It sizes itself. Each device has a width it looks right at, and it never grows wider than its container. width changes the first; a max-w-* or w-* class works too.
Content stays clear of the hardware. The status bar, the island and the home indicator are drawn over the glass, and the content box sits between them, scrolling when it is taller. Give the glass a colour with classes={{ screen: '…' }} and it runs edge to edge behind them.
import { DeviceMockup } from 'omaris' <DeviceMockup device="phone"> <MyScreen /></DeviceMockup><DeviceMockup device="browser" src="https://example.com" label="Example" /> Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
root- The box the drawing takes up on the page.
frame- The body: the metal rail and the bezel inside it, or a window.
screen- The glass. Everything on it is clipped to its corners.
content- The box your content is laid out in — clear of the hardware, scrolling.
status- The clock and the signal, wi-fi and battery glyphs across the top.
cutout- The island or the punch-hole.
button- A physical key, standing proud of the rail.
stand- The laptop's base, the watch's straps.
chrome- The browser window's title bar.
Props
device Defaults to 'phone'
DeviceId Which frame to draw.
orientation Defaults to 'portrait'
DeviceOrientation Turned sideways. The laptop, the browser and the watch ignore it.
finish Defaults to 'graphite'
DeviceFinish The body's colour. The browser window follows the theme instead.
graphitesilvergoldskyporcelain
width number | string How wide to draw it — px, or any CSS length. It still never grows past its container. Defaults to a width that suits the device.
image string A screenshot to fill the screen with, edge to edge.
src string A site to load on the screen, at the device's own viewport.
label string Accessible name: the iframe's title, the image's alt text.
sandbox string sandbox for the iframe. Set it for a site you do not control.
allow string allow for the iframe — clipboard, camera, fullscreen.
url string What the browser window's address bar reads. Defaults to src's host.
statusBar Defaults to true
boolean The clock and battery across the top of a phone, tablet or watch.
safeArea Defaults to true
boolean Keep content clear of the status bar, the island and the home indicator. Off, it runs under them — for a wallpaper or a full-bleed picture. An image is always full bleed.
class string classes DeviceMockupClasses children Snippet<[DeviceScreenContext]> What goes on the screen. Takes the screen's size, if it wants it.