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

Every color in the app comes from one of these tokens. Vuetify exposes each 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})).

Brand greens

Three working greens: primary (main actions), primary-dark (hovers, headings on tinted blocks), and secondary (chrome, callouts).

primary
Primary actions, links, focus rings, step Next buttons
color="primary" · rgb(var(--v-theme-primary))
primary-dark
Hover state for primary, headings on tinted blocks
rgb(var(--v-theme-primary-dark))
secondary
Header chrome, "Why" callout, mid-emphasis surfaces
color="secondary"

Brand blues / chrome

header
Dark navy site chrome, hero strip
color="header"
header-light
Light blue accent on dark navy
color="header-light"
header-medium
Mid teal-blue, between header and header-light (was the token named `accent`)
color="header-medium"
header-dark
Duplicate of header (same #1a3a52); kept for naming symmetry, not consumed directly
color="header-dark"

Semantic

success
Success alerts, completion checkmarks
type="success" / color="success"
info
Info alerts, neutral notifications (alias of header-medium)
type="info" / color="info"
warning
Warning alerts, validation feedback
type="warning" / color="warning"
error
Error alerts, validation failures
type="error" / color="error"

Surfaces & structure

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

panel
Page background, inline panel backgrounds
background: rgb(var(--v-theme-panel))
border
Default 1–2px borders on form fields, sidebars
border: 1px solid rgb(var(--v-theme-border))
step-tint
Step header card background
color="step-tint"
sidebar-tint
Sidebar / left-column background
color="sidebar-tint" · rgb(var(--v-theme-sidebar-tint))

Neutral scale

A 5-step gray scale, ordered darkest → lightest. Folds ~13 ad-hoc grays onto roles. Consumed via CSS variable in scoped styles.

neutral-900
Near-black body text and headings on light surfaces
rgb(var(--v-theme-neutral-900))
neutral-700
Strong body text, field labels
rgb(var(--v-theme-neutral-700))
neutral-600
Secondary labels, muted body, icon tint
rgb(var(--v-theme-neutral-600))
neutral-500
Tertiary text — hints, captions, metadata
rgb(var(--v-theme-neutral-500))
neutral-300
Hairline borders, dividers, disabled text
rgb(var(--v-theme-neutral-300))

Path accent

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

path
Path-themed accents in General / Informed / Learn UI
color="path"

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.

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