Component Library Overview
A robust, reusable component library ensures consistency, accessibility compliance, and accelerated development across 223+ pages. The library must accommodate specialized content types demanded by legal professionals, data journalists, and clinicians.
Alert Components
Alert Levels
| Level | Color | Usage |
|---|---|---|
| Info | Blue | General updates, context |
| Warning | Orange | Approaching deadlines, policy shifts |
| Critical | Red | Immediate threats, emergencies |
| Success | Green | Completed actions, confirmations |
HTML Structure
<!-- Info Alert -->
<div class="alert alert--info" role="note">
<span class="alert__icon" aria-hidden="true">βΉοΈ</span>
<div class="alert__content">
<p>DACA renewal applications are currently being processed.</p>
</div>
</div>
<!-- Warning Alert -->
<div class="alert alert--warning" role="alert">
<span class="alert__icon" aria-hidden="true">β οΈ</span>
<div class="alert__content">
<p><strong>Deadline approaching:</strong> TPS re-registration ends April 15, 2026.</p>
</div>
</div>
<!-- Critical Alert -->
<div class="alert alert--critical" role="alert" aria-live="assertive">
<span class="alert__icon" aria-hidden="true">π¨</span>
<div class="alert__content">
<p><strong>Emergency:</strong> ICE enforcement activity reported in this area.</p>
<a href="/emergency/">View safety resources</a>
</div>
</div>
CSS Styles
.alert {
display: flex;
gap: 1rem;
padding: 1rem 1.25rem;
border-radius: 8px;
margin: 1.5rem 0;
}
.alert--info {
background: var(--color-info-bg);
border: 1px solid var(--color-info);
}
.alert--warning {
background: var(--color-warning-bg);
border-left: 4px solid var(--color-warning);
}
.alert--critical {
background: var(--color-error-bg);
border: 2px solid var(--color-error);
}
.alert__icon {
flex-shrink: 0;
font-size: 1.25rem;
}
Dismissible Alerts
<div class="alert alert--dismissible" role="alert">
<div class="alert__content">
<p>Policy update available.</p>
</div>
<button class="alert__dismiss" aria-label="Dismiss alert">
<span aria-hidden="true">Γ</span>
</button>
</div>
// Remember dismissal
document.querySelectorAll('.alert__dismiss').forEach(btn => {
btn.addEventListener('click', () => {
const alert = btn.closest('.alert');
const alertId = alert.dataset.alertId;
localStorage.setItem(`dismissed-${alertId}`, 'true');
alert.remove();
});
});
Card Components
Resource Card
<article class="card card--resource">
<header class="card__header">
<h3 class="card__title">
<a href="/resources/legal-documents/organization-name/">
Immigration Legal Aid Center
</a>
</h3>
<span class="card__badge">Free Services</span>
</header>
<div class="card__body">
<address class="card__address">
123 Main Street, Los Angeles, CA 90012
</address>
<p class="card__languages">
<strong>Languages:</strong> English, Spanish, Chinese
</p>
<p class="card__hours">
<strong>Hours:</strong> Mon-Fri 9am-5pm
</p>
</div>
<footer class="card__footer">
<a href="tel:+12135551234" class="button button--primary">
Call: (213) 555-1234
</a>
<a href="/directions/" class="button button--secondary">
Get Directions
</a>
</footer>
</article>
.card {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.card__header {
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--color-border);
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.card__title {
margin: 0;
font-size: var(--text-lg);
}
.card__badge {
background: var(--color-success-bg);
color: var(--color-success);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: var(--text-sm);
font-weight: 500;
}
.card__body {
padding: 1.25rem;
flex: 1;
}
.card__footer {
padding: 1rem 1.25rem;
background: var(--color-bg-subtle);
display: flex;
gap: 0.75rem;
}
Facility Card
<article class="card card--facility">
<div class="card__status card__status--active">
Active Facility
</div>
<header class="card__header">
<h3 class="card__title">Adelanto ICE Processing Center</h3>
<span class="card__type">Contract Detention Facility</span>
</header>
<div class="card__body">
<dl class="card__stats">
<div>
<dt>Capacity</dt>
<dd>1,940</dd>
</div>
<div>
<dt>Current Population</dt>
<dd>1,456</dd>
</div>
<div>
<dt>ERO Field Office</dt>
<dd>Los Angeles</dd>
</div>
</dl>
</div>
</article>
Accordion Components
FAQ Accordion
<div class="accordion" role="region" aria-labelledby="faq-heading">
<h2 id="faq-heading">Frequently Asked Questions</h2>
<div class="accordion__item">
<h3>
<button class="accordion__trigger"
aria-expanded="false"
aria-controls="panel-1"
id="accordion-1">
<span class="accordion__title">
What if ICE comes to my door?
</span>
<span class="accordion__icon" aria-hidden="true"></span>
</button>
</h3>
<div class="accordion__panel"
id="panel-1"
role="region"
aria-labelledby="accordion-1"
hidden>
<p>You have the right to not open your door unless...</p>
</div>
</div>
<div class="accordion__item">
<h3>
<button class="accordion__trigger"
aria-expanded="false"
aria-controls="panel-2"
id="accordion-2">
<span class="accordion__title">
Do I have to answer questions?
</span>
<span class="accordion__icon" aria-hidden="true"></span>
</button>
</h3>
<div class="accordion__panel"
id="panel-2"
role="region"
aria-labelledby="accordion-2"
hidden>
<p>You have the right to remain silent...</p>
</div>
</div>
</div>
.accordion__trigger {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: var(--text-base);
}
.accordion__trigger[aria-expanded="true"] {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.accordion__icon::after {
content: "+";
font-size: 1.5rem;
font-weight: 300;
}
.accordion__trigger[aria-expanded="true"] .accordion__icon::after {
content: "β";
}
.accordion__panel {
padding: 1.25rem;
border: 1px solid var(--color-border);
border-top: none;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.accordion__item + .accordion__item {
margin-top: 0.5rem;
}
// Accordion behavior
document.querySelectorAll('.accordion__trigger').forEach(trigger => {
trigger.addEventListener('click', () => {
const expanded = trigger.getAttribute('aria-expanded') === 'true';
const panel = document.getElementById(trigger.getAttribute('aria-controls'));
trigger.setAttribute('aria-expanded', !expanded);
panel.hidden = expanded;
});
});
// Open accordion from anchor link
if (window.location.hash) {
const targetPanel = document.querySelector(window.location.hash);
if (targetPanel?.classList.contains('accordion__panel')) {
const trigger = document.querySelector(`[aria-controls="${targetPanel.id}"]`);
trigger?.setAttribute('aria-expanded', 'true');
targetPanel.hidden = false;
}
}
Form Components
Input Fields
<div class="form-field">
<label for="name" class="form-field__label">
Full Name <span class="required">(Required)</span>
</label>
<input type="text"
id="name"
name="name"
class="form-field__input"
required
aria-describedby="name-hint">
<p id="name-hint" class="form-field__hint">
Enter your name as it appears on legal documents
</p>
</div>
<div class="form-field form-field--error">
<label for="phone" class="form-field__label">
Phone Number <span class="required">(Required)</span>
</label>
<input type="tel"
id="phone"
name="phone"
class="form-field__input"
aria-invalid="true"
aria-describedby="phone-error">
<p id="phone-error" class="form-field__error" role="alert">
Please enter a valid 10-digit phone number
</p>
</div>
.form-field {
margin-bottom: 1.5rem;
}
.form-field__label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-field__input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--color-border);
border-radius: 8px;
font-size: var(--text-base);
}
.form-field__input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-light);
}
.form-field--error .form-field__input {
border-color: var(--color-error);
}
.form-field__error {
color: var(--color-error);
font-size: var(--text-sm);
margin-top: 0.5rem;
}
.form-field__error::before {
content: "β ";
}
.required {
color: var(--color-error);
font-size: var(--text-sm);
}
Anonymous Submission Notice
<div class="anonymous-notice">
<span class="anonymous-notice__icon" aria-hidden="true">π</span>
<div class="anonymous-notice__content">
<strong>Anonymous Submission</strong>
<p>We do not collect IP addresses or identifying information.</p>
</div>
</div>
Quick Exit Component
Critical Safety Feature
<button class="quick-exit" id="quick-exit">
<span class="quick-exit__icon" aria-hidden="true">β</span>
<span class="quick-exit__text">Quick Exit</span>
</button>
.quick-exit {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 9999;
background: var(--color-error);
color: white;
border: none;
padding: 0.75rem 1.25rem;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.quick-exit:hover,
.quick-exit:focus {
background: var(--color-error-dark);
}
.quick-exit:focus {
outline: 3px solid var(--color-error-light);
outline-offset: 2px;
}
/* Mobile: larger target */
@media (max-width: 768px) {
.quick-exit {
padding: 1rem 1.5rem;
font-size: 1.125rem;
}
}
// Quick Exit functionality
const quickExit = document.getElementById('quick-exit');
const safeUrl = 'https://weather.com';
quickExit.addEventListener('click', () => {
// Replace history to prevent back button
window.location.replace(safeUrl);
});
// Escape key trigger (press twice)
let escapeCount = 0;
let escapeTimer;
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
escapeCount++;
clearTimeout(escapeTimer);
if (escapeCount >= 2) {
window.location.replace(safeUrl);
}
escapeTimer = setTimeout(() => {
escapeCount = 0;
}, 500);
}
});
Navigation Components
Header Navigation
<header class="site-header">
<div class="site-header__container">
<a href="/" class="site-header__logo">
Immigration Rights Guide
</a>
<nav class="site-header__nav" aria-label="Main navigation">
<ul class="nav-list">
<li><a href="/emergency/" class="nav-list__link nav-list__link--emergency">Emergency</a></li>
<li><a href="/know-your-rights/" class="nav-list__link">Know Your Rights</a></li>
<li><a href="/find-help/" class="nav-list__link">Find Help</a></li>
<li><a href="/resources/" class="nav-list__link">Resources</a></li>
</ul>
</nav>
<div class="site-header__actions">
<a href="/es/" class="lang-switch" lang="es">EspaΓ±ol</a>
<button class="quick-exit quick-exit--header">Exit</button>
</div>
</div>
</header>
Breadcrumbs
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol class="breadcrumbs__list">
<li class="breadcrumbs__item">
<a href="/">Home</a>
</li>
<li class="breadcrumbs__item">
<a href="/know-your-rights/">Know Your Rights</a>
</li>
<li class="breadcrumbs__item" aria-current="page">
<span>Home Raids</span>
</li>
</ol>
</nav>
.breadcrumbs__list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
list-style: none;
padding: 0;
font-size: var(--text-sm);
}
.breadcrumbs__item:not(:last-child)::after {
content: "/";
margin-left: 0.5rem;
color: var(--color-text-muted);
}
.breadcrumbs__item[aria-current="page"] {
color: var(--color-text-muted);
}
Bottom Navigation (Mobile)
<nav class="bottom-nav" aria-label="Quick navigation">
<a href="/emergency/" class="bottom-nav__item bottom-nav__item--emergency">
<span class="bottom-nav__icon" aria-hidden="true">π</span>
<span class="bottom-nav__label">Emergency</span>
</a>
<a href="/know-your-rights/" class="bottom-nav__item">
<span class="bottom-nav__icon" aria-hidden="true">π</span>
<span class="bottom-nav__label">Rights</span>
</a>
<a href="/find-help/" class="bottom-nav__item">
<span class="bottom-nav__icon" aria-hidden="true">π</span>
<span class="bottom-nav__label">Find Help</span>
</a>
</nav>
Button Components
Button Variants
<button class="button button--primary">Primary Action</button>
<button class="button button--secondary">Secondary Action</button>
<button class="button button--tertiary">Tertiary Action</button>
<button class="button button--emergency">Emergency Action</button>
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
min-height: 48px;
font-size: var(--text-base);
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
.button--primary {
background: var(--color-primary);
color: white;
border: 2px solid transparent;
}
.button--primary:hover {
background: var(--color-primary-dark);
}
.button--secondary {
background: transparent;
color: var(--color-primary);
border: 2px solid var(--color-primary);
}
.button--tertiary {
background: none;
color: var(--color-primary);
border: none;
text-decoration: underline;
padding: 0.5rem;
}
.button--emergency {
background: var(--color-error);
color: white;
border: none;
min-height: 56px;
font-weight: bold;
}
Legal Citation Popovers
Traditional legal footnotes force users to scroll to the bottom of the page, causing them to lose their cognitive anchor within the text. Legal citations must be handled inline using the modern HTML popover API or ARIA-compliant tooltips.
Interaction Design
| Device Type | Trigger | Dismiss |
|---|---|---|
| Pointer (mouse) | :hover |
Move away |
| Touch | Tap | Tap outside |
| Keyboard | :focus |
Escape key |
Citation Popover Component
<span class="citation-wrapper">
<a href="#cite-1"
class="citation-trigger"
aria-describedby="popover-cite-1"
popovertarget="popover-cite-1">
<cite>Reno v. Flores, 507 U.S. 292 (1993)</cite>
</a>
<div id="popover-cite-1"
class="citation-popover"
popover>
<div class="citation-popover__header">
<strong>Reno v. Flores</strong>
<button popovertarget="popover-cite-1"
popovertargetaction="hide"
aria-label="Close citation">Γ</button>
</div>
<div class="citation-popover__content">
<p><strong>Citation:</strong> 507 U.S. 292 (1993)</p>
<p><strong>Court:</strong> Supreme Court of the United States</p>
<p><strong>Holding:</strong> INS regulation requiring
detention of alien juveniles was facially valid...</p>
<a href="https://supreme.justia.com/cases/federal/us/507/292/"
target="_blank" rel="noopener">
View Full Opinion β
</a>
</div>
</div>
</span>
CSS Styles
.citation-trigger {
color: var(--color-primary);
text-decoration: underline;
text-decoration-style: dotted;
cursor: help;
}
.citation-popover {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
max-width: 400px;
padding: 0;
/* Popover positioning */
position-anchor: --citation-anchor;
inset-area: block-end span-inline-end;
margin: 0.5rem 0;
}
.citation-popover__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-subtle);
}
.citation-popover__content {
padding: 1rem;
font-size: var(--text-sm);
line-height: 1.6;
}
.citation-popover__content p {
margin: 0.5rem 0;
}
/* Fallback for browsers without popover API */
@supports not selector(:popover-open) {
.citation-popover {
display: none;
position: absolute;
z-index: 100;
}
.citation-trigger:hover + .citation-popover,
.citation-trigger:focus + .citation-popover {
display: block;
}
}
Copy to Clipboard Integration
Every citation must feature frictionless copy functionality for legal brief drafting:
document.querySelectorAll('.citation-popover').forEach(popover => {
const copyBtn = popover.querySelector('.copy-citation');
const citationText = popover.dataset.bluebook;
copyBtn?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(citationText);
copyBtn.textContent = 'β Copied';
setTimeout(() => copyBtn.textContent = 'Copy Citation', 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
});
});
Complex Data Tables
Comparative legal analysis (e.g., detailing differences between Bivens Actions and FTCA Claims) relies heavily on structured data tables.
Desktop: Sticky Headers
Tables require sticky <thead> elements to maintain context during extensive vertical scrolling.
<div class="table-wrapper">
<table class="data-table data-table--sticky">
<thead>
<tr>
<th scope="col">Characteristic</th>
<th scope="col">Bivens Action</th>
<th scope="col">FTCA Claim</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Defendant</th>
<td>Individual federal officers</td>
<td>United States government</td>
</tr>
<tr>
<th scope="row">Immunity</th>
<td>Qualified immunity applies</td>
<td>Sovereign immunity waived</td>
</tr>
<tr>
<th scope="row">Damages</th>
<td>Compensatory + punitive</td>
<td>Compensatory only</td>
</tr>
<tr>
<th scope="row">Jury Trial</th>
<td>Available</td>
<td>Not available (bench trial)</td>
</tr>
<tr>
<th scope="row">Statute of Limitations</th>
<td>Varies by state (1-6 years)</td>
<td>2 years from accrual</td>
</tr>
</tbody>
</table>
</div>
CSS: Sticky and Responsive
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-sm);
}
.data-table th,
.data-table td {
padding: 0.75rem 1rem;
border: 1px solid var(--color-border);
text-align: left;
}
.data-table thead th {
background: var(--color-bg-subtle);
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
}
.data-table--sticky thead th {
box-shadow: 0 2px 0 var(--color-border);
}
.data-table tbody th {
font-weight: 500;
background: var(--color-bg);
}
.data-table tbody tr:hover {
background: var(--color-highlight);
}
Mobile: Card Transform
Below 768px, each table row transforms into an individual, self-contained card:
@media (max-width: 768px) {
.data-table thead {
display: none;
}
.data-table tbody tr {
display: block;
margin-bottom: 1rem;
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 1rem;
}
.data-table tbody th,
.data-table tbody td {
display: block;
border: none;
padding: 0.5rem 0;
}
.data-table tbody th {
font-size: var(--text-lg);
font-weight: 600;
border-bottom: 1px solid var(--color-border);
margin-bottom: 0.5rem;
}
.data-table tbody td::before {
content: attr(data-label);
font-weight: 600;
display: block;
margin-bottom: 0.25rem;
color: var(--color-text-muted);
}
}
11ty Data Labels
{# Add data-label attributes for mobile card transform #}
{% for row in tableData %}
<tr>
<th scope="row">{{ row.characteristic }}</th>
<td data-label="Bivens Action">{{ row.bivens }}</td>
<td data-label="FTCA Claim">{{ row.ftca }}</td>
</tr>
{% endfor %}
Interactive Legal Timelines
Visualizing immigration court proceedings, detention lengths, or appellate deadlines requires robust timelines.
Vertical Timeline Pattern
Vertical orientation ensures seamless degradation to mobile viewports without breaking chronological flow.
<div class="legal-timeline" role="list" aria-label="Immigration Court Process Timeline">
<div class="timeline-item" role="listitem">
<div class="timeline-marker timeline-marker--complete">
<span class="sr-only">Completed</span>
</div>
<div class="timeline-content">
<details class="timeline-details">
<summary class="timeline-summary">
<time datetime="2026-01-15">January 15, 2026</time>
<h3>Master Calendar Hearing</h3>
</summary>
<div class="timeline-expanded">
<p><strong>Purpose:</strong> Initial appearance before immigration judge</p>
<p><strong>Required Forms:</strong> EOIR-28 (if represented)</p>
<p><strong>Outcome:</strong> Pleadings taken, individual hearing scheduled</p>
<a href="/legal/court/master-calendar/">Learn more about Master Calendar Hearings β</a>
</div>
</details>
</div>
</div>
<div class="timeline-item" role="listitem">
<div class="timeline-marker timeline-marker--current">
<span class="sr-only">Current</span>
</div>
<div class="timeline-content">
<details class="timeline-details" open>
<summary class="timeline-summary">
<time datetime="2026-03-20">March 20, 2026</time>
<h3>Individual (Merits) Hearing</h3>
</summary>
<div class="timeline-expanded">
<p><strong>Purpose:</strong> Present evidence and testimony</p>
<p><strong>Deadline:</strong> All evidence must be submitted 15 days prior</p>
<p><strong>Required:</strong> I-589, supporting declarations, country conditions</p>
<div class="timeline-alert">
<strong>Action Required:</strong> Submit evidence package by March 5, 2026
</div>
</div>
</details>
</div>
</div>
<div class="timeline-item" role="listitem">
<div class="timeline-marker timeline-marker--future">
<span class="sr-only">Upcoming</span>
</div>
<div class="timeline-content">
<details class="timeline-details">
<summary class="timeline-summary">
<time datetime="2026-04-01">After Decision</time>
<h3>Appeal to BIA (if denied)</h3>
</summary>
<div class="timeline-expanded">
<p><strong>Deadline:</strong> 30 days from IJ decision</p>
<p><strong>Form:</strong> EOIR-26 Notice of Appeal</p>
<a href="/legal/appeals/bia/">BIA Appeals Guide β</a>
</div>
</details>
</div>
</div>
</div>
Timeline CSS
.legal-timeline {
position: relative;
padding-left: 2rem;
}
.legal-timeline::before {
content: '';
position: absolute;
left: 0.5rem;
top: 0;
bottom: 0;
width: 2px;
background: var(--color-border);
}
.timeline-item {
position: relative;
padding-bottom: 2rem;
}
.timeline-marker {
position: absolute;
left: -1.75rem;
width: 1rem;
height: 1rem;
border-radius: 50%;
border: 2px solid var(--color-border);
background: var(--color-bg);
}
.timeline-marker--complete {
background: var(--color-success);
border-color: var(--color-success);
}
.timeline-marker--current {
background: var(--color-primary);
border-color: var(--color-primary);
box-shadow: 0 0 0 4px var(--color-primary-light);
}
.timeline-marker--future {
background: var(--color-bg);
border-color: var(--color-border);
}
.timeline-summary {
display: flex;
flex-direction: column;
cursor: pointer;
padding: 0.5rem 0;
}
.timeline-summary time {
font-size: var(--text-sm);
color: var(--color-text-muted);
}
.timeline-summary h3 {
margin: 0;
font-size: var(--text-base);
}
.timeline-expanded {
padding: 1rem;
margin-top: 0.5rem;
background: var(--color-bg-subtle);
border-radius: 8px;
}
.timeline-alert {
margin-top: 1rem;
padding: 0.75rem;
background: var(--color-warning-bg);
border-left: 4px solid var(--color-warning);
border-radius: 0 4px 4px 0;
}
Clinical Assessment Forms
Digitizing critical screening tools like the Refugee Health Screener (RHS-15) and Patient Health Questionnaire (PHQ-2) requires specialized form UX designed to prevent user error.
Form Structure with Fieldset/Legend
Questions must be grouped logically using semantic HTML:
<form class="assessment-form" id="phq2-assessment">
<div class="assessment-form__privacy">
<span class="privacy-icon" aria-hidden="true">π</span>
<p>
<strong>Privacy Notice:</strong> Your responses are stored locally on your device
and are never transmitted to any server. This is a screening tool, not a diagnosis.
</p>
</div>
<fieldset class="assessment-section">
<legend>Over the last 2 weeks, how often have you been bothered by the following?</legend>
<div class="assessment-question">
<label id="q1-label">1. Little interest or pleasure in doing things</label>
<div class="assessment-options" role="radiogroup" aria-labelledby="q1-label">
<label class="assessment-option">
<input type="radio" name="q1" value="0" required>
<span>Not at all (0)</span>
</label>
<label class="assessment-option">
<input type="radio" name="q1" value="1">
<span>Several days (1)</span>
</label>
<label class="assessment-option">
<input type="radio" name="q1" value="2">
<span>More than half the days (2)</span>
</label>
<label class="assessment-option">
<input type="radio" name="q1" value="3">
<span>Nearly every day (3)</span>
</label>
</div>
</div>
<div class="assessment-question">
<label id="q2-label">2. Feeling down, depressed, or hopeless</label>
<div class="assessment-options" role="radiogroup" aria-labelledby="q2-label">
<label class="assessment-option">
<input type="radio" name="q2" value="0" required>
<span>Not at all (0)</span>
</label>
<label class="assessment-option">
<input type="radio" name="q2" value="1">
<span>Several days (1)</span>
</label>
<label class="assessment-option">
<input type="radio" name="q2" value="2">
<span>More than half the days (2)</span>
</label>
<label class="assessment-option">
<input type="radio" name="q2" value="3">
<span>Nearly every day (3)</span>
</label>
</div>
</div>
</fieldset>
<div class="assessment-score" aria-live="polite">
<h3>Screening Score</h3>
<div class="score-display">
<span class="score-value" id="total-score">0</span>
<span class="score-max">/ 6</span>
</div>
<p class="score-interpretation" id="score-interpretation">
Complete all questions to see interpretation
</p>
</div>
<div class="assessment-disclaimer">
<p>
<strong>Important:</strong> This screening tool indicates potential symptoms
and is not a medical diagnosis. A score of 3 or higher suggests possible
depression and warrants further clinical evaluation.
</p>
</div>
<div class="assessment-actions">
<button type="button" class="button button--secondary" id="save-progress">
Save Progress
</button>
<button type="button" class="button button--primary" id="find-provider">
Find Mental Health Provider
</button>
</div>
</form>
Real-Time Scoring JavaScript
const form = document.getElementById('phq2-assessment');
const scoreDisplay = document.getElementById('total-score');
const interpretation = document.getElementById('score-interpretation');
const interpretations = {
0: { text: 'Minimal symptoms', level: 'low' },
1: { text: 'Minimal symptoms', level: 'low' },
2: { text: 'Minimal symptoms', level: 'low' },
3: { text: 'Possible depression - clinical evaluation recommended', level: 'moderate' },
4: { text: 'Possible depression - clinical evaluation recommended', level: 'moderate' },
5: { text: 'Probable depression - clinical evaluation strongly recommended', level: 'high' },
6: { text: 'Probable depression - clinical evaluation strongly recommended', level: 'high' }
};
function calculateScore() {
const formData = new FormData(form);
let total = 0;
for (const [key, value] of formData) {
if (key.startsWith('q')) {
total += parseInt(value, 10);
}
}
scoreDisplay.textContent = total;
const result = interpretations[total];
if (result) {
interpretation.textContent = result.text;
interpretation.className = `score-interpretation score-interpretation--${result.level}`;
}
// Auto-save to localStorage
localStorage.setItem('phq2-progress', JSON.stringify(Object.fromEntries(formData)));
}
// Listen for changes
form.addEventListener('change', calculateScore);
// Restore saved progress
document.addEventListener('DOMContentLoaded', () => {
const saved = localStorage.getItem('phq2-progress');
if (saved) {
const data = JSON.parse(saved);
for (const [name, value] of Object.entries(data)) {
const input = form.querySelector(`input[name="${name}"][value="${value}"]`);
if (input) input.checked = true;
}
calculateScore();
}
});
Assessment Form CSS
.assessment-form__privacy {
display: flex;
gap: 1rem;
padding: 1rem;
background: var(--color-info-bg);
border: 1px solid var(--color-info);
border-radius: 8px;
margin-bottom: 2rem;
}
.assessment-section {
border: none;
padding: 0;
margin-bottom: 2rem;
}
.assessment-section legend {
font-size: var(--text-lg);
font-weight: 600;
margin-bottom: 1.5rem;
}
.assessment-question {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.assessment-question label[id] {
display: block;
font-weight: 500;
margin-bottom: 1rem;
}
.assessment-options {
display: grid;
gap: 0.5rem;
}
.assessment-option {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border: 2px solid var(--color-border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.assessment-option:hover {
border-color: var(--color-primary);
background: var(--color-primary-bg);
}
.assessment-option:has(:checked) {
border-color: var(--color-primary);
background: var(--color-primary-bg);
}
.assessment-score {
text-align: center;
padding: 2rem;
background: var(--color-bg-subtle);
border-radius: 12px;
margin: 2rem 0;
}
.score-display {
font-size: 3rem;
font-weight: bold;
}
.score-value {
color: var(--color-primary);
}
.score-interpretation--low {
color: var(--color-success);
}
.score-interpretation--moderate {
color: var(--color-warning);
}
.score-interpretation--high {
color: var(--color-error);
font-weight: 600;
}
.assessment-disclaimer {
padding: 1rem;
background: var(--color-warning-bg);
border-left: 4px solid var(--color-warning);
border-radius: 0 8px 8px 0;
margin: 1.5rem 0;
}
Testing Checklist
Accessibility
- [ ] All interactive elements keyboard accessible
- [ ] Focus indicators visible
- [ ] ARIA attributes correctly applied
- [ ] Color contrast meets WCAG AA
Functionality
- [ ] Alerts dismissible and rememberable
- [ ] Accordions open/close correctly
- [ ] Quick Exit navigates immediately
- [ ] Forms validate properly
Responsiveness
- [ ] Components work on mobile
- [ ] Touch targets 56px minimum
- [ ] Cards stack properly
Related Resources
- Accessibility - ARIA patterns
- Visual Design - Color, typography
- Mobile-First - Touch targets
- Audience Journeys - User flow patterns
- Mental Health Resources - Clinical screening tools
- Information Architecture - Taxonomy design