diff --git a/CHANGELOG.md b/CHANGELOG.md
index f07d08f..158865e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ All notable changes to Previewer are listed here. The format follows
## [Unreleased]
+### Added
+
+- The mouse pointer is a fingertip inside the phone, like DevTools' touch pointer, and darkens while you press.
+
## [1.0.0] - 2026-10-30
### Added
diff --git a/README.md b/README.md
index bf2a32b..237288d 100644
--- a/README.md
+++ b/README.md
@@ -95,7 +95,8 @@ controlled" banner on every page:
- `navigator.userAgent` in JavaScript still reports desktop Chrome. Server-side detection works; client-side sniffing
doesn't.
-- Touch: `(hover: hover)` and `(pointer: fine)` still match, and you scroll with a mouse or trackpad.
+- Touch: inside the phone the mouse pointer becomes a fingertip, but it is still a mouse. Hover effects still work,
+ `(hover: hover)` and `(pointer: fine)` still match, and you scroll with a mouse wheel or trackpad.
- The rendering engine is Chrome's Blink, not Safari's WebKit. Previewer shows layout, not WebKit-specific bugs.
If a link inside the phone leads to a different website that refuses to be shown in a frame, Chrome shows its own
diff --git a/extension/overlay.js b/extension/overlay.js
index 7d9928e..9ba8a60 100644
--- a/extension/overlay.js
+++ b/extension/overlay.js
@@ -35,6 +35,18 @@
tablet: `9:41${WIFI}100%${BATTERY}`,
};
+ // A fingertip like DevTools' touch pointer: grey with a light and a dark ring so
+ // it shows on any background, darker while pressed. Hotspot in the centre.
+ const touchCursor = (fill, r) => {
+ const svg = ``;
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}") 12 12, default`;
+ };
+ const FRAME_CSS = `
+ html { scrollbar-width: none; }
+ ::-webkit-scrollbar { display: none; }
+ *, *::before, *::after { cursor: ${touchCursor('rgba(70,74,82,.38)', 10)} !important; }
+ *:active, *:active::before, *:active::after { cursor: ${touchCursor('rgba(70,74,82,.7)', 9)} !important; }`;
+
let ui = null; // everything that exists while the preview is open
function open({ devices, settings, css }) {
@@ -265,13 +277,14 @@
}
// Phones overlay their scrollbars; hide desktop scrollbars so the page gets
- // its full width (Windows otherwise takes ~15px of a 402px viewport).
+ // its full width (Windows otherwise takes ~15px of a 402px viewport). Show a
+ // fingertip instead of the mouse pointer, so it looks and feels like touch.
try {
const sheet = new doc.defaultView.CSSStyleSheet();
- sheet.replaceSync('html { scrollbar-width: none; } ::-webkit-scrollbar { display: none; }');
+ sheet.replaceSync(FRAME_CSS);
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet];
} catch {
- // Not critical: the page just keeps its scrollbar.
+ // Not critical: the page keeps its scrollbar and mouse pointer.
}
doc.addEventListener('keydown', onKeydown, true);
diff --git a/test/e2e/preview.test.js b/test/e2e/preview.test.js
index 4e17b9f..eaa943c 100644
--- a/test/e2e/preview.test.js
+++ b/test/e2e/preview.test.js
@@ -120,6 +120,7 @@ test('shows the page at the phone width with the phone user agent', async () =>
assert.equal(await frame.evaluate(() => innerWidth), 402);
assert.match(await frame.evaluate(() => navigator.userAgent), /Chrome/, 'JS user agent is not overridden (documented)');
assert.match(seenUserAgents.at(-1), /iPhone/);
+ assert.match(await frame.evaluate(() => getComputedStyle(document.querySelector('.hero')).cursor), /^url\("data:image\/svg\+xml/, 'touch pointer');
await page.waitForTimeout(300);
await shoot(page, 'iphone-18-pro');