skills/healthcare-ui-design/references/compliance-accessibility.md

499 lines
17 KiB
Markdown

# Compliance, Accessibility & Security UI Patterns
Cross-platform reference for HIPAA compliance, WCAG 2.2 AA accessibility, and security UI patterns: Web (Bootstrap 5/Tabler + PHP) and Android (Jetpack Compose + Material 3).
---
## 1. HIPAA UI Requirements
### 1.1 Protected Health Information (PHI) Handling
**Never display PHI in:**
- URLs or query parameters (`/patient?ssn=123-45-6789` is a violation)
- Browser tab titles or `<title>` tags (use "Patient Record" not "John Smith - DOB 1985")
- Push notification text (use "You have a new message" not "Lab result: HIV positive")
- Error messages or stack traces shown to users
- Console logs (`console.log(patient)` must be stripped in production)
- Autocomplete suggestions on PHI fields
**Mask sensitive identifiers:**
| Data Type | Display Format | Full Access |
|-----------|---------------|-------------|
| SSN | `***-**-6789` | Behind re-auth click |
| DOB | `**/**/1985` or age only | Role-dependent |
| MRN | Full (operational need) | Always visible to clinical |
| Phone | `(***) ***-4321` | Role-dependent |
| Address | City, State only | Role-dependent |
**Auto-lock and session timeout:**
- Lock screen after 15 minutes inactivity (configurable per facility)
- Show blurred overlay, not blank screen (preserves context awareness)
- Session timeout with state preservation: save draft to server, restore on re-login
- Countdown warning at 2 minutes before timeout
**Minimum Necessary Rule:** Only show PHI needed for the user's current role and task. A receptionist sees demographics, not diagnoses. A lab tech sees orders, not billing.
### 1.2 Audit Trail Requirements
Log every interaction with patient data:
| Event | Fields Logged |
|-------|---------------|
| Record access | user_id, patient_id, timestamp, IP, device, section_viewed |
| Data modification | user_id, field, old_value, new_value, reason, timestamp |
| Export/Print/Download | user_id, patient_id, format, fields_included, timestamp |
| Failed access attempt | user_id, patient_id, reason_denied, timestamp |
**UI requirements:**
- Display "Last accessed by [Name] on [Date]" on sensitive records
- Provide audit log viewer for compliance officers (filterable, exportable)
- Show access history tab on patient profiles (visible to admin roles)
### 1.3 Break-the-Glass Access
Emergency override for restricted patient records:
1. User clicks "Emergency Access" on restricted record
2. Modal requires: documented reason (dropdown + free text), acknowledgment checkbox
3. Access granted for limited window (default 4 hours, configurable)
4. Auto-notify privacy officer immediately
5. Access logged with elevated audit detail
6. Banner persists on record: "Emergency access active - expires [time]"
### 1.4 Platform-Specific HIPAA Controls
**Web (PHP):**
```php
// Secure session configuration
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.gc_maxlifetime', 900); // 15 min
header('Content-Security-Policy: default-src \'self\'; script-src \'self\'');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: no-referrer'); // Prevent PHI leaking in referrer
```
**Android (Kotlin):**
```kotlin
// Sensitive screen protection
class PatientRecordActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
}
}
// Encrypted storage for cached PHI
val prefs = EncryptedSharedPreferences.create(
"phi_cache",
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
```
---
## 2. WCAG 2.2 AA Compliance
### 2.1 Color Contrast Requirements
| Element | Minimum Ratio | Test Tool |
|---------|---------------|-----------|
| Normal text (<18px) | 4.5:1 | WebAIM Contrast Checker |
| Large text (18px+ or 14px+ bold) | 3:1 | Browser DevTools |
| Non-text (icons, borders, focus rings) | 3:1 | axe DevTools |
| Clinical status colors vs background | 4.5:1 | Test each status color |
**Critical:** Test ALL clinical status colors (critical red, warning amber, success green, info blue) against both light and dark card backgrounds.
### 2.2 Keyboard Navigation
Every healthcare screen must be fully keyboard-operable:
- **Tab** reaches every interactive element in logical order (top-to-bottom, left-to-right)
- **Focus indicators** are always visible: `outline: 2px solid var(--clinical-primary); outline-offset: 2px;`
- **Skip-to-content** link on every page (first focusable element)
- **Escape** closes all modals, dropdowns, and overlays
- **Enter/Space** activates buttons and toggles
- **Arrow keys** navigate within components (tabs, menus, date pickers)
**Web implementation:**
```html
<!-- Skip link — first element in <body> -->
<a href="#main-content" class="visually-hidden-focusable">
Skip to main content
</a>
<!-- Focus indicator — never use outline: none -->
<style>
:focus-visible {
outline: 2px solid var(--clinical-primary);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
</style>
```
### 2.3 Screen Reader Support
**Web ARIA patterns:**
```html
<!-- Vital signs with live updates -->
<div role="region" aria-label="Vital Signs Monitor" aria-live="polite">
<dl>
<dt>Heart Rate</dt>
<dd aria-label="Heart rate 72 beats per minute, normal">
<span class="vital-value">72</span> <span class="vital-unit">bpm</span>
<span class="badge badge-success">Normal</span>
</dd>
</dl>
</div>
<!-- Critical alert — assertive announcement -->
<div role="alert" aria-live="assertive">
Critical: Blood pressure reading 180/120 — immediate attention required
</div>
<!-- Table with proper scope headers -->
<table aria-label="Patient Medications">
<thead>
<tr><th scope="col">Medication</th><th scope="col">Dosage</th><th scope="col">Frequency</th><th scope="col">Status</th></tr>
</thead>
<tbody>
<tr><th scope="row">Metformin</th><td>500mg</td><td>Twice daily</td><td><span aria-label="Active">Active</span></td></tr>
</tbody>
</table>
<!-- ARIA landmarks -->
<header role="banner">...</header>
<nav role="navigation" aria-label="Main">...</nav>
<main role="main" id="main-content">...</main>
<aside role="complementary" aria-label="Patient summary">...</aside>
```
**Android Compose semantics:**
```kotlin
@Composable
fun VitalSignCard(label: String, value: String, unit: String, status: VitalStatus) {
Card(
modifier = Modifier.semantics(mergeDescendants = true) {
contentDescription = "$label $value $unit, ${status.label}"
stateDescription = status.label
if (status == VitalStatus.CRITICAL) liveRegion = LiveRegionMode.Assertive
}
) {
Text(text = label, style = MaterialTheme.typography.labelMedium)
Text(text = "$value $unit", style = MaterialTheme.typography.headlineMedium)
StatusBadge(status = status)
}
}
@Composable
fun CriticalAlert(message: String) {
Card(
colors = CardDefaults.cardColors(containerColor = ClinicalColors.CriticalLight),
modifier = Modifier.semantics {
liveRegion = LiveRegionMode.Assertive
contentDescription = "Critical alert: $message"
}
) { Text(text = message, color = ClinicalColors.Critical) }
}
```
### 2.4 Touch Target & Form Accessibility
**Touch targets:**
| Context | Web Minimum | Android Minimum | Healthcare Recommended |
|---------|-------------|-----------------|----------------------|
| Standard interactive | 44x44px | 48x48dp | 48px / 48dp |
| Primary clinical action | 48x48px | 56x56dp | 56px / 56dp |
| Gap between targets | 8px | 8dp | 8px / 8dp |
**Forms -- mandatory patterns:**
```html
<!-- ALWAYS visible labels, NEVER placeholder-only -->
<div class="mb-3">
<label for="allergies" class="form-label">
Known Allergies <span class="text-danger" aria-hidden="true">*</span>
<span class="visually-hidden">(required)</span>
</label>
<textarea id="allergies" class="form-control" required
aria-describedby="allergies-help allergies-error"></textarea>
<div id="allergies-help" class="form-text">List all known drug and food allergies</div>
<div id="allergies-error" class="invalid-feedback" role="alert">Required for patient safety</div>
</div>
<!-- Group related fields with fieldset/legend -->
<fieldset>
<legend>Emergency Contact</legend>
<label for="ec-name" class="form-label">Full Name</label>
<input type="text" id="ec-name" class="form-control mb-3">
<label for="ec-phone" class="form-label">Phone Number</label>
<input type="tel" id="ec-phone" class="form-control">
</fieldset>
```
---
## 3. Color-Blind Safe Design
**Rule:** Never use color alone to convey clinical meaning.
Every color indicator must be paired with at least one of: icon, text label, pattern, or shape.
### 3.1 Status Indicator Pattern
| Status | Color | Icon | Text Label | All Three Together |
|--------|-------|------|------------|-------------------|
| Critical | `#DC2626` | Warning triangle | "Critical" | Red triangle + "Critical" text |
| Warning | `#D97706` | Exclamation circle | "Warning" | Amber circle + "Warning" text |
| Normal | `#059669` | Checkmark | "Normal" | Green check + "Normal" text |
| Info | `#0284C7` | Info circle | "Pending" | Blue info + "Pending" text |
### 3.2 Chart Accessibility
For clinical charts (vitals over time, lab trends):
- Use different line patterns: solid, dashed, dotted, dash-dot
- Add distinct markers: circle, square, triangle, diamond
- Include data labels at key points
- Provide tabular data alternative
### 3.3 Safe Color Pairs
Instead of red/green (indistinguishable for 8% of males):
| Use Case | Recommended Pair | Avoid |
|----------|-----------------|-------|
| Good/Bad | Blue `#2563EB` + Orange `#D97706` | Red + Green |
| High contrast | Dark blue `#1E40AF` + Yellow `#CA8A04` | Red + Green |
| Status range | Blue/Amber/Dark gray | Red/Yellow/Green alone |
Test all palettes with Deuteranopia, Protanopia, and Tritanopia simulators (Chrome DevTools > Rendering > Emulate vision deficiencies).
---
## 4. Role-Based Access Control UI
### 4.1 UI Visibility Rules
| Strategy | When to Use |
|----------|-------------|
| **Hide** nav items/menu entries | User's role has zero access to the feature |
| **Disable** (gray out) buttons/actions | User can see but needs higher permission to act |
| **Show explanation** on unauthorized attempt | User clicks disabled action or navigates via URL |
Never rely on UI hiding alone for security. Server-side permission checks are mandatory.
### 4.2 Healthcare Role Hierarchy
| Role | Scope | Typical Access |
|------|-------|----------------|
| Super Admin | System-wide | Full platform configuration |
| Facility Admin | Facility | User management, facility settings, reports |
| Physician | Assigned patients | Records, orders, prescriptions, referrals |
| Nurse | Assigned ward/unit | Vitals, medication admin, notes, assessments |
| Lab Technician | Lab module | Lab orders, results entry, specimen tracking |
| Pharmacist | Pharmacy module | Medication orders, dispensing, interactions |
| Receptionist | Front desk | Scheduling, check-in, demographics, billing |
| Patient | Own records | View records, appointments, messages, payments |
### 4.3 Platform Integration
**Web:** Use `dual-auth-rbac` skill for server-side checks + UI filtering:
```php
// Server-side: always check before rendering
if ($auth->hasPermission('view_patient_records')) {
include 'partials/patient-records-nav.php';
}
// Client-side disabled state for visible but restricted actions
<button class="btn btn-primary" <?= !$auth->hasPermission('prescribe') ? 'disabled aria-disabled="true" title="Requires physician role"' : '' ?>>
Write Prescription
</button>
```
**Android:** Use `mobile-rbac` skill PermissionGate composable:
```kotlin
PermissionGate(permission = "view_patient_records") {
PatientRecordsScreen()
}
// Disabled button for visible but restricted actions
PermissionButton(
permission = "prescribe",
onClick = { navController.navigate("prescribe/$patientId") },
disabledMessage = "Requires physician role"
) {
Text("Write Prescription")
}
```
---
## 5. Data Privacy UI Patterns
### 5.1 Consent Management
Provide granular, revocable consent toggles:
```
Consent Settings
-------------------------------------------------
[ON] Share records with my care team
[OFF] Share anonymized data with research
[OFF] Share records with insurance provider
[ON] Receive appointment reminders via SMS
[OFF] Allow third-party health app access
-------------------------------------------------
Last updated: Feb 15, 2026 | Version 3
[View full consent agreement] [Download my consent history]
```
**Requirements:**
- Each toggle has a plain-language explanation (not legal jargon)
- Revocable at any time from patient settings
- All consent changes are version-tracked with timestamps
- Consent state is checked server-side before any data sharing
### 5.2 Data Portability
- **Export my health data** -- FHIR R4 format (JSON) or human-readable PDF
- **Request data deletion** -- with clear notice of legal retention periods (e.g., "Records retained 7 years per state law")
- **Data sharing dashboard** -- shows who has access to what, with revoke option
### 5.3 Privacy Notices
- Layered notice: short summary visible, full policy one click away (plain language, 6th grade reading level)
- Web: cookie/tracking consent banner (GDPR/state law) | Android: privacy policy in settings, consent at onboarding
---
## 6. Security UI Patterns
### 6.1 Authentication
- **MFA required** for all clinical users (password + OTP or biometric)
- **Session indicator:** lock icon + "Secure Session" in header
- **Failed login handling:** lockout after 5 consecutive failures, CAPTCHA after 3
- **Password strength:** minimum 12 characters, complexity indicator, healthcare-grade policy
### 6.2 Sensitive Action Verification
Re-authenticate before high-risk actions:
| Action | Verification Method |
|--------|-------------------|
| Prescribing medication | Password or biometric re-entry |
| Accessing restricted record | Break-the-glass flow (Section 1.3) |
| Exporting patient data | Password + reason documentation |
| Overriding a block | Supervisor PIN entry |
| Digital signature | Credentials + displayed signer identity |
### 6.3 Copy/Paste and Screen Capture
**Web:**
```html
<!-- Warn on copy of PHI fields -->
<input type="text" id="ssn-field" class="phi-field"
oncopy="logPhiCopy('ssn', userId); showCopyWarning();"
style="user-select: none;" readonly>
<script>
function showCopyWarning() {
Swal.fire({
icon: 'warning',
title: 'PHI Copy Detected',
text: 'Copying protected health information has been logged.',
confirmButtonColor: 'var(--clinical-primary)'
});
}
</script>
```
**Android:**
```kotlin
// Prevent screenshots on sensitive screens
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
// Disable copy on PHI text fields
TextField(
value = maskedSsn,
onValueChange = {},
readOnly = true,
modifier = Modifier.semantics {
// Announce that copy is restricted
contentDescription = "Social Security Number, last four digits, copy restricted"
},
visualTransformation = SsnMaskTransformation()
)
```
---
## 7. Per-Screen Compliance Checklist
Use this checklist for **every** healthcare screen built:
### Accessibility
- [ ] Color contrast verified: 4.5:1 for text, 3:1 for non-text elements
- [ ] All color indicators paired with text labels and/or icons
- [ ] Keyboard navigation works completely (Tab, Enter, Escape, arrows)
- [ ] Visible focus indicators on all interactive elements
- [ ] Screen reader announces all content correctly (test with NVDA/TalkBack)
- [ ] Touch targets meet minimum 48dp (Android) / 44px (web)
- [ ] Forms have visible labels, not placeholder-only
- [ ] Error messages programmatically associated with fields (aria-describedby)
- [ ] Loading states announced via `aria-live` regions
- [ ] Skip-to-content link present (web)
### HIPAA & Privacy
- [ ] PHI not exposed in URLs, page titles, or browser tabs
- [ ] PHI not logged to console or included in error messages
- [ ] Sensitive identifiers masked (SSN, DOB per role)
- [ ] Session timeout implemented with state preservation
- [ ] Audit logging for all data access and modifications
- [ ] "Last accessed by" displayed on sensitive records
- [ ] Minimum Necessary Rule applied (role-scoped data)
### Security
- [ ] Role-based visibility applied (hide/disable per permission)
- [ ] Server-side permission check backs up every UI gate
- [ ] Re-authentication required for sensitive actions
- [ ] FLAG_SECURE set on sensitive Android screens
- [ ] CSP headers and secure cookies configured (web)
- [ ] Copy/paste restrictions on PHI fields with logging
- [ ] Break-the-glass flow available for emergency access
---
**Cross-references:** `dual-auth-rbac` for auth patterns | `mobile-rbac` for Android permissions | `vibe-security-skill` for general web security | `design-tokens.md` for clinical color values