Decision Tree Wizard
Stepper wizard where answers in step N change the options or the flow in step N+1. Pattern for auto-deriving complex configurations from simple user decisions.
Live Preview
- 1Fuel Energy source configuration
- 2Heat meters Check installation
- 3Recommendation System suggestion
Which fuel configuration?
Features
- Dynamically visible steps via $derived and a skipIf predicate
- Auto-derived recommendation in the review step
- Answer path determines the options available in the next step
- Back navigation always allowed, forward only once the current answer is given
- No URL-state persistence — the wizard is ephemeral
Code skeleton
DecisionTreeWizard.svelte
<script lang="ts">
import { Stepper, StepperStep, Card, RadioGroup, RadioItem, Button, Alert } from '@urbicon-ui/blocks';
let answers = $state<{ fuelType?: string; hasMeter?: string; hybridSplit?: string }>({});
let currentStep = $state(0);
// Step 3 is skipped when answer 1 != "hybrid"
const steps = $derived.by(() => {
const all = [
{ id: 'fuelType', title: 'Fuel' },
{ id: 'hasMeter', title: 'Meters' },
{ id: 'hybridSplit', title: 'Hybrid share', skipIf: () => answers.fuelType !== 'hybrid' },
{ id: 'review', title: 'Recommendation' }
];
return all.filter((s) => !s.skipIf?.());
});
// Auto-recommendation based on the answer path
const recommendation = $derived.by(() => {
if (answers.fuelType === 'single' && answers.hasMeter === 'yes') return 'HeizKV § 7';
if (answers.fuelType === 'single' && answers.hasMeter === 'no') return 'HeizKV § 9a';
if (answers.fuelType === 'hybrid' && answers.hybridSplit === 'meter') return 'HeizKV § 9 (2)';
return null;
});
const canNext = $derived.by(() => {
const id = steps[currentStep]?.id;
if (id === 'fuelType') return !!answers.fuelType;
if (id === 'hasMeter') return !!answers.hasMeter;
if (id === 'hybridSplit') return !!answers.hybridSplit;
return true;
});
</script>
<Stepper bind:activeStep={currentStep}>
{#each steps as step (step.id)}
<StepperStep label={step.title} />
{/each}
</Stepper>
<Card>
{#if steps[currentStep]?.id === 'fuelType'}
<RadioGroup bind:value={answers.fuelType} name="fuelType">
<RadioItem value="single" label="Single fuel" />
<RadioItem value="hybrid" label="Hybrid" />
</RadioGroup>
{:else if steps[currentStep]?.id === 'review'}
<Alert intent="success" title="Recommendation">{recommendation}</Alert>
{/if}
</Card>
<div class="flex justify-between">
<Button onclick={() => currentStep--} disabled={currentStep === 0}>Back</Button>
<Button onclick={() => currentStep++} disabled={!canNext}>Next</Button>
</div>Best Practices
Don't put answers in URL params
Wizard state is usually ephemeral — URL state would affect bookmarks or refreshes in unexpected ways. Only opt in with `?step=2` or similar when explicitly desired.
$derived instead of imperative skip logic
The steps array is filtered at render time via `$derived.by`. As soon as an answer changes, the next step is (de)activated automatically — no manual `goto()` or event listeners.
Auto-recommendation in the review
The last step shows not just the answers but also the derived recommendation. Optionally the user can override it — add a "Choose a different one?" toggle for that.
Back navigation always allowed
Forward may be locked while the current answer is missing. Back must never block — otherwise the user can't escape a dead end.