Roadless UI — Design System

Reference documentation · Live as of 2026-04-27

Overview

This is the canonical reference for the visual language of the Roadless UI frontend. Every token, font, and component pattern described here is wired into the running app — the swatches and demos below render with the live theme.

Source of truth lives in nuxt.config.ts (theme tokens, font loader, Vuetify defaults) and components/inputs/ (the canonical input wrappers). Adding a new color, font, or input convention means updating those files first, then this page.

Color tokens

Colors are organized in two layers: a primitive palette (the raw colors that exist in the app) and semantic tokens (what components actually consume, each one pointing at a primitive). The semantic layer is the source of intent — if you're building UI, you pick a semantic token, never a primitive.

Vuetify exposes every semantic token as a named option for the color prop and as a CSS variable --v-theme-{name}, consumable in scoped styles via rgb(var(--v-theme-{name})).

1 · Primitive palette

The raw colors. Reference only — do not consume these directly in components. Use a semantic token instead. Add a new primitive here when no existing color fits the design.

Greens

Three working greens in the brand family.

green-base
theme ref: primary
Description
Mid-dark forest green. The main brand green.
green-dark
theme ref: primary-dark
Description
Darker shade of the base green for hover and tinted-block headings.
green-mid
theme ref: secondary
Description
Lighter, mid-toned green used for chrome and callouts.

Blues / navy

navy-base
theme ref: header
Description
Dark navy used for site chrome and the hero strip.
navy-mid
theme ref: header-medium
Description
Mid teal-blue sitting between navy and the light blue.
sky-light
theme ref: header-light
Description
Very light blue accent that reads on the navy chrome.

Browns

Warm-earth family. Sits alongside the forest greens and navy blues — used for warm text, decorative highlights, and warm surface tints.

brown-base
theme ref: brown-base
Description
Dark warm brown. AAA text on white; also a dark surface that hosts a light accent.
brown-dark
theme ref: brown-dark
Description
Espresso. Hover / emphasis pair to brown-base.
brown-mid
theme ref: brown-mid
Description
Saturated tan / leather. Passes AA on white (not AAA); reserved for decorative use.
cream-light
theme ref: cream-light
Description
Pale warm cream. Surface tint, and the light accent that reads on brown-base.

Status palette

Reserved hues for state communication.

status-success
theme ref: success
Description
Confirmation green.
status-warning
theme ref: warning
Description
Warning amber.
status-error
theme ref: error
Description
Error red.

Gray scale

Five-step neutral scale, darkest → lightest. Replaces ~13 ad-hoc grays previously used across the app.

gray-900
theme ref: neutral-900
Description
Near-black.
gray-700
theme ref: neutral-700
Description
Dark gray.
gray-600
theme ref: neutral-600
Description
Medium gray.
gray-500
theme ref: neutral-500
Description
Light-medium gray.
gray-300
theme ref: neutral-300
Description
Pale gray — borders, dividers.

Surface tints

The pale background tints used for inline panels and step framing.

tint-panel
theme ref: panel
Description
Page and panel background tint.
tint-step
theme ref: step-tint
Description
Pale green tint for step headers.
tint-sidebar
theme ref: sidebar-tint
Description
Sidebar / left-column tint.
tint-border
theme ref: border
Description
The hairline color used for borders and dividers.

Accents

Raw colors that serve as high-contrast partners for the brand colors. Each is documented as a semantic token below and visualized with measured WCAG ratios in the Accent — contrast partners section.

cream-warm
theme ref: primary-accent
Description
Warm cream. Light accent that reads on the brand greens (primary and secondary).
cream-cool
theme ref: primary-dark-accent
Description
Slightly cooler cream. Light accent for the darker brand green.
gold-bright
theme ref: header-accent
Description
Saturated bright gold. Light accent that reads on the navy header.
navy-deep
theme ref: header-light-accent
Description
Very deep navy. Dark accent that reads on the very-light blue.

2 · Semantic tokens

What components consume. Each token expresses an intent (primary action, body text, error state) and points at one primitive. When you build UI, pick the token whose intent matches; never reach past it to a primitive.

Actions

