Skip to content
omaris

Containment

Device Mockup

A device drawn around whatever you put on its screen.

import { DeviceMockup } from 'omaris'
Learn

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.

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

Phone
Android
Tablet
Laptop
Browser
Watch
<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.

graphite
silver
gold
sky
porcelain
<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.

<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 device and a finish, 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 for tablet, 1440 for laptop), however small the mockup is drawn.
  • A responsive preview. Drive device and orientation from a Segmented Button; the snippet is told the width it is laid out at.
  • A live site. src loads it in an iframe at the device's viewport, so it lays out, scrolls and navigates like it would on the hardware.
  • A screenshot. image fills 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 a w-* / 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 as safe.
  • 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 with sandbox="allow-scripts allow-same-origin".
  • Add inert when 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. orientation only applies to phone, android and tablet.
  • Point src at the page the mockup is on. The iframe loads that page, mockup and all.
  • Load several live sites on one page. Each src is a full browsing context. Use image for the ones that are only looked at.

Quick reference

finish
  • graphite (default)
  • silver
  • gold
  • sky
  • porcelain

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.

graphite
silver
gold
sky
porcelain
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.