Accessibility & Lighthouse CI
This document covers Constellation's accessibility standards, how to run Lighthouse CI audits, and how to fix common issues.
Accessibility Standards
We target WCAG 2.1 Level AA compliance. Every pull request that touches UI must pass these checks.
Core Rules
| Rule | Requirement |
|---|---|
| Color contrast | Text must have โฅ 4.5:1 contrast ratio against its background (3:1 for large text). Use only theme variables from globals.css that exist in both :root and .dark. |
| Keyboard navigation | All interactive elements must be reachable and operable via keyboard. |
| Semantic HTML | Use <button> for actions, <a> for navigation, <ul>/<li> for lists. Avoid <div onClick>. |
| Alt text | All <Image> / <img> elements must have descriptive alt (or alt="" + aria-hidden="true" if purely decorative). |
| Form labels | Every <input>, <select>, <textarea> must have an associated <label> or aria-label. |
| ARIA attributes | Only use aria-* on elements whose role supports them. Prefer native HTML semantics over ARIA. |
| Focus indicators | Focus rings must be visible. Never remove outline without providing an alternative. |
| Document structure | Pages must have a <title>, one <h1>, and a logical heading hierarchy. |
Common Mistakes to Avoid
// โ Bad โ div used as a button, no accessible name
<div onClick={handleClick} className="cursor-pointer">
<Icon />
</div>
// โ
Good โ semantic button with accessible label
<button type="button" onClick={handleClick} aria-label="Close panel">
<Icon />
</button>
// โ Bad โ <li> outside of <ul>, or non-<li> inside <ul>
<SidebarGroup>
<SidebarMenuItem>...</SidebarMenuItem>
</SidebarGroup>
// โ
Good โ <li> wrapped in <ul>
<SidebarGroup>
<SidebarMenu>
<SidebarMenuItem>...</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
// โ Bad โ generic or empty alt text
<Image src="/photo.jpg" alt="Image" />
// โ
Good โ descriptive alt text
<Image src="/photo.jpg" alt="Team working on a constellation map" />
ESLint A11y Plugin
The project enforces accessibility rules via eslint-plugin-jsx-a11y:
// eslint.config.mjs
{
rules: {
...jsxA11y.configs.recommended.rules,
},
}
Run bun lint to catch violations before committing.
Google Lighthouse CI
Lighthouse CI audits every key page for Performance, Accessibility, Best Practices, and SEO.
The application is really heavy on the client side, so we set a more lenient threshold for Performance (โฅ 50%) while still enforcing โฅ 90% for the other categories.
Score Thresholds
| Category | Minimum Score |
|---|---|
| Performance | โฅ 0.5 (50%) |
| Accessibility | โฅ 0.9 (90%) |
| Best Practices | โฅ 0.9 (90%) |
| SEO | โฅ 0.9 (90%) |
These are configured in lighthouserc.js.
Pages Audited
Lighthouse runs against both public and authenticated pages:
/(landing page)/dashboard/home,/dashboard/graph,/dashboard/elements,/dashboard/maps/workspace(multiple view variants: files, elements, graph)/about,/blog,/about-us
Authentication is handled automatically via lighthouse-auth.js, which injects a token into localStorage before each run.
Running Lighthouse Locally
Prerequisites: The backend API must be running so the auth script can obtain a token.
# Full audit (builds, starts server, audits all pages, asserts thresholds)
LIGHTHOUSE_EMAIL=your@email.com LIGHTHOUSE_PASSWORD=yourpass bun run lighthouse
# Or with a pre-existing token
LIGHTHOUSE_TOKEN=your_jwt_token bun run lighthouse
# Collect only (no threshold assertion)
LIGHTHOUSE_EMAIL=your@email.com LIGHTHOUSE_PASSWORD=yourpass bun run lighthouse:collect
# Assert only (re-check thresholds on existing results)
bun run lighthouse:assert
Viewing Reports
Reports are saved as HTML files in .lighthouseci/:
# Open a specific report
xdg-open .lighthouseci/localhost--2026_02_09_22_07_43.report.html # Linux
open .lighthouseci/localhost--2026_02_09_22_07_43.report.html # macOS
# Open all reports
for f in .lighthouseci/localhost-*.report.html; do xdg-open "$f"; done
You can also drag and drop any .html report onto Lighthouse Viewer to get a shareable link.
Understanding Results
The assertion results summary is in .lighthouseci/assertion-results.json. Each entry shows:
{
"auditProperty": "accessibility",
"actual": 0.84,
"expected": 0.9,
"passed": false,
"url": "http://localhost:3000/dashboard/home"
}
For details, open the corresponding HTML report and look at the failed audits section.
Common Lighthouse Fixes
Accessibility
| Issue | Fix |
|---|---|
document-title |
Ensure root layout exports metadata with a title (server component, not "use client"). |
color-contrast |
Use theme CSS variables. For custom backgrounds, compute text color dynamically (see getContrastTextColor in environment-indicator.tsx). |
list / listitem |
Wrap <SidebarMenuItem> (<li>) inside <SidebarMenu> (<ul>). Never put non-<li> elements directly inside <ul>. |
aria-allowed-attr |
Don't put aria-haspopup on <div>. Use <button> as the trigger for dropdown/dialog. |
aria-command-name |
Elements with role="button" must have an accessible name via aria-label, text content, or aria-labelledby. |
button-name |
All <button> elements must have visible text or aria-label. |
Performance
| Issue | Fix |
|---|---|
uses-rel-preconnect |
Add <link rel="preconnect" href="..."> in the root layout <head> for third-party origins (fonts, analytics). |
render-blocking-resources |
Add &display=swap to Google Fonts URLs. Use next/font when possible. |
unminified-javascript |
This is usually a dev-mode artifact. Ensure you run bun build before auditing. |
unused-javascript |
Use dynamic imports (next/dynamic) for heavy components not needed on initial load. |
server-response-time |
This reflects the local build server. Not actionable for local audits but monitor in production. |
SEO
| Issue | Fix |
|---|---|
meta-description |
Export metadata with a description from the root layout or page. |
document-title |
Same as accessibility โ use Next.js metadata export. |
robots-txt |
Ensure public/robots.txt exists with proper directives. |
PR Checklist โ Accessibility & Performance
Before submitting a UI-related PR:
- [ ]
bun lintpasses (includesjsx-a11yrules) - [ ] All interactive elements are keyboard accessible
- [ ] Images have descriptive
alttext - [ ] Color contrast meets 4.5:1 (use browser DevTools to verify)
- [ ] No
<div onClick>โ use<button>or<a>instead - [ ] Lists use proper
<ul>/<li>nesting - [ ] Run
bun run lighthouseand check no new regressions are introduced