primary
→ primitive: green-base
Use for
Primary actions, links, focus rings, step Next buttons.
Don't use for
Decorative accents or large background fills. Use a tint or secondary instead.
Consume
color="primary" · rgb(var(--v-theme-primary))
primary-dark
→ primitive: green-dark
Use for
Hover/active state for primary; heading text inside step-tint blocks.
Don't use for
Default button color (use primary).
Consume
rgb(var(--v-theme-primary-dark))

Chrome & branding

secondary
→ primitive: green-mid
Use for
Header chrome, "Why" callouts, mid-emphasis surfaces.
Don't use for
Primary CTAs (use primary). Body text on white (low contrast).
Consume
color="secondary"
header
→ primitive: navy-base
Use for
Dark navy site chrome and hero strip backgrounds.
Don't use for
Buttons or surfaces unrelated to top-level site chrome.
Consume
color="header"
header-medium
→ primitive: navy-mid
Use for
Mid-tone band between header and header-light; also aliased by info.
Don't use for
Body text — mid-tone fails AA against most surfaces.
Consume
color="header-medium"
header-light
→ primitive: sky-light
Use for
Light blue accent on the dark navy header (text and icons).
Don't use for
Large surface fills — the very-light value washes out.
Consume
color="header-light"
header-dark
→ primitive: navy-base
Use for
Naming-symmetry alias of header; kept for completeness.
Don't use for
Direct consumption — prefer header.
Consume
color="header-dark"

Status

success
→ primitive: status-success
Use for
Success alerts, completion checkmarks, confirmation states.
Don't use for
General positive emphasis (use primary).
Consume
type="success" / color="success"
info
→ primitive: navy-mid
Use for
Info alerts, neutral notifications. Aliases header-medium.
Don't use for
Warnings or errors — info should not look urgent.
Consume
type="info" / color="info"
warning
→ primitive: status-warning
Use for
Warning alerts, validation feedback.
Don't use for
Errors (use error). Decorative amber accents.
Consume
type="warning" / color="warning"
error
→ primitive: status-error
Use for
Error alerts, validation failures, destructive-action emphasis.
Don't use for
General emphasis or "limited-time" badges — saving error for actual errors keeps it loud.
Consume
type="error" / color="error"

Surfaces & structure

Consumed via the color prop on v-card or via CSS variables in scoped styles.

panel
→ primitive: tint-panel
Use for
Page background and inline panel backgrounds.
Don't use for
Card backgrounds where contrast against the page is needed (use white).
Consume
background: rgb(var(--v-theme-panel))
border
→ primitive: tint-border
Use for
Default 1–2px borders on form fields, cards, sidebars, dividers.
Don't use for
Decorative strokes that should stand out (use primary or secondary).
Consume
border: 1px solid rgb(var(--v-theme-border))
step-tint
→ primitive: tint-step
Use for
Step header card background.
Don't use for
Non-step contexts — the pale green is a step affordance.
Consume
color="step-tint"
sidebar-tint
→ primitive: tint-sidebar
Use for
Sidebar / left-column background.
Don't use for
Main content surfaces.
Consume
color="sidebar-tint" · rgb(var(--v-theme-sidebar-tint))

Text

Neutral text roles. Pick by emphasis level, not by color.

neutral-900
→ primitive: gray-900
Use for
Near-black body text and headings on light surfaces.
Don't use for
Secondary or muted text (use neutral-600).
Consume
rgb(var(--v-theme-neutral-900))
neutral-700
→ primitive: gray-700
Use for
Strong body text and field labels.
Consume
rgb(var(--v-theme-neutral-700))
neutral-600
→ primitive: gray-600
Use for
Secondary labels, muted body copy, icon tint.
Consume
rgb(var(--v-theme-neutral-600))
neutral-500
→ primitive: gray-500
Use for
Tertiary text — hints, captions, metadata.
Don't use for
Primary reading copy (contrast too low).
Consume
rgb(var(--v-theme-neutral-500))
neutral-300
→ primitive: gray-300
Use for
Hairline borders, dividers, disabled text.
Don't use for
Active text content.
Consume
rgb(var(--v-theme-neutral-300))

Warm text & highlight

Brown-family roles for warm text, decorative highlights, and warm surface backgrounds.

