More Layouts Snippets
Phone Mockup — Free CSS iPhone Device Frame Snippet
Phone Mockup · Layouts · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Phone Mockup — Pure-CSS Device Frame for App Screens

A phone mockup is a device frame you drop a screen design into — for landing pages, app-store shots, portfolio pieces, and design presentations. This one is built entirely in HTML and CSS (no image of a phone), so it's crisp at any size, theme-able, and you can put live, scrolling, interactive content inside it. It ships with a sample finance app screen and a live status-bar clock, in vanilla JavaScript with no dependency.
The frame from nested rounded rectangles
The body is a dark rounded box with heavy padding, and the screen is an inner rounded box — the gap between their border-radius values creates the realistic bezel curve where a smaller radius nested inside a larger one reads as a uniform-width rim. An inset box-shadow adds a subtle metal edge, and a large soft drop shadow lifts the whole device off the page. No device photo is involved, so it scales losslessly.
Dynamic island and hardware buttons
The notch is a single absolutely-positioned pill (.pm-island) centered at the top with a high z-index so it floats over the screen content. The volume and power buttons are thin absolutely-positioned slivers hanging off the frame's left and right edges — tiny details that sell the realism for the cost of three empty spans.
A real, scrollable screen — not a picture
Because the screen is live DOM, the app inside it actually works: overflow-y:auto makes the transaction list scroll within the device, the status bar shows a CSS-drawn battery and signal bars, and a toggle switches the screen to dark mode by swapping one class. This is the advantage of a CSS mockup over an image — you can demo genuine interaction, dark mode, and responsive content in context.
The CSS-drawn status bar
The signal bars are four <rect>s of increasing height in a tiny inline SVG, the wifi glyph is a stroked arc, and the battery is a bordered box with a nub pseudo-element (::after) and a percentage fill — all currentColor, so they invert automatically in dark mode. A live clock updates the time so it never looks frozen.
Reusing it as a wrapper
Replace the .pm-app contents with your own screen and the frame becomes a reusable container for any mobile UI. Keep the device markup as a component and pass children into the screen slot — ideal for showcasing a mobile login screen or onboarding flow on a marketing page.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA CSS phone renders with a sample app screen inside it.
- 2Scroll the screenThe transaction list scrolls inside the device frame.
- 3Watch the clockThe status-bar time updates live like a real phone.
- 4Toggle dark modeOne class swaps the screen between light and dark.
- 5Drop in your screenReplace the app contents with your own mobile UI.
- 6Reuse as a wrapperKeep the frame as a component with a screen slot.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Pure CSS. The device is a dark rounded box with padding, the screen is a nested rounded box, and the gap between their border-radius values forms the bezel. The notch and side buttons are positioned spans. Because there's no photo, it stays sharp at any zoom and you can recolor or resize it freely.
Yes — that's the main advantage over a screenshot. The screen is live DOM, so content scrolls with overflow-y:auto, the status-bar clock ticks, and a class toggle switches the screen to dark mode. You can embed forms, lists, animations, or any other snippet and it behaves normally within the frame.
By nesting two rounded rectangles. The outer frame has a large border-radius and padding; the inner screen has a slightly smaller radius. The constant-width gap between them, plus an inset box-shadow for a metal edge, reads as a uniform bezel — a standard trick for CSS device frames that avoids any images.
Replace everything inside the .pm-app element with your markup. The frame, status bar, and scrolling are independent of the content, so your screen just needs to fit the screen width. Keeping the device portion as a reusable component lets you pass different screens in as children.
Make the device frame a component that renders a screen slot (children, a slot, or ng-content) so you can wrap any screen. Move the live-clock interval into a mount effect with cleanup. Toggle dark mode via a state-bound class. In Tailwind, build the frame with rounded-[46px], padding, and shadow utilities, and the status-bar icons with small inline SVGs.
Phone Mockup — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Phone Mockup</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#e2e8f0;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.pm-stage{text-align:center}
.pm-phone{position:relative;width:270px;height:560px;background:#0b1220;border-radius:46px;padding:13px;box-shadow:0 30px 60px -20px rgba(15,23,42,.5),inset 0 0 0 2px #1e293b}
.pm-island{position:absolute;top:24px;left:50%;transform:translateX(-50%);width:88px;height:26px;background:#000;border-radius:14px;z-index:5}
.pm-btn{position:absolute;background:#0b1220;border-radius:3px}
.pm-vol-up{left:-3px;top:130px;width:3px;height:34px}
.pm-vol-dn{left:-3px;top:176px;width:3px;height:34px}
.pm-power{right:-3px;top:150px;width:3px;height:54px}
.pm-screen{position:relative;width:100%;height:100%;border-radius:34px;overflow:hidden;background:#f1f5f9;color:#0f172a;transition:background .3s,color .3s}
.pm-status{display:flex;justify-content:space-between;align-items:center;padding:14px 22px 4px;font-size:13px;font-weight:700;font-variant-numeric:tabular-nums}
.pm-status-r{display:flex;align-items:center;gap:5px;fill:currentColor}
.pm-batt{width:22px;height:11px;border:1.4px solid currentColor;border-radius:3px;position:relative;display:inline-block;opacity:.9}
.pm-batt::after{content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:currentColor;border-radius:0 1px 1px 0}
.pm-batt i{position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:62%;background:currentColor;border-radius:1px}
.pm-app{padding:8px 18px 18px;height:calc(100% - 36px);overflow-y:auto}
.pm-head{display:flex;align-items:center;justify-content:space-between;margin:8px 0 16px}
.pm-head small{font-size:12px;color:#94a3b8;font-weight:600}
.pm-head h2{font-size:20px;font-weight:800}
.pm-ava{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#22d3ee);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800}
.pm-balance{background:linear-gradient(135deg,#4f46e5,#7c3aed);color:#fff;border-radius:18px;padding:18px;text-align:left;margin-bottom:14px}
.pm-balance small{opacity:.8;font-size:12px}
.pm-balance strong{display:block;font-size:27px;font-weight:800;margin:3px 0}
.pm-up{font-size:12px;background:rgba(255,255,255,.2);padding:2px 8px;border-radius:99px}
.pm-actions{display:flex;gap:8px;margin-bottom:18px}
.pm-actions button{flex:1;border:none;background:#fff;color:#4338ca;border-radius:12px;padding:10px;font-size:12px;font-weight:700;font-family:inherit;cursor:pointer;box-shadow:0 4px 12px -6px rgba(0,0,0,.3)}
.pm-list{display:flex;flex-direction:column;gap:13px}
.pm-tx{display:flex;align-items:center;gap:11px}
.pm-ic{width:38px;height:38px;border-radius:11px;color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.pm-tx div{flex:1;text-align:left;min-width:0}
.pm-tx b{font-size:13.5px;font-weight:700}
.pm-tx small{display:block;font-size:11px;color:#94a3b8}
.pm-tx em{font-style:normal;font-weight:800;font-size:13px}
.pm-tx em.pos{color:#16a34a}
.pm-screen.dark{background:#0f172a;color:#e2e8f0}
.pm-screen.dark .pm-actions button{background:#1e293b;color:#a5b4fc}
.pm-screen.dark .pm-tx small,.pm-screen.dark .pm-head small{color:#64748b}
.pm-toggle{margin-top:22px;background:#0f172a;color:#fff;border:none;border-radius:9px;padding:9px 16px;font-size:12.5px;font-weight:700;cursor:pointer;font-family:inherit}
.pm-toggle:hover{background:#1e293b}
</style>
</head>
<body>
<div class="pm-stage">
<div class="pm-phone" id="pmPhone">
<span class="pm-island"></span>
<span class="pm-btn pm-vol-up"></span>
<span class="pm-btn pm-vol-dn"></span>
<span class="pm-btn pm-power"></span>
<div class="pm-screen">
<div class="pm-status">
<span id="pmTime">9:41</span>
<span class="pm-status-r">
<svg viewBox="0 0 18 12" width="17"><rect x="0" y="7" width="3" height="5" rx="1"/><rect x="4" y="5" width="3" height="7" rx="1"/><rect x="8" y="3" width="3" height="9" rx="1"/><rect x="12" y="1" width="3" height="11" rx="1" opacity=".35"/></svg>
<svg viewBox="0 0 16 12" width="15"><path d="M8 11.5 1 4.5a10 10 0 0 1 14 0z" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="8" cy="9.4" r="1.3"/></svg>
<span class="pm-batt"><i></i></span>
</span>
</div>
<div class="pm-app">
<header class="pm-head">
<div><small>Good morning</small><h2>Alex</h2></div>
<div class="pm-ava">A</div>
</header>
<div class="pm-balance">
<small>Total balance</small>
<strong>$12,480.55</strong>
<span class="pm-up">+2.4% this week</span>
</div>
<div class="pm-actions">
<button>Send</button><button>Request</button><button>Top up</button>
</div>
<div class="pm-list">
<div class="pm-tx"><span class="pm-ic" style="background:#6366f1">N</span><div><b>Netflix</b><small>Subscription</small></div><em>-$15.99</em></div>
<div class="pm-tx"><span class="pm-ic" style="background:#22c55e">S</span><div><b>Salary</b><small>Acme Inc</small></div><em class="pos">+$4,200</em></div>
<div class="pm-tx"><span class="pm-ic" style="background:#f59e0b">U</span><div><b>Uber</b><small>Transport</small></div><em>-$8.40</em></div>
<div class="pm-tx"><span class="pm-ic" style="background:#ec4899">D</span><div><b>Dribbble</b><small>Pro plan</small></div><em>-$60.00</em></div>
</div>
</div>
</div>
</div>
<button type="button" class="pm-toggle" id="pmToggle">Toggle dark screen</button>
</div>
<script>
var screen = document.querySelector('.pm-screen');
var timeEl = document.getElementById('pmTime');
// Live status-bar clock so the mockup feels real.
function tick() {
var d = new Date();
var h = d.getHours() % 12 || 12;
timeEl.textContent = h + ':' + ('0' + d.getMinutes()).slice(-2);
}
tick();
setInterval(tick, 10000);
document.getElementById('pmToggle').addEventListener('click', function () {
screen.classList.toggle('dark');
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Phone Mockup</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#e2e8f0;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
}
.pm-batt::after {
content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:currentColor;border-radius:0 1px 1px 0
}
.pm-batt i {
position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:62%;background:currentColor;border-radius:1px
}
.pm-head small {
font-size:12px;color:#94a3b8;font-weight:600
}
.pm-head h2 {
font-size:20px;font-weight:800
}
.pm-balance small {
opacity:.8;font-size:12px
}
.pm-balance strong {
display:block;font-size:27px;font-weight:800;margin:3px 0
}
.pm-actions button {
flex:1;border:none;background:#fff;color:#4338ca;border-radius:12px;padding:10px;font-size:12px;font-weight:700;font-family:inherit;cursor:pointer;box-shadow:0 4px 12px -6px rgba(0,0,0,.3)
}
.pm-tx div {
flex:1;text-align:left;min-width:0
}
.pm-tx b {
font-size:13.5px;font-weight:700
}
.pm-tx small {
display:block;font-size:11px;color:#94a3b8
}
.pm-tx em {
font-style:normal;font-weight:800;font-size:13px
}
.pm-tx em.pos {
color:#16a34a
}
.pm-screen.dark {
background:#0f172a;color:#e2e8f0
}
.pm-screen.dark .pm-actions button {
background:#1e293b;color:#a5b4fc
}
.pm-screen.dark .pm-tx small,.pm-screen.dark .pm-head small {
color:#64748b
}
</style>
</head>
<body>
<div class="text-center">
<div class="relative w-[270px] h-[560px] bg-[#0b1220] rounded-[46px] p-[13px] shadow-[0_30px_60px_-20px_rgba(15,23,42,.5),inset_0_0_0_2px_#1e293b]" id="pmPhone">
<span class="absolute top-6 left-1/2 [transform:translateX(-50%)] w-[88px] h-[26px] bg-[#000] rounded-[14px] z-[5]"></span>
<span class="absolute bg-[#0b1220] rounded-[3px] left-[-3px] top-[130px] w-[3px] h-[34px]"></span>
<span class="absolute bg-[#0b1220] rounded-[3px] left-[-3px] top-44 w-[3px] h-[34px]"></span>
<span class="absolute bg-[#0b1220] rounded-[3px] right-[-3px] top-[150px] w-[3px] h-[54px]"></span>
<div class="pm-screen relative w-full h-full rounded-[34px] overflow-hidden bg-[#f1f5f9] text-[#0f172a] [transition:background_.3s,color_.3s]">
<div class="flex justify-between items-center pt-3.5 px-[22px] pb-1 text-[13px] font-bold [font-variant-numeric:tabular-nums]">
<span id="pmTime">9:41</span>
<span class="flex items-center gap-[5px] fill-current">
<svg viewBox="0 0 18 12" width="17"><rect x="0" y="7" width="3" height="5" rx="1"/><rect x="4" y="5" width="3" height="7" rx="1"/><rect x="8" y="3" width="3" height="9" rx="1"/><rect x="12" y="1" width="3" height="11" rx="1" opacity=".35"/></svg>
<svg viewBox="0 0 16 12" width="15"><path d="M8 11.5 1 4.5a10 10 0 0 1 14 0z" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="8" cy="9.4" r="1.3"/></svg>
<span class="pm-batt w-[22px] h-[11px] border rounded-[3px] relative inline-block opacity-90"><i></i></span>
</span>
</div>
<div class="pt-2 px-[18px] pb-[18px] h-[calc(100% - 36px)] overflow-y-auto">
<header class="pm-head flex items-center justify-between mt-2 mx-0 mb-4">
<div><small>Good morning</small><h2>Alex</h2></div>
<div class="w-10 h-10 rounded-full [background:linear-gradient(135deg,#6366f1,#22d3ee)] text-[#fff] flex items-center justify-center font-extrabold">A</div>
</header>
<div class="pm-balance [background:linear-gradient(135deg,#4f46e5,#7c3aed)] text-[#fff] rounded-[18px] p-[18px] text-left mb-3.5">
<small>Total balance</small>
<strong>$12,480.55</strong>
<span class="text-xs bg-[rgba(255,255,255,.2)] py-0.5 px-2 rounded-[99px]">+2.4% this week</span>
</div>
<div class="pm-actions flex gap-2 mb-[18px]">
<button>Send</button><button>Request</button><button>Top up</button>
</div>
<div class="flex flex-col gap-[13px]">
<div class="pm-tx flex items-center gap-[11px]"><span class="w-[38px] h-[38px] rounded-[11px] text-[#fff] font-extrabold flex items-center justify-center shrink-0" style="background:#6366f1">N</span><div><b>Netflix</b><small>Subscription</small></div><em>-$15.99</em></div>
<div class="pm-tx flex items-center gap-[11px]"><span class="w-[38px] h-[38px] rounded-[11px] text-[#fff] font-extrabold flex items-center justify-center shrink-0" style="background:#22c55e">S</span><div><b>Salary</b><small>Acme Inc</small></div><em class="pos">+$4,200</em></div>
<div class="pm-tx flex items-center gap-[11px]"><span class="w-[38px] h-[38px] rounded-[11px] text-[#fff] font-extrabold flex items-center justify-center shrink-0" style="background:#f59e0b">U</span><div><b>Uber</b><small>Transport</small></div><em>-$8.40</em></div>
<div class="pm-tx flex items-center gap-[11px]"><span class="w-[38px] h-[38px] rounded-[11px] text-[#fff] font-extrabold flex items-center justify-center shrink-0" style="background:#ec4899">D</span><div><b>Dribbble</b><small>Pro plan</small></div><em>-$60.00</em></div>
</div>
</div>
</div>
</div>
<button type="button" class="mt-[22px] bg-[#0f172a] text-[#fff] border-0 rounded-[9px] py-[9px] px-4 text-[12.5px] font-bold cursor-pointer font-[inherit] hover:bg-[#1e293b]" id="pmToggle">Toggle dark screen</button>
</div>
<script>
var screen = document.querySelector('.pm-screen');
var timeEl = document.getElementById('pmTime');
// Live status-bar clock so the mockup feels real.
function tick() {
var d = new Date();
var h = d.getHours() % 12 || 12;
timeEl.textContent = h + ':' + ('0' + d.getMinutes()).slice(-2);
}
tick();
setInterval(tick, 10000);
document.getElementById('pmToggle').addEventListener('click', function () {
screen.classList.toggle('dark');
});
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to PhoneMockup.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#e2e8f0;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.pm-stage{text-align:center}
.pm-phone{position:relative;width:270px;height:560px;background:#0b1220;border-radius:46px;padding:13px;box-shadow:0 30px 60px -20px rgba(15,23,42,.5),inset 0 0 0 2px #1e293b}
.pm-island{position:absolute;top:24px;left:50%;transform:translateX(-50%);width:88px;height:26px;background:#000;border-radius:14px;z-index:5}
.pm-btn{position:absolute;background:#0b1220;border-radius:3px}
.pm-vol-up{left:-3px;top:130px;width:3px;height:34px}
.pm-vol-dn{left:-3px;top:176px;width:3px;height:34px}
.pm-power{right:-3px;top:150px;width:3px;height:54px}
.pm-screen{position:relative;width:100%;height:100%;border-radius:34px;overflow:hidden;background:#f1f5f9;color:#0f172a;transition:background .3s,color .3s}
.pm-status{display:flex;justify-content:space-between;align-items:center;padding:14px 22px 4px;font-size:13px;font-weight:700;font-variant-numeric:tabular-nums}
.pm-status-r{display:flex;align-items:center;gap:5px;fill:currentColor}
.pm-batt{width:22px;height:11px;border:1.4px solid currentColor;border-radius:3px;position:relative;display:inline-block;opacity:.9}
.pm-batt::after{content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:currentColor;border-radius:0 1px 1px 0}
.pm-batt i{position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:62%;background:currentColor;border-radius:1px}
.pm-app{padding:8px 18px 18px;height:calc(100% - 36px);overflow-y:auto}
.pm-head{display:flex;align-items:center;justify-content:space-between;margin:8px 0 16px}
.pm-head small{font-size:12px;color:#94a3b8;font-weight:600}
.pm-head h2{font-size:20px;font-weight:800}
.pm-ava{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#22d3ee);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800}
.pm-balance{background:linear-gradient(135deg,#4f46e5,#7c3aed);color:#fff;border-radius:18px;padding:18px;text-align:left;margin-bottom:14px}
.pm-balance small{opacity:.8;font-size:12px}
.pm-balance strong{display:block;font-size:27px;font-weight:800;margin:3px 0}
.pm-up{font-size:12px;background:rgba(255,255,255,.2);padding:2px 8px;border-radius:99px}
.pm-actions{display:flex;gap:8px;margin-bottom:18px}
.pm-actions button{flex:1;border:none;background:#fff;color:#4338ca;border-radius:12px;padding:10px;font-size:12px;font-weight:700;font-family:inherit;cursor:pointer;box-shadow:0 4px 12px -6px rgba(0,0,0,.3)}
.pm-list{display:flex;flex-direction:column;gap:13px}
.pm-tx{display:flex;align-items:center;gap:11px}
.pm-ic{width:38px;height:38px;border-radius:11px;color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.pm-tx div{flex:1;text-align:left;min-width:0}
.pm-tx b{font-size:13.5px;font-weight:700}
.pm-tx small{display:block;font-size:11px;color:#94a3b8}
.pm-tx em{font-style:normal;font-weight:800;font-size:13px}
.pm-tx em.pos{color:#16a34a}
.pm-screen.dark{background:#0f172a;color:#e2e8f0}
.pm-screen.dark .pm-actions button{background:#1e293b;color:#a5b4fc}
.pm-screen.dark .pm-tx small,.pm-screen.dark .pm-head small{color:#64748b}
.pm-toggle{margin-top:22px;background:#0f172a;color:#fff;border:none;border-radius:9px;padding:9px 16px;font-size:12.5px;font-weight:700;cursor:pointer;font-family:inherit}
.pm-toggle:hover{background:#1e293b}
`;
export default function PhoneMockup() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var screen = document.querySelector('.pm-screen');
var timeEl = document.getElementById('pmTime');
// Live status-bar clock so the mockup feels real.
function tick() {
var d = new Date();
var h = d.getHours() % 12 || 12;
timeEl.textContent = h + ':' + ('0' + d.getMinutes()).slice(-2);
}
tick();
setInterval(tick, 10000);
document.getElementById('pmToggle').addEventListener('click', function () {
screen.classList.toggle('dark');
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{css}</style>
<div className="pm-stage">
<div className="pm-phone" id="pmPhone">
<span className="pm-island"></span>
<span className="pm-btn pm-vol-up"></span>
<span className="pm-btn pm-vol-dn"></span>
<span className="pm-btn pm-power"></span>
<div className="pm-screen">
<div className="pm-status">
<span id="pmTime">9:41</span>
<span className="pm-status-r">
<svg viewBox="0 0 18 12" width="17"><rect x="0" y="7" width="3" height="5" rx="1"/><rect x="4" y="5" width="3" height="7" rx="1"/><rect x="8" y="3" width="3" height="9" rx="1"/><rect x="12" y="1" width="3" height="11" rx="1" opacity=".35"/></svg>
<svg viewBox="0 0 16 12" width="15"><path d="M8 11.5 1 4.5a10 10 0 0 1 14 0z" fill="none" stroke="currentColor" strokeWidth="1.4"/><circle cx="8" cy="9.4" r="1.3"/></svg>
<span className="pm-batt"><i></i></span>
</span>
</div>
<div className="pm-app">
<header className="pm-head">
<div><small>Good morning</small><h2>Alex</h2></div>
<div className="pm-ava">A</div>
</header>
<div className="pm-balance">
<small>Total balance</small>
<strong>$12,480.55</strong>
<span className="pm-up">+2.4% this week</span>
</div>
<div className="pm-actions">
<button>Send</button><button>Request</button><button>Top up</button>
</div>
<div className="pm-list">
<div className="pm-tx"><span className="pm-ic" style={{ background: '#6366f1' }}>N</span><div><b>Netflix</b><small>Subscription</small></div><em>-$15.99</em></div>
<div className="pm-tx"><span className="pm-ic" style={{ background: '#22c55e' }}>S</span><div><b>Salary</b><small>Acme Inc</small></div><em className="pos">+$4,200</em></div>
<div className="pm-tx"><span className="pm-ic" style={{ background: '#f59e0b' }}>U</span><div><b>Uber</b><small>Transport</small></div><em>-$8.40</em></div>
<div className="pm-tx"><span className="pm-ic" style={{ background: '#ec4899' }}>D</span><div><b>Dribbble</b><small>Pro plan</small></div><em>-$60.00</em></div>
</div>
</div>
</div>
</div>
<button type="button" className="pm-toggle" id="pmToggle">Toggle dark screen</button>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function PhoneMockup() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var screen = document.querySelector('.pm-screen');
var timeEl = document.getElementById('pmTime');
// Live status-bar clock so the mockup feels real.
function tick() {
var d = new Date();
var h = d.getHours() % 12 || 12;
timeEl.textContent = h + ':' + ('0' + d.getMinutes()).slice(-2);
}
tick();
setInterval(tick, 10000);
document.getElementById('pmToggle').addEventListener('click', function () {
screen.classList.toggle('dark');
});
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#e2e8f0;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px
}
.pm-batt::after {
content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:currentColor;border-radius:0 1px 1px 0
}
.pm-batt i {
position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:62%;background:currentColor;border-radius:1px
}
.pm-head small {
font-size:12px;color:#94a3b8;font-weight:600
}
.pm-head h2 {
font-size:20px;font-weight:800
}
.pm-balance small {
opacity:.8;font-size:12px
}
.pm-balance strong {
display:block;font-size:27px;font-weight:800;margin:3px 0
}
.pm-actions button {
flex:1;border:none;background:#fff;color:#4338ca;border-radius:12px;padding:10px;font-size:12px;font-weight:700;font-family:inherit;cursor:pointer;box-shadow:0 4px 12px -6px rgba(0,0,0,.3)
}
.pm-tx div {
flex:1;text-align:left;min-width:0
}
.pm-tx b {
font-size:13.5px;font-weight:700
}
.pm-tx small {
display:block;font-size:11px;color:#94a3b8
}
.pm-tx em {
font-style:normal;font-weight:800;font-size:13px
}
.pm-tx em.pos {
color:#16a34a
}
.pm-screen.dark {
background:#0f172a;color:#e2e8f0
}
.pm-screen.dark .pm-actions button {
background:#1e293b;color:#a5b4fc
}
.pm-screen.dark .pm-tx small,.pm-screen.dark .pm-head small {
color:#64748b
}
`}</style>
<div className="text-center">
<div className="relative w-[270px] h-[560px] bg-[#0b1220] rounded-[46px] p-[13px] shadow-[0_30px_60px_-20px_rgba(15,23,42,.5),inset_0_0_0_2px_#1e293b]" id="pmPhone">
<span className="absolute top-6 left-1/2 [transform:translateX(-50%)] w-[88px] h-[26px] bg-[#000] rounded-[14px] z-[5]"></span>
<span className="absolute bg-[#0b1220] rounded-[3px] left-[-3px] top-[130px] w-[3px] h-[34px]"></span>
<span className="absolute bg-[#0b1220] rounded-[3px] left-[-3px] top-44 w-[3px] h-[34px]"></span>
<span className="absolute bg-[#0b1220] rounded-[3px] right-[-3px] top-[150px] w-[3px] h-[54px]"></span>
<div className="pm-screen relative w-full h-full rounded-[34px] overflow-hidden bg-[#f1f5f9] text-[#0f172a] [transition:background_.3s,color_.3s]">
<div className="flex justify-between items-center pt-3.5 px-[22px] pb-1 text-[13px] font-bold [font-variant-numeric:tabular-nums]">
<span id="pmTime">9:41</span>
<span className="flex items-center gap-[5px] fill-current">
<svg viewBox="0 0 18 12" width="17"><rect x="0" y="7" width="3" height="5" rx="1"/><rect x="4" y="5" width="3" height="7" rx="1"/><rect x="8" y="3" width="3" height="9" rx="1"/><rect x="12" y="1" width="3" height="11" rx="1" opacity=".35"/></svg>
<svg viewBox="0 0 16 12" width="15"><path d="M8 11.5 1 4.5a10 10 0 0 1 14 0z" fill="none" stroke="currentColor" strokeWidth="1.4"/><circle cx="8" cy="9.4" r="1.3"/></svg>
<span className="pm-batt w-[22px] h-[11px] border rounded-[3px] relative inline-block opacity-90"><i></i></span>
</span>
</div>
<div className="pt-2 px-[18px] pb-[18px] h-[calc(100% - 36px)] overflow-y-auto">
<header className="pm-head flex items-center justify-between mt-2 mx-0 mb-4">
<div><small>Good morning</small><h2>Alex</h2></div>
<div className="w-10 h-10 rounded-full [background:linear-gradient(135deg,#6366f1,#22d3ee)] text-[#fff] flex items-center justify-center font-extrabold">A</div>
</header>
<div className="pm-balance [background:linear-gradient(135deg,#4f46e5,#7c3aed)] text-[#fff] rounded-[18px] p-[18px] text-left mb-3.5">
<small>Total balance</small>
<strong>$12,480.55</strong>
<span className="text-xs bg-[rgba(255,255,255,.2)] py-0.5 px-2 rounded-[99px]">+2.4% this week</span>
</div>
<div className="pm-actions flex gap-2 mb-[18px]">
<button>Send</button><button>Request</button><button>Top up</button>
</div>
<div className="flex flex-col gap-[13px]">
<div className="pm-tx flex items-center gap-[11px]"><span className="w-[38px] h-[38px] rounded-[11px] text-[#fff] font-extrabold flex items-center justify-center shrink-0" style={{ background: '#6366f1' }}>N</span><div><b>Netflix</b><small>Subscription</small></div><em>-$15.99</em></div>
<div className="pm-tx flex items-center gap-[11px]"><span className="w-[38px] h-[38px] rounded-[11px] text-[#fff] font-extrabold flex items-center justify-center shrink-0" style={{ background: '#22c55e' }}>S</span><div><b>Salary</b><small>Acme Inc</small></div><em className="pos">+$4,200</em></div>
<div className="pm-tx flex items-center gap-[11px]"><span className="w-[38px] h-[38px] rounded-[11px] text-[#fff] font-extrabold flex items-center justify-center shrink-0" style={{ background: '#f59e0b' }}>U</span><div><b>Uber</b><small>Transport</small></div><em>-$8.40</em></div>
<div className="pm-tx flex items-center gap-[11px]"><span className="w-[38px] h-[38px] rounded-[11px] text-[#fff] font-extrabold flex items-center justify-center shrink-0" style={{ background: '#ec4899' }}>D</span><div><b>Dribbble</b><small>Pro plan</small></div><em>-$60.00</em></div>
</div>
</div>
</div>
</div>
<button type="button" className="mt-[22px] bg-[#0f172a] text-[#fff] border-0 rounded-[9px] py-[9px] px-4 text-[12.5px] font-bold cursor-pointer font-[inherit] hover:bg-[#1e293b]" id="pmToggle">Toggle dark screen</button>
</div>
</>
);
}<template>
<div class="pm-stage">
<div class="pm-phone" id="pmPhone">
<span class="pm-island"></span>
<span class="pm-btn pm-vol-up"></span>
<span class="pm-btn pm-vol-dn"></span>
<span class="pm-btn pm-power"></span>
<div class="pm-screen">
<div class="pm-status">
<span id="pmTime">9:41</span>
<span class="pm-status-r">
<svg viewBox="0 0 18 12" width="17"><rect x="0" y="7" width="3" height="5" rx="1"/><rect x="4" y="5" width="3" height="7" rx="1"/><rect x="8" y="3" width="3" height="9" rx="1"/><rect x="12" y="1" width="3" height="11" rx="1" opacity=".35"/></svg>
<svg viewBox="0 0 16 12" width="15"><path d="M8 11.5 1 4.5a10 10 0 0 1 14 0z" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="8" cy="9.4" r="1.3"/></svg>
<span class="pm-batt"><i></i></span>
</span>
</div>
<div class="pm-app">
<header class="pm-head">
<div><small>Good morning</small><h2>Alex</h2></div>
<div class="pm-ava">A</div>
</header>
<div class="pm-balance">
<small>Total balance</small>
<strong>$12,480.55</strong>
<span class="pm-up">+2.4% this week</span>
</div>
<div class="pm-actions">
<button>Send</button><button>Request</button><button>Top up</button>
</div>
<div class="pm-list">
<div class="pm-tx"><span class="pm-ic" style="background:#6366f1">N</span><div><b>Netflix</b><small>Subscription</small></div><em>-$15.99</em></div>
<div class="pm-tx"><span class="pm-ic" style="background:#22c55e">S</span><div><b>Salary</b><small>Acme Inc</small></div><em class="pos">+$4,200</em></div>
<div class="pm-tx"><span class="pm-ic" style="background:#f59e0b">U</span><div><b>Uber</b><small>Transport</small></div><em>-$8.40</em></div>
<div class="pm-tx"><span class="pm-ic" style="background:#ec4899">D</span><div><b>Dribbble</b><small>Pro plan</small></div><em>-$60.00</em></div>
</div>
</div>
</div>
</div>
<button type="button" class="pm-toggle" id="pmToggle">Toggle dark screen</button>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
let screen;
let timeEl;
// Live status-bar clock so the mockup feels real.
function tick() {
var d = new Date();
var h = d.getHours() % 12 || 12;
timeEl.textContent = h + ':' + ('0' + d.getMinutes()).slice(-2);
}
onMounted(() => {
screen = document.querySelector('.pm-screen');
timeEl = document.getElementById('pmTime');
tick();
setInterval(tick, 10000);
document.getElementById('pmToggle').addEventListener('click', function () {
screen.classList.toggle('dark');
});
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#e2e8f0;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.pm-stage{text-align:center}
.pm-phone{position:relative;width:270px;height:560px;background:#0b1220;border-radius:46px;padding:13px;box-shadow:0 30px 60px -20px rgba(15,23,42,.5),inset 0 0 0 2px #1e293b}
.pm-island{position:absolute;top:24px;left:50%;transform:translateX(-50%);width:88px;height:26px;background:#000;border-radius:14px;z-index:5}
.pm-btn{position:absolute;background:#0b1220;border-radius:3px}
.pm-vol-up{left:-3px;top:130px;width:3px;height:34px}
.pm-vol-dn{left:-3px;top:176px;width:3px;height:34px}
.pm-power{right:-3px;top:150px;width:3px;height:54px}
.pm-screen{position:relative;width:100%;height:100%;border-radius:34px;overflow:hidden;background:#f1f5f9;color:#0f172a;transition:background .3s,color .3s}
.pm-status{display:flex;justify-content:space-between;align-items:center;padding:14px 22px 4px;font-size:13px;font-weight:700;font-variant-numeric:tabular-nums}
.pm-status-r{display:flex;align-items:center;gap:5px;fill:currentColor}
.pm-batt{width:22px;height:11px;border:1.4px solid currentColor;border-radius:3px;position:relative;display:inline-block;opacity:.9}
.pm-batt::after{content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:currentColor;border-radius:0 1px 1px 0}
.pm-batt i{position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:62%;background:currentColor;border-radius:1px}
.pm-app{padding:8px 18px 18px;height:calc(100% - 36px);overflow-y:auto}
.pm-head{display:flex;align-items:center;justify-content:space-between;margin:8px 0 16px}
.pm-head small{font-size:12px;color:#94a3b8;font-weight:600}
.pm-head h2{font-size:20px;font-weight:800}
.pm-ava{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#22d3ee);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800}
.pm-balance{background:linear-gradient(135deg,#4f46e5,#7c3aed);color:#fff;border-radius:18px;padding:18px;text-align:left;margin-bottom:14px}
.pm-balance small{opacity:.8;font-size:12px}
.pm-balance strong{display:block;font-size:27px;font-weight:800;margin:3px 0}
.pm-up{font-size:12px;background:rgba(255,255,255,.2);padding:2px 8px;border-radius:99px}
.pm-actions{display:flex;gap:8px;margin-bottom:18px}
.pm-actions button{flex:1;border:none;background:#fff;color:#4338ca;border-radius:12px;padding:10px;font-size:12px;font-weight:700;font-family:inherit;cursor:pointer;box-shadow:0 4px 12px -6px rgba(0,0,0,.3)}
.pm-list{display:flex;flex-direction:column;gap:13px}
.pm-tx{display:flex;align-items:center;gap:11px}
.pm-ic{width:38px;height:38px;border-radius:11px;color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.pm-tx div{flex:1;text-align:left;min-width:0}
.pm-tx b{font-size:13.5px;font-weight:700}
.pm-tx small{display:block;font-size:11px;color:#94a3b8}
.pm-tx em{font-style:normal;font-weight:800;font-size:13px}
.pm-tx em.pos{color:#16a34a}
.pm-screen.dark{background:#0f172a;color:#e2e8f0}
.pm-screen.dark .pm-actions button{background:#1e293b;color:#a5b4fc}
.pm-screen.dark .pm-tx small,.pm-screen.dark .pm-head small{color:#64748b}
.pm-toggle{margin-top:22px;background:#0f172a;color:#fff;border:none;border-radius:9px;padding:9px 16px;font-size:12.5px;font-weight:700;cursor:pointer;font-family:inherit}
.pm-toggle:hover{background:#1e293b}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-phone-mockup',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="pm-stage">
<div class="pm-phone" id="pmPhone">
<span class="pm-island"></span>
<span class="pm-btn pm-vol-up"></span>
<span class="pm-btn pm-vol-dn"></span>
<span class="pm-btn pm-power"></span>
<div class="pm-screen">
<div class="pm-status">
<span id="pmTime">9:41</span>
<span class="pm-status-r">
<svg viewBox="0 0 18 12" width="17"><rect x="0" y="7" width="3" height="5" rx="1"/><rect x="4" y="5" width="3" height="7" rx="1"/><rect x="8" y="3" width="3" height="9" rx="1"/><rect x="12" y="1" width="3" height="11" rx="1" opacity=".35"/></svg>
<svg viewBox="0 0 16 12" width="15"><path d="M8 11.5 1 4.5a10 10 0 0 1 14 0z" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="8" cy="9.4" r="1.3"/></svg>
<span class="pm-batt"><i></i></span>
</span>
</div>
<div class="pm-app">
<header class="pm-head">
<div><small>Good morning</small><h2>Alex</h2></div>
<div class="pm-ava">A</div>
</header>
<div class="pm-balance">
<small>Total balance</small>
<strong>$12,480.55</strong>
<span class="pm-up">+2.4% this week</span>
</div>
<div class="pm-actions">
<button>Send</button><button>Request</button><button>Top up</button>
</div>
<div class="pm-list">
<div class="pm-tx"><span class="pm-ic" style="background:#6366f1">N</span><div><b>Netflix</b><small>Subscription</small></div><em>-$15.99</em></div>
<div class="pm-tx"><span class="pm-ic" style="background:#22c55e">S</span><div><b>Salary</b><small>Acme Inc</small></div><em class="pos">+$4,200</em></div>
<div class="pm-tx"><span class="pm-ic" style="background:#f59e0b">U</span><div><b>Uber</b><small>Transport</small></div><em>-$8.40</em></div>
<div class="pm-tx"><span class="pm-ic" style="background:#ec4899">D</span><div><b>Dribbble</b><small>Pro plan</small></div><em>-$60.00</em></div>
</div>
</div>
</div>
</div>
<button type="button" class="pm-toggle" id="pmToggle">Toggle dark screen</button>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#e2e8f0;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.pm-stage{text-align:center}
.pm-phone{position:relative;width:270px;height:560px;background:#0b1220;border-radius:46px;padding:13px;box-shadow:0 30px 60px -20px rgba(15,23,42,.5),inset 0 0 0 2px #1e293b}
.pm-island{position:absolute;top:24px;left:50%;transform:translateX(-50%);width:88px;height:26px;background:#000;border-radius:14px;z-index:5}
.pm-btn{position:absolute;background:#0b1220;border-radius:3px}
.pm-vol-up{left:-3px;top:130px;width:3px;height:34px}
.pm-vol-dn{left:-3px;top:176px;width:3px;height:34px}
.pm-power{right:-3px;top:150px;width:3px;height:54px}
.pm-screen{position:relative;width:100%;height:100%;border-radius:34px;overflow:hidden;background:#f1f5f9;color:#0f172a;transition:background .3s,color .3s}
.pm-status{display:flex;justify-content:space-between;align-items:center;padding:14px 22px 4px;font-size:13px;font-weight:700;font-variant-numeric:tabular-nums}
.pm-status-r{display:flex;align-items:center;gap:5px;fill:currentColor}
.pm-batt{width:22px;height:11px;border:1.4px solid currentColor;border-radius:3px;position:relative;display:inline-block;opacity:.9}
.pm-batt::after{content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:currentColor;border-radius:0 1px 1px 0}
.pm-batt i{position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:62%;background:currentColor;border-radius:1px}
.pm-app{padding:8px 18px 18px;height:calc(100% - 36px);overflow-y:auto}
.pm-head{display:flex;align-items:center;justify-content:space-between;margin:8px 0 16px}
.pm-head small{font-size:12px;color:#94a3b8;font-weight:600}
.pm-head h2{font-size:20px;font-weight:800}
.pm-ava{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#22d3ee);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800}
.pm-balance{background:linear-gradient(135deg,#4f46e5,#7c3aed);color:#fff;border-radius:18px;padding:18px;text-align:left;margin-bottom:14px}
.pm-balance small{opacity:.8;font-size:12px}
.pm-balance strong{display:block;font-size:27px;font-weight:800;margin:3px 0}
.pm-up{font-size:12px;background:rgba(255,255,255,.2);padding:2px 8px;border-radius:99px}
.pm-actions{display:flex;gap:8px;margin-bottom:18px}
.pm-actions button{flex:1;border:none;background:#fff;color:#4338ca;border-radius:12px;padding:10px;font-size:12px;font-weight:700;font-family:inherit;cursor:pointer;box-shadow:0 4px 12px -6px rgba(0,0,0,.3)}
.pm-list{display:flex;flex-direction:column;gap:13px}
.pm-tx{display:flex;align-items:center;gap:11px}
.pm-ic{width:38px;height:38px;border-radius:11px;color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.pm-tx div{flex:1;text-align:left;min-width:0}
.pm-tx b{font-size:13.5px;font-weight:700}
.pm-tx small{display:block;font-size:11px;color:#94a3b8}
.pm-tx em{font-style:normal;font-weight:800;font-size:13px}
.pm-tx em.pos{color:#16a34a}
.pm-screen.dark{background:#0f172a;color:#e2e8f0}
.pm-screen.dark .pm-actions button{background:#1e293b;color:#a5b4fc}
.pm-screen.dark .pm-tx small,.pm-screen.dark .pm-head small{color:#64748b}
.pm-toggle{margin-top:22px;background:#0f172a;color:#fff;border:none;border-radius:9px;padding:9px 16px;font-size:12.5px;font-weight:700;cursor:pointer;font-family:inherit}
.pm-toggle:hover{background:#1e293b}
`]
})
export class PhoneMockupComponent implements AfterViewInit {
ngAfterViewInit(): void {
var screen = document.querySelector('.pm-screen');
var timeEl = document.getElementById('pmTime');
// Live status-bar clock so the mockup feels real.
function tick() {
var d = new Date();
var h = d.getHours() % 12 || 12;
timeEl.textContent = h + ':' + ('0' + d.getMinutes()).slice(-2);
}
tick();
setInterval(tick, 10000);
document.getElementById('pmToggle').addEventListener('click', function () {
screen.classList.toggle('dark');
});
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { tick });
}
}