Skip to main content
Urbicon UI
Back to Recipes

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

  1. 1
    Fuel Energy source configuration
  2. 2
    Heat meters Check installation
  3. 3
    Recommendation 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.