Einzelbeispiel · v1
Lighthouse und axe: dieselbe Produktseite.
Eine Schreibtischleuchte mit gezielt eingebauten Barrieren. Vergleiche Regelauswahl, Tastaturbedienung und einen geöffneten Dialog in drei getrennten Fällen.
Du brauchst weder Anmeldung noch Warenkorb oder Cookie-Auswahl. Vorbereiten leert nur die vorübergehende Aktion und den Dialog dieses Beispiels. Setze für wiederholbare Scans die Darstellung auf 100 % Textgröße, ohne Textabstände und Vorschau.
Referenzversionen: Lighthouse 13.5.0, axe-core 4.13.0. Halte die Versionen Deiner eigenen Berichte fest.
Starte einen Navigation-Lauf bei geschlossenem Dialog. Öffne die Leuchteneinstellungen und lasse den Dialog für einen Lighthouse-Snapshot und einen separaten axe-Scan offen. Der Schließen-Button der Barrieren-Variante hat keinen Namen. Der verbesserte Button hat einen Namen. Escape bringt Dich zurück.

Demo-Produkt · kein Kauf
Schreibtischleuchte
49,00 €
Olivgrünes Metall, ein runder Schirm und warmes Licht für Deinen Schreibtisch. Dieses Produkt ist erfunden.
Gemeinsamen Quellcode ansehen
Dies sind die tatsächlichen Quelldateien dieses Beispiels. Der erzeugte Stand wird bei der Build-Validierung mit der Implementierung verglichen. Imports und gemeinsame Hilfsfunktionen bleiben sichtbar; es gibt keine getrennte Lehrkopie.
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;}