text-warm
→ primitive: brown-base
Use for
Warm body or heading text on light surfaces, where pure neutral gray feels too cold.
Don't use for
Text on dark surfaces (use cream-light or white).
Consume
rgb(var(--v-theme-text-warm))
text-warm-strong
→ primitive: brown-dark
Use for
Emphasis pair to text-warm; hover state for brown links.
Don't use for
Default body text (use text-warm).
Consume
rgb(var(--v-theme-text-warm-strong))
highlight
→ primitive: brown-mid
Use for
Decorative accents — pull-quote bars, icon tints, callout flourishes.
Don't use for
Status communication (use warning / error).
Consume
color="highlight" · rgb(var(--v-theme-highlight))
tint-warm
→ primitive: cream-light
Use for
Warm surface background — paper / parchment feel for callout blocks.
Don't use for
High-traffic primary surfaces.
Consume
color="tint-warm" · rgb(var(--v-theme-tint-warm))

Path accent

A single token shared by all three paths. Splits later if per-path color differentiation is wanted.

path
→ primitive: green-mid
Use for
Path-themed accents across General / Informed / Learn UI.
Don't use for
Non-path contexts.
Consume
color="path"

Contrast accents

High-contrast partners locked to a specific base color — each accent's ratio is only meaningful when paired with its base, so use it only in that pairing. Measured ratios shown in the Accent — contrast partners section below.

primary-accent
→ primitive: cream-warm
Use for
Light text and icons on primary surfaces; surface fill when paired with primary text/icons on top (e.g. HeaderComponent banner).
Don't use for
Surfaces other than primary-paired contexts — the contrast ratio is only meaningful in that pairing.
Consume
color="primary-accent" · rgb(var(--v-theme-primary-accent))
primary-dark-accent
→ primitive: cream-cool
Use for
Light text and icons on primary-dark surfaces.
Don't use for
Surfaces other than primary-dark.
Consume
rgb(var(--v-theme-primary-dark-accent))
secondary-accent
→ primitive: cream-warm
Use for
Bold or large text on secondary surfaces (clears 3:1 for bold/large only — not AA for body).
Don't use for
Body text on secondary (fails AA). Any non-secondary surface.
Consume
rgb(var(--v-theme-secondary-accent))
header-accent
→ primitive: gold-bright
Use for
Gold text and icons on the navy header surface.
Don't use for
Surfaces other than header.
Consume
rgb(var(--v-theme-header-accent))
header-light-accent
→ primitive: navy-deep
Use for
Dark text and icons on the header-light very-light blue.
Don't use for
Surfaces other than header-light.
Consume
rgb(var(--v-theme-header-light-accent))
Adding a new color
  1. Add the raw color to nuxt.config.ts as a new theme entry with a descriptive (not role-based) name — e.g. amber-base, not highlight.
  2. Document it under Primitive palette above (which group it belongs to, what it looks like).
  3. Add a semantic token in nuxt.config.ts with a role-based name that points at the primitive — e.g. highlight: amber-base.
  4. Document the semantic token under Semantic tokens with its "Use for" and "Don't use for" guidance.
  5. Consume the semantic token in components — never the primitive.

Accent — contrast partners

A high-contrast partner for each brand color that can host one. Light accents pair with the dark bases; the one dark accent reads only on the very-light header-light. secondary is mid-tone, so its accent clears 3:1 for bold/large text only (not AA body); header-medium gets none — no accent reads on it. Each block renders the accent on its base with the measured WCAG ratio; colors are read live from the theme.

Defend the RuleAa
primary-accent
… on primary · 5.57:1 · AA
Defend the RuleAa
primary-dark-accent
… on primary-dark · 6.39:1 · AA
Defend the RuleAa
secondary-accent
… on secondary · 3.22:1 · bold/large only
secondary is mid-tone — cream clears 3:1 for bold/large text (e.g. nav links), not AA for body text.
Defend the RuleAa
header-accent
… on header · 7.39:1 · AAA
Defend the RuleAa
header-light-accent
… on header-light · 9.26:1 · AAA
A dark accent reads here only because header-light is a very light blue.
Defend the RuleAa
brown-accent
… on brown-base · 9.94:1 · AAA
Cream accent on dark brown.
Defend the RuleAa
cream-accent
… on cream-light · 13.70:1 · AAA
Espresso accent on cream.

