Skip to content

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 lint passes (includes jsx-a11y rules)
  • [ ] All interactive elements are keyboard accessible
  • [ ] Images have descriptive alt text
  • [ ] 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 lighthouse and check no new regressions are introduced