---
title: Mockup
description: Frame a screenshot or a live composition in a browser, window, code, phone, or tablet shell
visibility: guest
draft: false
---

# Mockup

A picture frame for a screenshot — or, better, for a **live** composition. Wrap
anything in a browser window, a desktop window, a code editor, a phone or a
tablet, and it reads as a product shot.

The frame is deliberately thin and neutral: it is made entirely of design
tokens, so it restyles with your theme instead of being a hardcoded drawing of
someone else's window chrome. The content inside is the point.

## Basic Usage

:::preview{title="A browser frame"}
<x-wirekit::mockup url="app.example.com">
    <x-wirekit::stack gap="sm" style="padding: 1rem;">
        <x-wirekit::heading :level="3" size="lg">Invoices</x-wirekit::heading>
        <x-wirekit::text variant="muted">12 open · 3 overdue</x-wirekit::text>
    </x-wirekit::stack>
</x-wirekit::mockup>
:::

## Frame a live composition, not a picture

The best use is not a PNG. Put **real components** inside and the shot can never
go stale — it restyles with the theme and stays in sync with the library.

:::preview{title="Real components inside the frame"}
<x-wirekit::mockup variant="window" title="Dashboard">
    <x-wirekit::stack gap="md" style="padding: 1rem;">
        <x-wirekit::row gap="sm" align="center">
            <x-wirekit::badge intent="success">Live</x-wirekit::badge>
            <x-wirekit::text size="sm" variant="muted">Updated just now</x-wirekit::text>
        </x-wirekit::row>
        <x-wirekit::progress :value="72" label="Storage used" show-value />
    </x-wirekit::stack>
</x-wirekit::mockup>
:::

## Code

The `code` frame gives a snippet a filename tab. Pair it with
[Code Block](/components/code-block).

:::preview{title="A code frame"}
<x-wirekit::mockup variant="code" title="TeamController.php">
    <x-wirekit::code-block language="php">public function index(): View
{
    return view('teams.index', [
        'teams' => auth()->user()->teams,
    ]);
}</x-wirekit::code-block>
</x-wirekit::mockup>
:::

## Devices

`phone` and `tablet` swap the chrome bar for a device shell.

:::preview{title="A phone frame"}
<x-wirekit::mockup variant="phone">
    <x-wirekit::stack gap="sm" style="padding: 1.5rem 1rem 1rem;">
        <x-wirekit::heading :level="3" size="md">Today</x-wirekit::heading>
        <x-wirekit::text size="sm" variant="muted">3 tasks left</x-wirekit::text>
    </x-wirekit::stack>
</x-wirekit::mockup>
:::

:::preview{title="A tablet frame"}
<x-wirekit::mockup variant="tablet">
    <x-wirekit::stack gap="sm" style="padding: 1.5rem;">
        <x-wirekit::heading :level="3" size="md">Dashboard</x-wirekit::heading>
        <x-wirekit::text size="sm" variant="muted">Everything at a glance.</x-wirekit::text>
    </x-wirekit::stack>
</x-wirekit::mockup>
:::

## Screenshots

When you do frame an image, use [Image](/components/image) so it stays CLS-safe
and keeps its alt text:

```blade
{{-- 1. The frame is decoration; the image carries the meaning --}}
<x-wirekit::mockup url="app.example.com">
    <x-wirekit::image
        src="/screenshots/dashboard.png"
        alt="The dashboard showing 12 open invoices"
        :ratio="16/9"
    />
</x-wirekit::mockup>
```

## Sizing

A mockup has no size prop — you control its width the way you would any block:
a `max-width` (or a Tailwind width class) on the component itself.

```blade
{{-- 1. A browser / window / code frame fills its container — cap it for a centered demo --}}
<x-wirekit::mockup url="app.example.com" style="max-width: 40rem; margin-inline: auto;">…</x-wirekit::mockup>

{{-- 2. Tailwind width utilities work just as well --}}
<x-wirekit::mockup variant="code" title="Example.php" class="max-w-2xl mx-auto">…</x-wirekit::mockup>
```

The `phone` and `tablet` device frames ship with **default sizes** so they read as
a device even around a small snippet: the phone is `15rem` wide with a `30rem`
minimum height, the tablet `22rem` wide with a `28rem` minimum height, and the
browser / window / code frames fill their container up to `34rem`. Override the
width when you want a different scale:

```blade
{{-- 3. A wider phone frame --}}
<x-wirekit::mockup variant="phone" style="width: 18rem;">…</x-wirekit::mockup>
```

The device proportions are chosen to *read* as a phone or tablet, not to match any
specific model — the tablet, for example, is a little squarer than a real one.

::: info Screenshot chrome, not a viewport
A device frame is decorative — it is **not** a viewport. The screen is an ordinary
element on the page, so media queries inside it (`@media (max-width: 640px)`, a
container query) resolve against the **page** width, not the frame. Reach for it to
present a composition or a screenshot; it will not tell you how a layout responds at
a phone width. The frame also **grows** to fit tall content rather than clipping it,
which is why its height is a minimum, not a fixed device height.
:::

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | string | `'browser'` | `browser`, `window`, `code`, `phone`, `tablet` |
| `url` | string\|null | `null` | Address shown in the `browser` frame's bar |
| `title` | string\|null | `null` | Title / filename shown in the `window` and `code` bars |
| `scope` | string\|null | `null` | Scoped personalization name |

## Accessibility

- **All chrome is decorative and `aria-hidden`** — the traffic lights, the
  address bar, the notch. A screen-reader user is never told about fake window
  buttons they cannot press.
- **The lights are inert spans, not buttons.** Rendering them as buttons would
  put three dead controls in the tab order of every marketing page.
- The slot content carries every bit of the meaning — so frame real components,
  or an [Image](/components/image) with real alt text.
- The address bar is not a URL a user can act on; do not rely on it to
  communicate anything the page does not also say in text.

## Keyboard Interaction

The frame is presentational and does not respond to keyboard input. Anything you
place inside keeps its own keyboard behavior.

## Pitfalls

- **Do not fake a product you do not have.** A frame full of invented UI is the
  oldest tell of a generated landing page. Frame the real thing.
- **Do not put an address bar on a non-browser frame.** A `url` on a `window` or
  `phone` frame is ignored on purpose — it would be a lie.
- **Do not treat the lights as controls.** They are a genre convention, drawn
  neutral rather than red/amber/green precisely so they do not read as status.

## Design Tokens

| Element | Token |
| --- | --- |
| Frame surface | `--color-wk-bg-elevated` |
| Frame border | `--color-wk-border`, `--border-wk-width` |
| Frame radius | `--radius-wk-lg` |
| Frame shadow | `--shadow-wk-md` |
| Chrome bar | `--color-wk-bg-muted` |
| Label text | `--text-wk-xs`, `--color-wk-text-muted` |
| Device shell | `--color-wk-bg-inverse` |
| Code filename | `--font-wk-mono` |

Every value is a token — the frame follows your theme rather than drawing
someone else's chrome.

## Further Reading

- [MDN: CSS logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values)