Typography

Inter is the default for all body and UI text. Merriweather Sans is opt-in via the .display-heading utility for hero / page-title use. Both are loaded by @nuxt/fonts (Bunny provider).

Roles

RoleFamilyApplies via
Body, UI, all Vuetify text-* utilitiesInterSet on body; Vuetify's $body-font-family + $heading-font-family
Display / hero / page titlesMerriweather SansAdd class="display-heading" to the element
Code, hex valuesMonospace stack<code> tag

Live samples

Defend the Roadless Rule

.display-heading · Merriweather Sans 700 · 2.25rem

Heading 4 — section titles
Heading 5 — most-used heading
Heading 6 — subsection / card titles
Subtitle 1
Body 1 — default reading size, Inter 400 / 1rem.
Body 2 — secondary content, often paired with text-medium-emphasis.
Caption — hints, metadata.

Common emphasis classes

  • font-weight-bold — strong emphasis
  • font-weight-medium — semi-emphasis
  • text-medium-emphasis — secondary / muted body text
  • text-primary — primary-color text (links, headings on light surfaces)
  • text-green-darken-3 — used in step header copy on the green tint

Spacing

Vuetify's 4px-base scale (m{a,t,r,b,l,x,y}-{0..16}, pa-*) is the standard. Use it.

Container max-widths used

WidthUsed for
max-width="600"Single-column form steps (contact, statement)
max-width="700"Larger form steps (area picker, identity)
max-width="800"Standard modals (Help, Submission Guide, Path Selection)
max-width="900"Large dialog modals

Iconography

Material Design Icons (MDI) via @mdi/font and @nuxt/icon. Vuetify default set is mdi. Use <v-icon> for inline icons; sizes small / default / large / 32 / 40 / 48 are all in use.

mdi-arrow-leftBack
mdi-arrow-rightNext
mdi-checkSelected / done
mdi-closeDismiss
mdi-pin-outlineSave to briefcase
mdi-magnifySearch
mdi-map-marker-outlineArea / location
mdi-waterClean water topic
mdi-treeForest topic
mdi-fireWildfire topic
mdi-pawWildlife topic
mdi-currency-usdEconomy topic
mdi-account-groupPublic support
mdi-phoneContact reps
mdi-share-variantShare
mdi-help-circle-outlineHelp
mdi-updateSite update banner

Buttons

Global default VBtn: { rounded: 'xl' } gives every button extra-large pill corners.

Variants

Semantic colors

Sizes

Canonical step navigation

Outlined Back, spacer, flat large Next with right arrow. Used in every step in the comment flow.

Buttons vs cards for choices

Single-line actions get a v-btn. Multi-line selections (long descriptions, rich content) get a clickable v-card — see Cards § selectable.

Cards & surfaces

Variants

Default
Elevated by default (Vuetify auto shadow).
Outlined
Most common choice across the app.
Flat
No shadow, no border. Used for selected state.
Tonal · success
Used for the home-page "Why" callout block.
Tonal · info
Light tinted block for informational sections.
step-tint
The canonical step header background.

Selectable card

Used in path selection, persona picker, concern picker, argument picker. Selected state flips to color="primary" + variant="flat".

Form input wrappers

Five wrappers in components/inputs/ codify field semantics — type, autocomplete, inputmode, format-validation rules — so browsers and phone keyboards get the right autofill and layout. The first four wrap <v-text-field>; RoadlessTextInput wraps <v-textarea> and adds automatic content sanitization.

What each wrapper locks

WrappertypeautocompleteinputmodeOtherBuilt-in rule
RoadlessEmailFieldemailemailemailRFC-loose x@y.z
RoadlessNameField which="given"textgiven-nameRequired-only
RoadlessNameField which="family"textfamily-nameRequired-only
RoadlessNameField which="full"textnameRequired-only
RoadlessZipFieldtextpostal-codenumericmaxlength=5, pattern=[0-9]{5}, digits-only sanitize-on-input 5 digits
RoadlessPhoneFieldtelteltel≥ 10 digits anywhere
RoadlessTextInputauto-grow, rows=5, sanitize-on-emit, trim-on-blur Required-only

