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.
Dieser Fall enthält sechs gezielte Kandidaten: ein unbenanntes Diagramm, einen mit Tab nicht erreichbaren Scrollbereich, verschachtelte Bedienelemente, feste Inline-Zeichenabstände, eine leere Aufklappbeschriftung und einen unbenannten Tab. Die verbesserte Variante korrigiert diese Stellen. Tastatur- und Dialogfälle haben eigene URLs.

Demo-Produkt · kein Kauf
Schreibtischleuchte
49,00 €
Olivgrünes Metall, ein runder Schirm und warmes Licht für Deinen Schreibtisch. Dieses Produkt ist erfunden.
Zwei Helligkeitsstufen
Die Balken zeigen die niedrige und die hohe Helligkeitsstufe.
Lieferung und Aufbau
Die Leuchte kommt in einem schützenden Karton. Bewahre die Verpackung auf, bis Du alle Teile geprüft hast.
Stelle den Fuß auf eine feste Fläche. Lasse Platz um den Schirm und halte das Kabel von scharfen Kanten fern.
Wähle die Helligkeitsstufe am Kabelschalter. Ziehe vor dem Reinigen den Stecker.
Warmes Licht zum Lesen, Schreiben und für die letzte Aufgabe des Tages.
Pflegehinweise
Wische das Metall mit einem weichen, trockenen Tuch ab. Vermeide scheuernde Reiniger.
Metallgehäuse · 32 cm hoch · zwei Helligkeitsstufen.
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;}