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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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

agency_name, agency_short, logo_path, primary, primary_hover, accent

Per-jurisdiction. Swapped at deploy time via the jurisdiction’s theme.toml. Each US state CRAIG deploys to gets its own brand layer.

Surface

surface, surface_raised, surface_sunken, text, text_muted, border

Consistent across jurisdictions. The product should feel like the same product regardless of state branding.

Semantic

error, success, warning, info

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:

  1. Engineering scaffolds a new theme directory at services/craig-web/static/themes/<jurisdiction>/

  2. State provides brand assets (logo, agency name, optional font face)

  3. State provides brand-layer color values (primary, primary_hover, accent)

  4. Surface + semantic + mode + type + layout layers are inherited from the shared base (no per-state customization)

  5. 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-specific agency_name (CRAIG’s is product-neutral), and comment churn. diff the 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 via build.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

  1. Brand swap — agency name, logo, primary color from the jurisdiction’s theme

  2. Terminology lookup — every label, button, tooltip, error message is a translation key resolved against the jurisdiction-mapped terminology dictionary

  3. Admin-unit rendering — "Fulton County" in Georgia, "Travis County" in Texas, "Region 4" in Hawaii

  4. Workflow-rule rendering — intake response clocks, ICPC deadlines, payment periods all come from per-jurisdiction config

  5. 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-authz enforces record-level authorization per jurisdiction

  • craig-reference holds per-jurisdiction admin-unit registries (georgia 159 + texas 254 counties seeded)

  • craig-rules per-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

  1. Dashboards consist of panels arranged on a grid

  2. Panels come from a library (worklist, my approvals, my notifications, my recent cases, my upcoming court dates, jurisdiction alerts, caseload heatmap, etc.)

  3. Users can add, remove, reorder, and resize panels

  4. Per-role default layouts (a new caseworker gets caseworker defaults; a new supervisor gets supervisor defaults)

  5. Per-user customizations persist across sessions

  6. 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:

  1. Sections (contacts, court orders, household, plans, attachments) can be hidden/shown per user preference

  2. Section order is user-rearrangeable

  3. Role-based default section ordering (a CPS investigator sees Investigation Notes first; a foster care worker sees Placements first)

  4. 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):

  1. Users save filter + sort + column-set combinations as named views

  2. Personal views are private; team views are shareable

  3. 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:

  1. Plain HTML — every interaction that can be a form submission is one

  2. HTMX — for partial-fragment swaps (search-as-you-type, person-picker autocomplete, attachment lists, paginated tables-within-pages)

  3. 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 inline style= 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.css comments 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/End per 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-only class (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-describedby is 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

admin

System Admin, Compliance Officer

User management, jurisdiction config, audit, federal reporting

supervisor

Unit Supervisor

Worklist oversight, approvals, escalations, timeliness

caseworker

CPS Investigator, Foster Care Worker

Intake triage, investigation, case management, placement

eligibility_worker

Title IV-E Eligibility Specialist

Eligibility determinations, payment authorization, rate-setting

icpc_coordinator

Interstate Placement Coordinator

Cross-state placement coordination via ICPC

readonly

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.

Public surfaces (no authentication)

craig-intake serves a small public submission surface (/report and /report/status) without authentication. CAPTCHA-gated; confirmation-code-based status lookup. No PII surfaces to unauthenticated users.

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

craig-intake (public submission UI)

  • 2 HTML pages: report.html (submission), status.html (lookup by confirmation code)

  • 3 JS files: report-form.js, status-check.js, alpine.min.js (all explicit routes; not ServeDir)

  • 1 CSS file: intake-public.css

Static assets

  • services/craig-web/static/themes/georgia-orchard/ — Georgia DHS logo + override CSS

  • Montserrat font (loaded via @font-face)

  • No icon library committed yet

  • Status badges via CSS background colors (no SVG icons)

Explicit non-goals

To prevent future drift, these are explicit non-goals:

  1. Mobile native apps. No iOS, no Android, no React Native. CRAIG’s mobile story is responsive web + (eventually) PWA wrapper.

  2. SPA frameworks. No React, no Vue, no Svelte, no Angular. The server-rendered + htmx + Alpine constraint is permanent.

  3. Inline styles or eval-based JS. Strict CSP is permanent.

  4. Marketing-site polish. This is a government tool, not a SaaS landing page. Focus is on operational usability, not delight aesthetics.

  5. 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:

  1. 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)

  2. Portal architecture: extension of craig-web with role-gated routes, or separate craig-portal-* services? (TBD; affects 3 portals identically)

  3. Dark mode model: system-preference detection + manual override, or manual only? (recommended: both; system preference is default with manual override)

  4. Saved views: separate feature from composability, or part of the same composability story? (TBD; affects roadmap sequencing)

  5. 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

Edit this page · latest