Fictional demo shop with intentional accessibility barriers. No real purchases.

Back to the demo shop

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.

Run a navigation audit with the dialog closed. Open lamp settings, keep the dialog open and run a Lighthouse snapshot and a separate axe scan. The barriers close button has no name. The improved button has a name. Use Escape to recover.

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.

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;}