API conventions

  • v-modelmodelValue: string; always returns string (never undefined)
  • variant="outlined" is locked — no escape hatch
  • label / placeholder default per type but are overridable
  • :rules composes additively after the built-in required + format rules
  • required / disabled / error pass through
  • Anything else passes through to the inner Vuetify component via v-bind="$attrs"

RoadlessTextInput sanitization

Every value emitted by RoadlessTextInput runs through this normalization before reaching the model:

CleanupWhat & why
Control charactersStrips \x00–\x08, \x0B, \x0C, \x0E–\x1F, \x7F (preserves \t and \n). Pasted from Word/PDF/Outlook; breaks downstream submission encoding.
Zero-width & bidi controlsStrips U+200B–U+200D, U+2060, U+FEFF, U+202A–U+202E. Invisible chars from rich-text paste.
Line endings\r\n / \r normalized to \n.
Excess blank lines3+ consecutive newlines collapse to 2 (one blank line). Preserves paragraph breaks.
Leading / trailing whitespaceTrimmed on blur only.
Smart quotes & em-dashesOff by default. Pass normalize-quotes to convert to ASCII equivalents.

Live demo

Other Vuetify inputs (no wrapper)

Use v-checkbox, v-select, etc. directly with color="primary" and outlined variants where applicable.

General

Alerts

variant="tonal" dominates; border="start" is used on prominent banners.

Prominent banner

Compact validation alert

Modals

Six dialogs in components/modals/ share this skeleton: primary-tinted header bar, top-right close, scrollable body, action footer with primary button right-aligned.

<v-dialog v-model="open" max-width="800" scrollable>
  <v-card>
    <v-card-title class="bg-primary text-white">
      <v-icon class="mr-2">mdi-info-outline</v-icon>
      Modal title
      <v-spacer />
      <v-btn icon="mdi-close" variant="text" color="white" @click="close" />
    </v-card-title>
    <v-card-text class="pa-6"> ... </v-card-text>
    <v-card-actions>
      <v-spacer />
      <v-btn color="primary" variant="elevated" @click="close">Done</v-btn>
    </v-card-actions>
  </v-card>
</v-dialog>

Step pattern

Every step in components/steps/*Step.vue shares this three-piece structure: green-tinted header (color="step-tint") with icon + title, outlined content card with the form panel, and a card-actions row with back / next buttons. Centered with ma-auto at max-width="600" or "700".

Step 1: Contact Information

We'll start by collecting your contact information.

Layout shell

Page composition

  1. HeaderComponent — full-bleed banner, rotating images, 200/150/100 px responsive heights, background secondary.
  2. v-container fluid — 3-column row: news sidebar (lg=3), main content (lg=6), optional resources sidebar.
  3. FooterComponentLight or FooterComponentDark.

Mobile / desktop branching

Single route per page. Each pages/*.vue imports both components/desktop/<Page>View.vue and components/mobile/<Page>View.vue and switches on useDevice().isDesktop.

<template>
  <DesktopView v-if="isDesktop" ... />
  <MobileView v-else ... />
</template>

<script setup>
const { isDesktop } = useDevice()
</script>

Path naming

The comment flow has three paths. They currently share the single path color token (aliased to secondary); per-path color differentiation can be introduced later by splitting the token.

PathStatusNote
GeneralLiveDefault fastest path.
InformedLiveAdds a learning step before content selection.
LearnNot yet developedEducation-first path (planned).

File references

  • nuxt.config.ts — theme tokens, font loader, Vuetify defaults (incl. VBtn rounded='xl')
  • assets/styles/global.css — body chrome, .display-heading, footer, briefcase indicator, accessibility media queries
  • assets/styles/mobile.css — mobile typography utilities
  • assets/styles/vuetify.scss — Vuetify body/heading family override (Inter)
  • components/inputs/Roadless{Email,Name,Zip,Phone}Field.vue — text-input wrappers
  • components/inputs/RoadlessTextInput.vue — textarea wrapper with sanitization
  • components/layout/HeaderComponent.vue, FooterComponent{Light,Dark}.vue — page chrome
  • components/steps/ContactInfoStep.vue — canonical step example
  • components/modals/PathSelectionModal.vue — selectable-card pattern