CRAIG UI Philosophy
On this page
This page is the durable engineering reference for how CRAIG’s UI is designed. It captures principles, constraints, and target architecture that future engineers — and future design partners — should treat as load-bearing.
It is NOT a roadmap (see plans/portals.adoc for unbuilt-portal sequencing and plans/ui-ux-overhaul.adoc for the existing-UI evolution history). It is NOT a how-to guide (see ui-overview.adoc for the role/module map and the per-domain design pages for screen-level walkthroughs). It IS the answer to "why do we build the UI this way" for any contributor.
Status
| Section | Description | Status |
|---|---|---|
1 |
Design principles |
Done (2026-06-08) — establishing the eight principles below |
2 |
Visual identity + per-jurisdiction theme model |
Done (2026-06-08) |
3 |
Multi-jurisdictional UX model |
Done (2026-06-08) |
4 |
Composability direction |
Done (2026-06-08) — target architecture; the engine, dashboard, and case-detail legs shipped since (ADR-035 / Plan X, #1552; see §Built so far, and what remains) |
5 |
Tech-stack constraints |
Done (2026-06-08) |
6 |
Performance budgets |
Done (2026-06-08) |
7 |
Accessibility floor |
Done (2026-06-08) |
8 |
Role + portal architecture |
Done (2026-06-08) |
9 |
Current UI surface inventory |
Done (2026-06-08) — kept current as code changes; PR-update on substantial surface changes |
10 |
Explicit non-goals |
Done (2026-06-08) |
Design principles
Eight principles that govern every UI decision. When two principles conflict, the earlier one wins.
-
Trauma-informed by default. Both worker-facing and constituent-facing surfaces serve users under stress. Copy is clear-not-clinical, error states are non-judgmental, destructive actions require confirmation, danger states are visually distinct from urgent-but-recoverable states.
-
Multi-jurisdictional first. Every label is a translation key, every admin-unit is jurisdiction-mapped, every brand element comes from the jurisdiction’s theme. A screen that hard-codes Georgia terminology, Georgia counties, or DHS branding is a bug.
-
Accessible by construction, not by audit. WCAG 2.1 AA + Section 508 compliance is non-negotiable. Components are designed with keyboard navigation, screen-reader semantics, and focus management baked in; a11y is not retrofitted.
-
Server-rendered first; client interactivity is layered. A CCWIS audit must be able to trace every state transition. Client-side state machines make that harder. HTMX + Alpine.js layer interactivity on top of server-rendered HTML; SPAs are not used.
-
Composable workspaces. Different roles do different work; one-size-fits-all dashboards fail everyone. Surfaces (dashboards, case details) support user-rearrangeable panels and sections with role-appropriate defaults.
-
Strict CSP, no exceptions.
default-src 'self'; script-src 'self'; style-src 'self'— no'unsafe-inline', no'unsafe-eval'. Designs that require inline styles or eval-based libraries are non-implementable. -
Self-teaching for common workflows. Most caseworkers receive minimal CRAIG-specific training. Empty states, progressive disclosure, and in-app guidance carry the onboarding burden; users should not need to consult an external manual for common tasks.
-
Shared design language, audience-appropriate presentation. All CRAIG surfaces share the same design tokens, primitives, accessibility patterns, and motion semantics. Presentation varies by audience: caseworker UI is dense / functional / efficient (power users, long sessions); foster-parent + CWCA portals are mid-density / professional / operational (moderate session length); constituent portal is spacious / calm / non-clinical (often-distressed users, short sessions, mobile-dominant). Same primitives, different compositions.
Visual identity model
Token-driven, per-jurisdiction theme
Every visual property is a token. Tokens are grouped into layers; each layer has a different mutability model.
| Layer | Examples | Mutability |
|---|---|---|
Brand |
|
Per-jurisdiction. Swapped at deploy time via the jurisdiction’s |
Surface |
|
Consistent across jurisdictions. The product should feel like the same product regardless of state branding. |
Semantic |
|
Consistent across jurisdictions. Semantic colors do not vary per state. |
Mode |
Light / dark / high-contrast palettes |
Per-user (with system-preference detection). Each mode is a parallel set of values for surface + semantic + a desaturated brand variant. |
Type |
Font family, type scale, line height, weight |
Mostly consistent; some jurisdictions may swap the primary face for a state-mandated alternative. |
Layout |
Grid gutters, max-width, breakpoints, radius, elevation, motion |
Consistent across jurisdictions. |
Starting point — Georgia theme
The first deployed jurisdiction is Georgia DHS. The current token values live at:
-
services/craig-web/static/css/tokens.css— runtime CSS custom properties -
services/craig-web/static/themes/georgia-orchard/— Georgia brand assets (logo, override CSS)
A more refined Georgia palette TOML exists at ~/code/canopy/design/canopy-web/canopy-handoff/georgia-theme.toml (sibling project; reference only). The CRAIG token model should evolve alongside whichever design partner is engaged at the time; the per-jurisdiction TOML pattern is durable.
Per-jurisdiction theming workflow
For a new state to onboard CRAIG:
-
Engineering scaffolds a new theme directory at
services/craig-web/static/themes/<jurisdiction>/ -
State provides brand assets (logo, agency name, optional font face)
-
State provides brand-layer color values (primary, primary_hover, accent)
-
Surface + semantic + mode + type + layout layers are inherited from the shared base (no per-state customization)
-
State’s terminology mappings are configured per §3
Ingesting a design-team handoff
The design partner delivers token and UI decisions as a zip handoff — typically a README (decisions + rationale + a per-mode WCAG-contrast table), a generated-CSS reference, patched palette TOML files, and a live proof HTML with a light/dark toggle. Two rules govern ingestion:
-
Apply only the additive key VALUES, not the file wholesale. A partner’s patched TOML carries cosmetic extras that must NOT land in CRAIG’s product-neutral palette — a
[theme]metadata block,fonts,logo_gold, an agency-specificagency_name(CRAIG’s is product-neutral), and comment churn.diffthe handoff palette against the corresponding theme TOML in the tree and apply ONLY the substantive key additions. -
Take the values; emit them through CRAIG’s own generator. A partner’s selector mechanism may differ from CRAIG’s. Design may hand off
[data-theme="dark"]/[data-contrast="high"]selectors, whereas CRAIG generates@media (prefers-color-scheme: dark)/@media (prefers-contrast: more)from the TOML’s[color.light]/[color.dark]/[color.high_contrast]maps viabuild.rs. Never copy a partner’s selectors — copy the color values into CRAIG’s model and let the generator emit the correct rules.
High-contrast values may need new generator plumbing before they can ship (parsing [color.high_contrast] and wiring it through the palette type); when that is the case, ship light/dark from the handoff and track the high-contrast wiring separately.
Multi-jurisdictional UX model
This is CRAIG’s hardest UI constraint and the largest deviation from a typical state-bound system.
What multi-jurisdictional means
CRAIG runs in any US state. Each state has:
-
Different terminology ("Case Worker" vs "Family Services Specialist" vs "Child Protective Investigator")
-
Different admin-unit labels (Georgia: 159 counties; Texas: 254 counties; some states use regions or districts or service areas)
-
Different intake protocols, mandated-reporter laws, time clocks for response
-
Different brand identity (state DHS logo, color, agency name)
-
Different languages of operation (English + Spanish in most; English + Spanish + additional languages in some)
-
Different federal-reporting nuances (AFCARS / NCANDS field expectations differ subtly per state)
There is a single CRAIG codebase. Every screen adapts per-jurisdiction.
What jurisdiction means in the UI
-
Brand swap — agency name, logo, primary color from the jurisdiction’s theme
-
Terminology lookup — every label, button, tooltip, error message is a translation key resolved against the jurisdiction-mapped terminology dictionary
-
Admin-unit rendering — "Fulton County" in Georgia, "Travis County" in Texas, "Region 4" in Hawaii
-
Workflow-rule rendering — intake response clocks, ICPC deadlines, payment periods all come from per-jurisdiction config
-
Feature toggles — some jurisdictions don’t use ICPC; some don’t use CWCA partners; the UI hides features the jurisdiction doesn’t use
Architecture surfaces
Four UI surfaces specifically address multi-jurisdictional requirements:
Jurisdiction theming model
Visual system that:
-
Allows brand layer to swap per-jurisdiction at deploy time
-
Keeps interaction patterns + information architecture identical across jurisdictions
-
Allows a screenshot to be re-rendered with a different state’s brand without losing visual coherence
Jurisdiction switcher (for cross-jurisdictional users)
Federal-level users (oversight, research) have access to multiple jurisdictions. The chrome must include an unobtrusive jurisdiction switcher that:
-
Makes the current jurisdiction unambiguously visible in chrome (not just a logo — a label)
-
Allows switching with one click; preserves screen context where possible
-
Communicates clearly when a user is acting in a non-home jurisdiction (e.g., a warning band)
Jurisdiction Studio (admin surface)
An admin surface for:
-
Viewing/editing this jurisdiction’s terminology mapping
-
Viewing/editing this jurisdiction’s brand layer (within constraints — e.g., contrast requirements)
-
Viewing/editing this jurisdiction’s admin-unit list
-
Viewing/editing this jurisdiction’s intake protocols and time clocks
-
Toggling feature flags (ICPC enabled/disabled, CWCA enabled/disabled)
Terminology-aware components
Every label-bearing component (buttons, tabs, table column headers, form labels) is a string-keyed terminology lookup, never a hard-coded label. Engineering treats every {key} as a translation key; the resolution happens at render time against the jurisdiction’s terminology dictionary.
Backend support
Multi-jurisdictional UX requires backend support that is already partly built:
-
craig-authzenforces record-level authorization per jurisdiction -
craig-referenceholds per-jurisdiction admin-unit registries (georgia159 +texas254 counties seeded) -
craig-rulesper-jurisdiction rulesets (-person-match,-intake-priority, etc.) -
ICPC deadline + payment-period config externalized per-jurisdiction
-
BFF and intake i18n hardcodes generic; specific terminology lookups TBD
What’s NOT yet built (relevant to UI):
-
Per-jurisdiction terminology dictionary (string-key → label resolution)
-
Per-jurisdiction feature-flag surface
-
Jurisdiction Studio admin UI
Composability direction
Why composability
Different workers do different work:
-
A CPS investigator’s day is intake-heavy + investigation-heavy + court-heavy
-
A foster care worker’s day is placement-heavy + family-engagement-heavy + visit-heavy
-
A supervisor’s day is approval-queue + caseload-monitoring + escalations
-
An ICPC coordinator’s day is interstate-coordination-heavy
One-size-fits-all dashboards fail everyone. The target architecture supports user-rearrangeable workspaces with role-appropriate defaults.
Dashboard composability
-
Dashboards consist of panels arranged on a grid
-
Panels come from a library (worklist, my approvals, my notifications, my recent cases, my upcoming court dates, jurisdiction alerts, caseload heatmap, etc.)
-
Users can add, remove, reorder, and resize panels
-
Per-role default layouts (a new caseworker gets caseworker defaults; a new supervisor gets supervisor defaults)
-
Per-user customizations persist across sessions
-
Per-jurisdiction default layouts can be set by admins via Jurisdiction Studio
Case-detail composability
The case-detail screen is the densest information surface in the product. Composability allows:
-
Sections (contacts, court orders, household, plans, attachments) can be hidden/shown per user preference
-
Section order is user-rearrangeable
-
Role-based default section ordering (a CPS investigator sees Investigation Notes first; a foster care worker sees Placements first)
-
Case-type-based defaults (a CPS case shows different sections than a Title IV-E eligibility case)
Saved views
For list surfaces (cases, placements, payments, audit log):
-
Users save filter + sort + column-set combinations as named views
-
Personal views are private; team views are shareable
-
Per-role default views shipped (e.g., "My Open Cases" for caseworkers)
Persistence model
Composability state lives in the database, scoped per:
-
User (personal layouts and views)
-
Role (default layouts shipped to all users of a role)
-
Jurisdiction (state-level default layouts)
The resolution order is: user override → role default → jurisdiction default → product default.
Built so far, and what remains
The composition layer engine shipped as ADR-035 / Plan X after this section was written:
craig-composition (port 8009) resolves role-filtered composed surfaces from
jurisdiction baselines that live as ops-edited rulesets/<jurisdiction>/<surface>.toml
files (bundles contribute only the surface DECLARATIONS, ADR-035 §8 — never baseline
trees). The dashboard renders its composed panel grid (lazy-loaded plugin panels with
engine-decided placement), composed case-detail sections render as tabs from the
engine-resolved case_detail surface (#1552/E4 — the GA baseline ships the
ssa-screening section, which replaced the native tab), the Jurisdiction Studio
(/studio/composition, admin-only) chooses the dashboard shell layout as a
jurisdiction-live override and shows the governed panels read-only, and users hold a
self-service personalize surface (own-sub-scoped overrides). Still unbuilt from the
target above: the five native case-detail tabs are not themselves composable (fixed in
the template), and saved views for list surfaces remain roadmap.
Tech-stack constraints
Server-rendered HTML first
CRAIG’s UI is rendered server-side via Askama templates (Rust). The server returns rendered HTML; the client never sees a JSON-then-render-in-browser pattern for primary content.
Layered client interactivity
Three layers, in order of preference:
-
Plain HTML — every interaction that can be a form submission is one
-
HTMX — for partial-fragment swaps (search-as-you-type, person-picker autocomplete, attachment lists, paginated tables-within-pages)
-
Alpine.js (CSP-friendly minified build) — for in-page interactivity that doesn’t require server round-trips (tabs, modals, dropdowns, table column resize, flash dismissal)
No SPA frameworks
No React, no Vue, no Svelte, no Angular. The constraint is deliberate:
-
A CCWIS audit must be able to trace every state transition; client-side state machines obscure this
-
Server-rendered HTML is dramatically faster on the typical state-government hardware/network combinations
-
Asset budgets stay small; first paint is fast
-
The skill bar for contributors is lower (Askama + htmx + Alpine is learnable in a day)
This is non-negotiable for CRAIG. If a feature requires client-side state management beyond what Alpine can express, the feature gets restructured — not the constraint.
Strict CSP
The CSP header (configured in services/craig-web/src/main.rs):
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
No 'unsafe-inline', no 'unsafe-eval'. Implications for design:
-
No inline
<style>blocks — all CSS is in external files -
No inline
style="…"attributes (Plan C § Step 14 retired the last 237 inlinestyle=instances; do not reintroduce) -
No eval-based animation or templating libraries
-
Alpine.js must be the CSP-friendly minified build (no eval of x-data expressions; static parser only)
Alpine.js load order (CSP gotcha)
Alpine x-data factory scripts must load before alpine.min.js. The pattern is implemented in base.html + report_base.html via {% block scripts_pre_alpine %}. Designers introducing a new Alpine component must place its factory in that block; otherwise Alpine’s CSP build silently no-ops the component.
Intake static assets
craig-intake uses include_str!() + explicit Axum routes (not ServeDir) for its static assets. New JS or CSS under services/craig-intake/static/ requires both the file and a serve_* handler + route in ui.rs. This is operational, not philosophical, but it bites every contributor who doesn’t know.
Performance budgets
These are firm targets, not aspirational:
| Surface | Budget | Why |
|---|---|---|
First contentful paint, constituent portal, 3G connection |
< 2s |
Most constituent users arrive via phone, often on metered connections |
First contentful paint, caseworker UI, broadband |
< 1s |
Caseworkers carry 50+ open tabs; slow paint kills throughput |
Server-side render, caseworker UI |
< 100ms p50, < 250ms p99 |
Caseworkers expect snap-fast navigation; the BFF caches aggressively but the structure must support this |
JS bundle, caseworker UI total |
< 100KB |
The current footprint (Alpine + htmx + small utilities) is well under; new dependencies must be justified |
JS bundle, portals total |
< 50KB |
Constituent portal users on phones; bundle budget is half the caseworker budget |
Bundle-size budget is enforced manually today; consider a CI gate if drift becomes a problem.
Accessibility floor
WCAG 2.1 AA + Section 508 are non-negotiable for government deployment. Specific commitments:
Visual
-
Color contrast meets WCAG AA at every type size used (verified in
tokens.csscomments for muted-text + warning-text) -
No information conveyed by color alone (icons + text labels reinforce semantic meaning)
-
Reduced-motion respected via
prefers-reduced-motion -
High-contrast mode supported (Windows high-contrast users; a top-of-mind audience for state systems)
-
Dark mode supported (currently placeholder; needs refinement)
Semantic HTML
-
Landmark roles on all chrome elements (
role="banner",role="navigation",role="main",role="alert") -
aria-current="page"on active nav links + breadcrumb current + pagination current -
<label>elements present on all inputs -
Tables use semantic
<table>/<thead>/<tbody>(no<div>-tables) -
Skip link at top of body (jumps to
#main-content)
Interaction
-
Every action reachable without a pointer (keyboard navigable)
-
Focus visible on every interactive element
-
Focus trapped on modals/dialogs (with focus return to the trigger on close)
-
Tab UI uses arrow-key navigation +
Home/Endper the WAI-ARIA Authoring Practices -
Form errors linked to inputs via
aria-describedby
Screen reader support
-
NVDA + JAWS tested on Windows
-
VoiceOver tested on macOS + iOS
-
Screen-reader-only context via
.sr-onlyclass (visually hidden, still announced)
Reading level
-
Default copy targets ~Grade 6 to ~Grade 8 reading level (per Section 508 plain-language guidance)
-
Constituent portal copy especially: Grade 6 target
-
Caseworker UI copy: technical terms are unavoidable but glossary tooltips are encouraged
Multi-language
-
Every label is a translation key (i18n already wired in
craig-common::i18n) -
String length variance accommodated (Spanish ~+20%, German ~+30%, double-byte scripts double the byte count)
-
RTL not currently required but components do not actively prevent it
-
Portal chrome includes a language switcher
What’s NOT yet at full a11y compliance
-
Focus management on modals/dialogs is partial (not a full focus trap)
-
Form-error linking via
aria-describedbyis partial (errors show as a block, not field-attached) -
Tables do not use
role="grid"patterns (currently rely on native<table>semantics) -
No documented keyboard-shortcut model (Track 5 / command palette will resolve)
Closing these gaps is engineering’s ongoing responsibility, not a future-phase concern.
Role + portal architecture
Caseworker UI roles
The caseworker UI surfaces work to six OIDC-mapped roles:
| Role | Title examples | Primary jobs |
|---|---|---|
|
System Admin, Compliance Officer |
User management, jurisdiction config, audit, federal reporting |
|
Unit Supervisor |
Worklist oversight, approvals, escalations, timeliness |
|
CPS Investigator, Foster Care Worker |
Intake triage, investigation, case management, placement |
|
Title IV-E Eligibility Specialist |
Eligibility determinations, payment authorization, rate-setting |
|
Interstate Placement Coordinator |
Cross-state placement coordination via ICPC |
|
Auditor, Researcher |
Read-only audit and research access |
Role-gating is enforced at the BFF route layer (services/craig-web/src/routes/*) and surfaced in nav via Askama conditionals (base.html lines 34-52). Role inheritance is NOT used; multi-role users (a supervisor who is also a caseworker) have both roles assigned independently and the BFF computes the union of capabilities.
Portal roles
Three public portals serve three distinct user types. Each portal has its own OIDC realm (separate Keycloak realm or client per portal):
| Portal | User |
|---|---|
Family / Constituent |
Parent or youth with an active case |
Foster Parent / Kinship Caregiver |
Caregiver currently caring for a child via DHS placement |
CWCA Provider |
Staff at contracted child welfare contributing agencies |
Separation by realm is deliberate: portal users should not see caseworker UI surfaces, and a constituent’s authentication should never grant access to other constituents' data. Cross-realm session sharing is explicitly disallowed.
See plans/portals.adoc for portal-specific scope.
Current UI surface inventory
A factual inventory of what exists today. Kept current as code changes; PRs that materially change surface counts should update this section.
craig-web (caseworker UI)
-
48 route modules at
services/craig-web/src/routes/ -
68 Askama templates at
services/craig-web/templates/ -
4 CSS files:
tokens.css,chrome.css,components.css,screens.css
| Domain | Routes | Notes |
|---|---|---|
Intake |
10 |
Worklist, referrals, investigations, reports; person-linking via HTMX (ADR-019) |
Cases |
8 |
List/create, detail/update, contacts, court-orders, household, plans, persons/{id}/chain |
Placement |
10 |
Matching, homes, placements, education, health |
Financial |
8 |
Payments, rates, claims |
Exchange |
5 |
Partners, agreements, transactions, ICPC |
Reporting |
9 |
AFCARS + NCANDS (generate/review/approve/transmit/export) |
Rules |
4 |
JDM ruleset CRUD; admin-only |
Security |
9 |
Audit, reviews, archive, changes, NIST, partners |
Explicit non-goals
To prevent future drift, these are explicit non-goals:
-
Mobile native apps. No iOS, no Android, no React Native. CRAIG’s mobile story is responsive web + (eventually) PWA wrapper.
-
SPA frameworks. No React, no Vue, no Svelte, no Angular. The server-rendered + htmx + Alpine constraint is permanent.
-
Inline styles or eval-based JS. Strict CSP is permanent.
-
Marketing-site polish. This is a government tool, not a SaaS landing page. Focus is on operational usability, not delight aesthetics.
-
Heavy print formatting. State-mandated print formats (AFCARS submissions, court reports) are handled in dedicated print stylesheets, not the main CSS.
Open architectural questions
These remain unresolved and should land as ADRs when decided:
-
Composability persistence: per-user-only, or per-user with per-role + per-jurisdiction defaults? (recommended: per-user with per-role + per-jurisdiction defaults; see §4)
-
Portal architecture: extension of
craig-webwith role-gated routes, or separatecraig-portal-*services? (TBD; affects 3 portals identically) -
Dark mode model: system-preference detection + manual override, or manual only? (recommended: both; system preference is default with manual override)
-
Saved views: separate feature from composability, or part of the same composability story? (TBD; affects roadmap sequencing)
-
Per-jurisdiction sub-branding for CWCA providers operating across multiple states: how do we visually convey "you’re acting in Georgia today, not Texas" without overloading chrome? (TBD)
References
-
UI Module Map — role + module matrix
-
Code:
services/craig-web/src/routes/(BFF routes) -
Code:
services/craig-web/templates/(Askama templates) -
Code:
services/craig-web/static/css/(tokens + chrome + components + screens CSS) -
Code:
services/craig-intake/static/(public submission UI) -
External reference:
~/code/canopy/design/canopy-web/(sibling project; design explorations applicable to CRAIG)