17 KiB
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-6789is 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:
- User clicks "Emergency Access" on restricted record
- Modal requires: documented reason (dropdown + free text), acknowledgment checkbox
- Access granted for limited window (default 4 hours, configurable)
- Auto-notify privacy officer immediately
- Access logged with elevated audit detail
- Banner persists on record: "Emergency access active - expires [time]"
1.4 Platform-Specific HIPAA Controls
Web (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):
// 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:
<!-- 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:
<!-- 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:
@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:
<!-- 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:
// 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:
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:
<!-- 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:
// 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-liveregions - 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