Skip to content
Frames & Mocks primitive

Device Showcase

The product-hero arrangement: a handset overlapping a laptop, overlapped with a negative inline-start margin so both stay in flow and the group keeps an honest height. Stacks below sm instead of shrinking to nothing.

291 linesNo dependenciesAdded 13 Sept 2026
  • showcase
  • hero
  • devices
  • responsive
  • mockup

What's included

  • components/ui/device-showcase.tsx
  • components/ui/laptop-frame.tsx
  • components/ui/phone-frame.tsx
  • No runtime dependencies

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add device-showcase

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Live

Interactive, in your current theme. Every state below is the real component — type in it, tab through it, switch the theme.

12:30

Source

One file. Create components/ui/device-showcase.tsx and paste.

components/ui/device-showcase.tsx
/**
 * <DeviceShowcase> — a laptop with a handset overlapping it.
 *
 * The arrangement every product hero uses, and the one that is fiddly enough
 * that people paste a screenshot of it instead. Two things make it fiddly and
 * both are handled here:
 *
 * 1. **The overlap is a negative margin, not absolute positioning.** Absolute
 *    positioning takes the phone out of flow, so the container collapses to
 *    the laptop's height and the phone hangs over whatever is below it. A
 *    negative inline-start margin keeps both in flow and keeps the group's
 *    height honest.
 *
 * 2. **It stacks rather than shrinking.** Below `sm` the two frames sit one
 *    above the other with the overlap removed, because a laptop scaled to fit
 *    a 360px viewport has a screen 200px wide and shows nothing. Every
 *    image-based version of this arrangement simply gets smaller.
 *
 * The negative margin is `-ms-` and not `-ml-`, so the phone sits on the
 * correct side of the laptop in an RTL layout rather than off the far edge.
 */

import * as React from 'react'

import { LaptopFrame } from './laptop-frame'
import { PhoneFrame } from './phone-frame'

export interface DeviceShowcaseProps {
  /** What the laptop shows. */
  desktop?: React.ReactNode
  /** What the handset shows. */
  mobile?: React.ReactNode
  /** Which side the handset sits on. */
  phoneSide?: 'start' | 'end'
  /** Laptop width in px; the handset is sized from it. */
  width?: number
  className?: string
}

export function DeviceShowcase({
  desktop,
  mobile,
  phoneSide = 'end',
  width = 460,
  className = '',
}: DeviceShowcaseProps) {
  const phone = (
    <PhoneFrame
      width={Math.round(width * 0.3)}
      buttons={false}
      className={
        phoneSide === 'end'
          ? 'z-10 -ms-10 mt-10 hidden sm:block'
          : 'z-10 -me-10 mt-10 hidden sm:block'
      }
    >
      {mobile}
    </PhoneFrame>
  )

  return (
    <div className={`flex flex-col items-center gap-6 sm:flex-row sm:items-end sm:gap-0 ${className}`}>
      {phoneSide === 'start' ? phone : null}
      <LaptopFrame width={width}>{desktop}</LaptopFrame>
      {phoneSide === 'end' ? phone : null}

      {/* The stacked fallback. Rendered only below `sm`, where the overlapped
          copy above is hidden — two elements rather than one so neither
          layout has to fight the other's margins. */}
      <div className="sm:hidden">
        <PhoneFrame width={Math.round(width * 0.42)} buttons={false}>
          {mobile}
        </PhoneFrame>
      </div>
    </div>
  )
}

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
desktopWhat the laptop shows.React.ReactNode—
mobileWhat the handset shows.React.ReactNode—
phoneSideWhich side the handset sits on.'start' | 'end''end'
widthLaptop width in px; the handset is sized from it.number460
classNamestring''

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

More frames & mocks

View all
Open the full page for this primitive

Browser Frame

Desktop browser chrome that wraps whatever you put inside it — a screenshot, a video, or the live component itself. Three variants, no image asset, and the window controls are grey rather than a copy of one vendor’s.

Frames & Mocks139 lines1 dep
Open the full page for this primitive

Phone & Tablet Frame

One handset bezel with the cut-out as a prop, so iPhone, Android and tablet are three settings rather than three assets. Status bar and home indicator included, and the clock is a prop so it can never be a hydration mismatch.

Frames & Mocks145 linesNo deps
Open the full page for this primitive

Laptop Frame

A lid, a hinge and a tapered base, with the taper done by clip-path rather than the border trick that cannot round or carry a background. Degrades to a boxier laptop where clip-path is missing.

Frames & Mocks68 linesNo deps