Goal Builder — Onboarding Phase B
Overview
The Goal Builder is a 4-slide sequence (Life Area → Goal Name → Goal Shape → Goal Why) introduced in Phase B of the Onboarding Overhaul. It replaces the legacy GoalTypeSlide with a guided, context-rich construction sequence that produces a fully-formed first goal before the user ever reaches the goal creation form.
There is no experiment gate — these slides run for every user as slides 2–5 of the single 13-slide onboarding registry (see Onboarding Flow (technical)). The variant_c concept referenced in earlier drafts of this doc no longer exists; it was retired once the guided goal flow reached 100% rollout, and variantId / getVariantId were removed from useOnboardingFlow and every callsite. All slides sit inside the existing SlideWizardLayout chrome — no new navigation shell is required.
Related docs:
- Onboarding Timeline — where the goal builder fits in the Day 1 arc
- Goals — how the builder's answers pre-fill
GoalCreate.vue - Meet Coach — the parallel configuration experience for AI Coach
- AI Coaching Expansion — the goal-discovery quiz branch off Step 2 (
GoalNameSlide.vue)
The 4-Step Flow
Step 1 — Life Area (LifeAreaSlide.vue)
The user picks the life area their first goal belongs to. Eight color-coded cards in a 2×4 grid (mobile: 2×4 scroll):
| ID | Label | Badge color | Icon |
|---|---|---|---|
health | Health | Emerald | HeartPulse |
wealth | Wealth | Amber | Coins |
career | Career | Sky | Briefcase |
personal-growth | Personal growth | Violet | Sprout |
relationships | Relationships | Rose | Heart |
family | Family | Orange | Users |
spirituality | Spirituality | Purple | Plus |
life-vision | Life vision | Indigo | Eye |
A ninth "Something else" dashed-border card reveals a text input (aria-label="Name your area") for users who don't fit the preset areas. The custom label is passed forward alongside the id: 'custom' sentinel.
Accessibility: role="radiogroup" wrapping the grid; each card has role="radio" + aria-checked. Custom input uses aria-required="false".
Answer stored: flow.answers.lifeArea = { id, label }.
Step 2 — Goal Name (GoalNameSlide.vue)
Free-text entry for the goal's name. Shows the selected life area label as a contextual label above the input and offers 3 area-specific suggestion chips below.
Suggestion chips by area:
| Area | Suggestions |
|---|---|
| health | Run my first 5K · Sleep 8 hours consistently · Cut out sugar for 90 days |
| wealth | Save three months of expenses · Pay off my credit card · Start investing monthly |
| career | Earn a promotion this year · Learn one new skill a quarter · Ship a side project |
| personal-growth | Read one book a month · Build a daily meditation habit · Journal three times a week |
| relationships | Schedule a weekly date night · Call a friend every week · Be more present at dinner |
| family | Establish a family dinner routine · Plan one trip together · Read bedtime stories nightly |
| spirituality | Build a daily meditation practice · Spend 10 minutes in reflection each morning · Read a chapter of scripture daily |
| life-vision | Define my 5-year vision · Write my personal mission statement · Identify what matters most |
Chips are rounded-full pills labeled "OR TRY —". Clicking a chip sets the textarea value but leaves the cursor in the textarea so the user can edit.
Accessibility: Heading gets tabindex="-1" focus on mount to announce slide to screen readers.
Answer stored: flow.answers.goalName = string.
Not sure what to set? (OBJ-1510): below the suggestion chips, a "Not sure what to set? Let Coach ask a few questions" branch button (behind goal_quiz_enabled) toggles this slide into an in-place quiz mode — the goal-discovery quiz renders inside the slide body instead of navigating away, so the wizard chrome and progress bar never change. Picking a suggestion prefills the textarea (same update:value path as a manual edit) and advances to Step 3; the quiz never creates a goal itself. Full flow/state reference: AI Coaching Expansion § Goal-discovery quiz.
Step 3 — Goal Shape & Target Date (GoalShapeSlide.vue)
The user picks from 4 goal shapes in a 2×2 grid:
| ID | Label | Description | Target date? | Default days |
|---|---|---|---|---|
sprint | Sprint | 30 days, one push. | Yes | 30 |
arc | Arc | A season or a year. | Yes | 90 |
habit | Habit | Ongoing. Build a streak. | No | — |
life-list | Life-list | No deadline. Someday. | No | — |
Selecting Sprint or Arc reveals a collapsible target date panel (v-show, aria-live="polite") with three preset chips ("30 days", "90 days", "1 year") and a native date input for custom dates. Selecting Habit or Life-list hides the panel and clears any stored targetDate.
The default date for Sprint is today + 30 days; for Arc, today + 90 days. Both can be overridden with the custom picker.
Answers stored: flow.answers.goalShape = ShapeId, flow.answers.targetDate = ISO date string | null.
Step 4 — Goal Why (GoalWhySlide.vue)
An optional textarea asking "Why this one?" The placeholder is Because… (italic). Users can skip this step — the CTA is enabled even with an empty textarea.
Heading: "Why this one?" with the emphasis word styled text-primary italic.
Accessibility: Textarea has aria-label="Your motivation" + aria-required="false".
Answer stored: flow.answers.goalWhy = string | null.
Registry Wiring (placeholderSlides.ts)
The four slides are registered unconditionally, in order, alongside every other onboarding slide — there is no variantId concept or per-slide skip flag gating them:
export const placeholderSlides: readonly OnboardingSlide[] = [
{ id: 'start', component: WelcomeIntro, /* ... */ },
{ id: 'life-area', component: LifeAreaSlide, nextLabel: 'Continue', hideWrapper: true },
{ id: 'goal-name', component: GoalNameSlide, nextLabel: 'Continue', hideWrapper: true },
{ id: 'goal-shape', component: GoalShapeSlide, nextLabel: 'Continue', hideWrapper: true },
{ id: 'goal-why', component: GoalWhySlide, nextLabel: 'Continue', hideWrapper: true },
// ...coach-builder, goal-motivation-snapshot, source, review, done
]See Onboarding Flow (technical) — Slide Registry for the full, current registry listing.
Handoff to Goal Creation
There is no query-param redirect keyed on a variant_c flag. DoneSlide.vue submits directly via a mutation, and the path depends only on whether the user named a goal:
goalNamepresent — calls the atomicCOMPLETE_ONBOARDING_AND_CREATE_GOAL_MUTATION(goalName,goalCategoryId,onboardingAnswers), which both stores onboarding state and creates the goal server-side in one transaction (Onboarding::CompleteOnboardingAndCreateGoal). Routes to/dashboard?source=wizardon success.goalNameabsent — callsSTORE_ONBOARDING_STATE_MUTATIONonly (no goal created), then routes to/goals/create?source=wizard&coachPersona=...&category=...so the user finishes goal creation on that page.
goalCategoryId for the mutation resolves from flow.answers.lifeArea.id (set by LifeAreaSlide). See Onboarding Flow (technical) — Backend: CompleteOnboardingAndCreateGoal Mutation for the full GraphQL signature and interaction details.
OnboardingAnswers Interface
Five new optional fields were added (ionic_frontend/src/composables/useOnboardingFlow.ts):
interface OnboardingAnswers {
// ... existing fields
lifeArea?: { id: string; label: string } | null
goalName?: string | null
goalShape?: 'sprint' | 'arc' | 'habit' | 'life-list' | null
targetDate?: string | null // ISO 8601 date string
goalWhy?: string | null
}Accessibility Summary
| Feature | Implementation |
|---|---|
| Card grids | role="radiogroup" → role="radio" + aria-checked |
| Target date panel | aria-live="polite" — announces when it appears |
| Custom area input | aria-label="Name your area" + aria-required="false" |
| Goal why textarea | aria-label="Your motivation" + aria-required="false" |
| Reduced motion | All transitions wrapped in motion-reduce:transition-none motion-reduce:transform-none |
| Tap targets | min-h-[80px] (life area, 2-col), min-h-[44px] (shape), chips use py-2.5 — all ≥44px |
| Focus on mount | Each slide tabindex="-1"-focuses its heading so the screen reader announces the new step |
Key Files
| File | Purpose |
|---|---|
ionic_frontend/src/views/welcome/slides/LifeAreaSlide.vue | Step 1 — life area picker |
ionic_frontend/src/views/welcome/slides/GoalNameSlide.vue | Step 2 — free-text goal name + suggestion chips |
ionic_frontend/src/views/welcome/slides/GoalShapeSlide.vue | Step 3 — shape selector + conditional target date panel |
ionic_frontend/src/views/welcome/slides/GoalWhySlide.vue | Step 4 — optional motivation textarea |
ionic_frontend/src/views/welcome/slides/DoneSlide.vue | Submits via CompleteOnboardingAndCreateGoal or StoreOnboardingState + redirects to /goals/create |
ionic_frontend/src/views/GoalCreate.vue | Manual goal-creation form (reached when no goal name was given in the wizard) |
ionic_frontend/src/views/welcome/placeholderSlides.ts | Full 13-slide registry — no variant gating |
ionic_frontend/src/composables/useOnboardingFlow.ts | OnboardingAnswers interface + shared flow state |
ionic_frontend/tests/unit/views/welcome/slides/ | Unit tests for all 4 slides + DoneSlide handoff |
.planning/phases/goal-builder/UI-SPEC.md | Desi's design contract for this phase |
Last updated: 2026-07-19