Standalone example · v1
Lighthouse and axe: the same product page.
A desk lamp with deliberately isolated barriers. Compare rule selection, keyboard operation and an opened dialog in three separate cases.
No login, cart or consent step is needed. Prepare clears only this example’s temporary action and dialog. For a repeatable scan, reset display tests to 100% text with spacing and preview off.
Reference versions: Lighthouse 13.5.0, axe-core 4.13.0. Record the versions in your own reports.
This case contains six deliberate candidates: an unnamed chart, a scroll region skipped by Tab, nested controls, fixed inline letter spacing, an empty disclosure summary and an unnamed tab. The improved variant corrects those targets. Keyboard and dialog cases have their own URLs.

Demo product · no purchase
Desk lamp
€49.00
Olive-green metal, a rounded shade and a warm pool of light for your desk. This product is fictional.
Two brightness settings
The bars show the low and high brightness settings.
Delivery and setup
The lamp arrives in a protective cardboard box. Keep the packaging until you have checked all parts.
Place the base on a stable surface. Leave enough room around the shade and keep the cable away from sharp edges.
Use the switch on the cable to choose a brightness setting. Disconnect the lamp before cleaning.
A warm light for reading, writing and the last task of the day.
Wipe the metal with a dry soft cloth. Avoid abrasive cleaners.
Metal housing · 32 cm high · two brightness settings.
View the shared source
These are the actual source files used by this fixture. A generated snapshot is checked against the implementation before each build validation. Imports and reusable helpers remain visible; this is not a separate teaching copy.
src/components/features/examples/lighthouse-product.tsx
'use client';
import Image from 'next/image';
import Link from 'next/link';
import {useRef,useState} from 'react';
import {useTranslations} from 'next-intl';
import {Button} from '@/components/ui/button';
import {Dialog,DialogPortal,DialogOverlay,DialogContent,DialogTitle,DialogDescription} from '@/components/ui/dialog';
import {asset} from '@/lib/fixtures/assets';
import {formatPrice} from '@/lib/format';
import {lighthouseExamplePath,type DemoRoute,type LighthouseCase} from '@/lib/routes';
import {lighthouseVersions} from '@/lib/examples/lighthouse';
export function LighthouseProduct({route,testCase}:{route:DemoRoute;testCase:LighthouseCase}){
const t=useTranslations('Lighthouse');
const assets=useTranslations('Assets');
const improved=route.variant==='improved';
const photo=asset('desk-lamp');
const [added,setAdded]=useState(false);
const [open,setOpen]=useState(false);
const trigger=useRef<HTMLButtonElement>(null);
const care=useRef<HTMLDetailsElement>(null);
function showCare(){if(care.current)care.current.open=true;}
return <section className="lighthouse-product" aria-labelledby="lh-product-title">
<nav className="lighthouse-case-nav" aria-label={t('cases')}>
{(['rules','keyboard','dialog'] as const).map(value=><Link key={value} href={lighthouseExamplePath(route,value)} aria-current={testCase===value?'page':undefined}>{t(`${value}.label`)}</Link>)}
</nav>
<p className="field-help">{t('versions',{...lighthouseVersions})}</p>
<p id="lh-case-instructions">{t(`${testCase}.instructions`)}</p>
<div className="product-detail-grid">
<div className="product-gallery"><Image src={photo.file} width={photo.width} height={photo.height} alt={assets('desk-lamp.alt')} sizes="(max-width: 760px) 100vw, 55vw" priority/></div>
<div className="product-information">
<p className="eyebrow">{t('edition')}</p><h2 id="lh-product-title">{t('product')}</h2>
<p className="product-detail-price">{formatPrice(4900,route.locale)}</p><p>{t('description')}</p>
{testCase==='rules'&&<>
<h3>{t('lightOutput')}</h3>
{/* Intentional role-img-alt: a meaningful chart has no name only in barriers. */}
<div id="lh-chart" role="img" aria-label={improved?t('chartAlternative'):undefined} className="lighthouse-chart"/>
<p className="field-help">{t('chartCaption')}</p>
<h3>{t('delivery')}</h3>
{/* Explicit -1 prevents Chromium's automatic scroll-container tab stop in barriers. */}
<div id="lh-scroll" className="lighthouse-scroll" tabIndex={improved?0:-1}>{[1,2,3].map(number=><p key={number}>{t(`delivery${number}`)}</p>)}</div>
{/* Intentional nested-interactive; shared Button remains unchanged. */}
<div id="lh-nested" className="lighthouse-nested" role={improved?undefined:'button'} tabIndex={improved?undefined:0} onClick={improved?undefined:showCare} onKeyDown={improved?undefined:event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();showCare();}}}><a href="#lh-details" onClick={showCare}>{t('detailsLink')}</a></div>
{/* Intentional avoid-inline-spacing; !important is applied after hydration. */}
<p id="lh-spacing" ref={node=>{if(node&&!improved)node.style.setProperty('letter-spacing','0','important');}}>{t('spacing')}</p>
<details ref={care} id="lh-details"><summary id="lh-summary" className="lighthouse-summary">{improved?t('care'):null}</summary><p>{t('careText')}</p></details>
<div role="tablist" aria-label={t('information')}><div id="lh-tab" className="lighthouse-tab" role="tab" aria-selected="true" aria-controls="lh-panel" tabIndex={0}>{improved?t('specifications'):null}</div></div>
<div id="lh-panel" role="tabpanel" aria-label={t('specifications')}><p>{t('specificationText')}</p></div>
</>}
{testCase==='keyboard'&&<>
{/* Intentional manual case: no role, tab stop or keyboard handler on this div. */}
{improved?<Button id="lh-cart" type="button" onClick={()=>setAdded(true)}>{t('add')}</Button>:<div id="lh-cart" className="button button-primary button-default" onClick={()=>setAdded(true)}>{t('add')}</div>}
<p id="lh-status" role="status" className="sample-result">{added?t('added'):t('empty')}</p>
<a id="lh-after-action" href="#example-source">{t('afterAction')}</a>
</>}
{testCase==='dialog'&&<>
<Button ref={trigger} id="lh-open" type="button" onClick={()=>setOpen(true)}>{t('open')}</Button>
<Dialog open={open} onOpenChange={setOpen}><DialogPortal><DialogOverlay className="dialog-overlay"/><DialogContent className="cookie-dialog" id="lh-dialog" onCloseAutoFocus={event=>{event.preventDefault();trigger.current?.focus();}}><DialogTitle>{t('dialogTitle')}</DialogTitle><DialogDescription>{t('dialogDescription')}</DialogDescription>
{/* The dialog, focus trap and Escape recovery are correct in both variants. */}
<Button id="lh-close" type="button" variant="outline" className="lighthouse-dialog-close" aria-label={improved?t('close'):undefined} onClick={()=>setOpen(false)}><svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 6 12 12M18 6 6 18" fill="none" stroke="currentColor" strokeWidth="2"/></svg></Button>
</DialogContent></DialogPortal></Dialog>
</>}
</div>
</div>
</section>;
}
src/lib/examples/lighthouse.ts
// These are intended rule candidates, not recorded scanner findings.
export const lighthouseRuleCases=[
{id:'lighthouse-role-image',targetId:'lh-chart',rule:'role-img-alt',sc:'1.1.1'},
{id:'lighthouse-scroll-region',targetId:'lh-scroll',rule:'scrollable-region-focusable',sc:'2.1.1'},
{id:'lighthouse-nested-control',targetId:'lh-nested',rule:'nested-interactive',sc:'4.1.2'},
{id:'lighthouse-text-spacing',targetId:'lh-spacing',rule:'avoid-inline-spacing',sc:'1.4.12'},
{id:'lighthouse-summary-name',targetId:'lh-summary',rule:'summary-name',sc:'4.1.2'},
{id:'lighthouse-tab-name',targetId:'lh-tab',rule:'aria-tab-name',sc:'4.1.2'}
] as const;
export const lighthouseVersions={lighthouse:'13.5.0',axe:'4.13.0'} as const;
src/app/[locale]/globals.css
.lighthouse-product {min-width:0;}
.lighthouse-scroll {height:6rem;overflow:auto;border:1px solid var(--border);padding:.75rem;margin-bottom:1rem;}
.lighthouse-chart {display:flex;gap:1rem;align-items:end;height:5rem;margin-bottom:.5rem;}
.lighthouse-chart::before {content:'';display:block;width:40%;height:60%;background:var(--primary);}
.lighthouse-chart::after {content:'';display:block;width:40%;height:100%;background:var(--primary);}
.lighthouse-summary {min-height:44px;padding:.5rem 0;cursor:pointer;}
.lighthouse-tab {display:inline-flex;align-items:center;min-height:44px;min-width:100px;background:var(--card);color:var(--foreground);border:1px solid var(--border);padding:.5rem 1rem;cursor:pointer;}
.lighthouse-nested {padding:.5rem 0;}.lighthouse-nested>a {display:inline-flex;min-height:44px;align-items:center;}
.lighthouse-case-nav {display:flex;flex-wrap:wrap;gap:1rem;margin-bottom:1rem;}
.lighthouse-dialog-close {min-width:48px;}