viewport-exposure-map
Track element visibility during scroll using IntersectionObserver and generate an exposure heatmap showing which content users actually see.
What this skill does
# Viewport Exposure Map
Measure how long each significant content element stays visible in the viewport during a full scroll session. Identify CTAs that are never seen, content with minimal exposure, and layout regions that dominate user attention.
## When to Use
- Auditing landing page layout to ensure CTAs are visible without excessive scrolling
- Validating that key content (hero sections, pricing, signup forms) receives adequate viewport time
- Comparing two page variants for scroll-based content exposure
- Detecting elements pushed below the fold or hidden behind lazy-load failures
- QA for long-form content pages (blogs, documentation, product pages)
## Prerequisites
- Playwright MCP server connected with a browser session available
- Target page must be publicly accessible or already authenticated in the browser session
- Page should be fully loaded before starting (wait for network idle or a specific selector)
## Workflow
### Step 1: Navigate to the Target Page
Use `browser_navigate` to load the page.
```
browser_navigate({ url: "https://example.com/landing" })
```
Wait for the page to be fully loaded:
```
browser_wait_for({ text: "some expected text" })
```
Or wait a fixed time if no specific text anchor exists:
```
browser_wait_for({ time: 3 })
```
### Step 2: Inject IntersectionObserver Tracking
Use `browser_evaluate` to attach an IntersectionObserver to all significant content elements. This must run before any scrolling occurs.
```javascript
browser_evaluate({
function: `() => {
const SELECTORS = [
'h1', 'h2', 'h3', 'h4',
'p', 'img', 'section', 'article',
'button', 'a',
'[data-testid]',
'figure', 'video',
'.card', '.hero', '.cta',
'form', 'nav', 'footer', 'header'
];
const elements = new Set();
SELECTORS.forEach(sel => {
document.querySelectorAll(sel).forEach(el => elements.add(el));
});
window.__exposureData = new Map();
let idCounter = 0;
const observer = new IntersectionObserver((entries) => {
const now = performance.now();
entries.forEach(entry => {
const id = entry.target.dataset.__exposureId;
const data = window.__exposureData.get(id);
if (!data) return;
if (entry.isIntersecting && entry.intersectionRatio > 0) {
if (!data._enteredAt) {
data._enteredAt = now;
if (!data.firstSeen) data.firstSeen = now;
}
if (entry.intersectionRatio > data.maxRatio) {
data.maxRatio = entry.intersectionRatio;
}
} else {
if (data._enteredAt) {
data.totalVisibleMs += (now - data._enteredAt);
data._enteredAt = null;
}
}
});
}, {
threshold: [0, 0.25, 0.5, 0.75, 1.0]
});
elements.forEach(el => {
const id = 'exp_' + (idCounter++);
el.dataset.__exposureId = id;
window.__exposureData.set(id, {
tag: el.tagName.toLowerCase(),
text: (el.textContent || '').trim().substring(0, 80),
className: el.className ? String(el.className).substring(0, 60) : '',
id: el.id || '',
rect: el.getBoundingClientRect().toJSON(),
firstSeen: null,
totalVisibleMs: 0,
maxRatio: 0,
_enteredAt: null
});
observer.observe(el);
});
window.__exposureObserver = observer;
return { tracked: elements.size };
}`
})
```
### Step 3: Perform Automated Scroll Sequence
Use `browser_run_code` to scroll the page smoothly from top to bottom and back. This simulates a real user scanning the page.
```javascript
browser_run_code({
code: `async (page) => {
// Scroll to bottom smoothly over ~5 seconds
const scrollHeight = await page.evaluate(() => document.documentElement.scrollHeight);
const viewportHeight = await page.evaluate(() => window.innerHeight);
const steps = 25;
const stepSize = (scrollHeight - viewportHeight) / steps;
for (let i = 1; i <= steps; i++) {
await page.evaluate((y) => window.scrollTo({ top: y, behavior: 'smooth' }), stepSize * i);
await page.waitForTimeout(200);
}
// Pause at bottom
await page.waitForTimeout(2000);
// Scroll back to top
for (let i = steps - 1; i >= 0; i--) {
await page.evaluate((y) => window.scrollTo({ top: y, behavior: 'smooth' }), stepSize * i);
await page.waitForTimeout(200);
}
// Pause at top
await page.waitForTimeout(1000);
return 'Scroll complete';
}`
})
```
### Step 4: Harvest Exposure Data
Use `browser_evaluate` to finalize timing for any currently-visible elements and extract the results.
```javascript
browser_evaluate({
function: `() => {
const now = performance.now();
const results = [];
window.__exposureData.forEach((data, id) => {
// Close any open visibility session
if (data._enteredAt) {
data.totalVisibleMs += (now - data._enteredAt);
data._enteredAt = null;
}
results.push({
id: id,
tag: data.tag,
text: data.text,
className: data.className,
elementId: data.id,
totalVisibleSec: Math.round(data.totalVisibleMs / 100) / 10,
maxRatio: Math.round(data.maxRatio * 100),
firstSeen: data.firstSeen ? Math.round(data.firstSeen) : null,
neverSeen: data.firstSeen === null
});
});
// Sort by totalVisibleSec descending
results.sort((a, b) => b.totalVisibleSec - a.totalVisibleSec);
const neverSeen = results.filter(r => r.neverSeen);
const underOneSecond = results.filter(r => !r.neverSeen && r.totalVisibleSec < 1.0);
const ctas = results.filter(r =>
r.tag === 'button' || r.tag === 'a' ||
r.className.includes('cta') || r.className.includes('btn')
);
const neverSeenCtas = ctas.filter(r => r.neverSeen);
return {
totalTracked: results.length,
neverSeenCount: neverSeen.length,
underOneSecondCount: underOneSecond.length,
neverSeenCtas: neverSeenCtas,
topExposed: results.slice(0, 15),
neverSeen: neverSeen.slice(0, 20),
underOneSecond: underOneSecond.slice(0, 20),
allResults: results
};
}`
})
```
### Step 5 (Optional): Inject CSS Heatmap Overlay
Use `browser_evaluate` to color-code elements based on their exposure time. Green indicates high exposure, yellow is moderate, red means never or barely seen.
```javascript
browser_evaluate({
function: `() => {
window.__exposureData.forEach((data, id) => {
const el = document.querySelector('[data-__exposure-id="' + id + '"]');
if (!el) return;
// Close any open session
const now = performance.now();
if (data._enteredAt) {
data.totalVisibleMs += (now - data._enteredAt);
data._enteredAt = null;
}
const sec = data.totalVisibleMs / 1000;
let color;
if (data.firstSeen === null) {
color = 'rgba(255, 0, 0, 0.3)'; // Red: never seen
} else if (sec < 1.0) {
color = 'rgba(255, 165, 0, 0.3)'; // Orange: <1s
} else if (sec < 3.0) {
color = 'rgba(255, 255, 0, 0.25)'; // Yellow: 1-3s
} else {
color = 'rgba(0, 200, 0, 0.25)'; // Green: 3s+
}
el.style.outline = '2px solid ' + color.replace('0.3', '0.8').replace('0.25', '0.8');
el.style.backgroundColor = color;
});
return 'Heatmap overlay applied';
}`
})
```
### Step 6: Capture Full-Page Screenshot
Take a full-page screenshot showing the heatmap overlay.
```
browser_take_screenshot({ fullPage: true, type: "png", filename: "exposure-heatmap.png" })
```
## Interpreting Results
### Exposure Time Thresholds
| Exposure Time | Status | Meaning |
|---|---|---|
| 0 seconds (never seen) | Critical | Element is off-screen or hidden; users never see it |
| < 1 second | Warning | Barely visible; users likely scan past it |
| 1 - 3 seconds | Acceptable | Moderate exposure; may be noticed duriRelated in Writing & Docs
jax-development
IncludedUse this skill when the user is writing, debugging, profiling, refactoring, reviewing, benchmarking, parallelising, exporting, or explaining JAX code, or when they mention JAX, jax.numpy, jit, grad, value_and_grad, vmap, scan, lax, random keys, pytrees, jax.Array, sharding, Mesh, PartitionSpec, NamedSharding, pmap, shard_map, Pallas, XLA, StableHLO, checkify, profiler, or the JAX repo. It helps turn NumPy or PyTorch-style code into pure functional JAX, fix tracer/control-flow/shape/PRNG bugs, remove recompiles and host-device syncs, choose transforms and sharding strategies, inspect jaxpr/lowering/IR, and benchmark compiled code correctly.
nature-article-writer
IncludedDrafts, rewrites, diagnostically critiques, and style-calibrates primary research manuscripts for Nature and Nature Portfolio journals. Use when the user wants a Nature-style title, summary paragraph or abstract, introduction, results, discussion, methods, figure legends, presubmission enquiry, cover letter, reviewer response, or when a scientific draft sounds generic, jargon-heavy, structurally weak, or AI-ish and needs precise, broad-reader-friendly prose without inventing data, analyses, or references. Best for primary research articles and letters rather than reviews or press releases unless explicitly adapting one.
deckrd
IncludedDocument-driven framework that derives requirements, specifications, implementation plans, and executable tasks from goals through structured AI dialogue. Use when user says "write requirements", "create spec", "plan implementation", "derive tasks", "structure this feature", "break down into tasks", or "document this module". Also use for reverse engineering existing code into docs (/deckrd rev). Do NOT use for direct code writing — use /deckrd-coder after tasks are generated. Do NOT use when the user only wants to run or fix existing code without planning.
clinical-decision-support
IncludedGenerate professional clinical decision support (CDS) documents for pharmaceutical and clinical research settings, including patient cohort analyses (biomarker-stratified with outcomes) and treatment recommendation reports (evidence-based guidelines with decision algorithms). Supports GRADE evidence grading, statistical analysis (hazard ratios, survival curves, waterfall plots), biomarker integration, and regulatory compliance. Outputs publication-ready LaTeX/PDF format optimized for drug development, clinical research, and evidence synthesis.
handling-sf-data
IncludedSalesforce data operations with 130-point scoring. Use this skill to create, update, delete, bulk import/export, generate test data, and clean up org records using sf CLI and anonymous Apex. TRIGGER when: user creates test data, performs bulk import/export, uses sf data CLI commands, needs data factory patterns for Apex tests, or needs to seed/clean records in a Salesforce org. DO NOT TRIGGER when: SOQL query writing only (use querying-soql), Apex test execution (use running-apex-tests), or metadata deployment (use deploying-metadata).
accelint-ac-to-playwright
IncludedConvert and validate acceptance criteria for Playwright test automation. Use when user asks to (1) review/evaluate/check if AC are ready for automation, (2) assess if AC can be converted as-is, (3) validate AC quality for Playwright, (4) turn AC into tests, (5) generate tests from acceptance criteria, (6) convert .md bullets or .feature Gherkin files to Playwright specs, (7) create test automation from requirements. Handles both bullet-style markdown and Gherkin syntax with JSON test plan generation and validation.