questionnaire

A multi-step questionnaire with single-choice, multiple-choice, freeform, and skippable questions.

PreviousNext
Question 1 of 3
What should the agent build next?

Choose a direction or describe another task.

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswer, ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

const DIRECTION_LABELS: Record<string, string> = {
  'tool-calls': 'tool call timeline',
  approvals: 'approval checkpoints',
  handoffs: 'sub-agent handoffs',
};

const UPDATE_LABELS: Record<string, string> = {
  progress: 'progress',
  decisions: 'decisions',
  risks: 'risks',
  'next-step': 'next step',
};

const TIMING_LABELS: Record<string, string> = {
  now: 'now',
  'next-cycle': 'next development cycle',
  backlog: 'backlog',
};

@Component({
  selector: 'z-demo-questionnaire-default',
  imports: [ZardQuestionnaireImports],
  template: `
    <z-questionnaire [zItems]="items" zShortcuts="letters" (zSubmit)="save($event)">
      <z-questionnaire-progress />

      <z-questionnaire-item zName="goal" zRequired>
        <z-questionnaire-title>What should the agent build next?</z-questionnaire-title>
        <z-questionnaire-description>Choose a direction or describe another task.</z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="tool-calls">
            <span class="font-medium">Tool call timeline</span>
            <z-questionnaire-choice-description>
              Show what the agent ran and what came back.
            </z-questionnaire-choice-description>
          </z-questionnaire-choice>
          <z-questionnaire-choice zValue="approvals">
            <span class="font-medium">Approval checkpoints</span>
            <z-questionnaire-choice-description>
              Ask before sensitive or destructive actions.
            </z-questionnaire-choice-description>
          </z-questionnaire-choice>
          <z-questionnaire-choice zValue="handoffs">
            <span class="font-medium">Sub-agent handoffs</span>
            <z-questionnaire-choice-description>
              Make delegated work and results easier to follow.
            </z-questionnaire-choice-description>
          </z-questionnaire-choice>
          <z-questionnaire-input placeholder="Describe another feature..." aria-label="Describe another feature" />
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="updates" zMultiple>
        <z-questionnaire-title>What should every progress update include?</z-questionnaire-title>
        <z-questionnaire-description>Select all that apply, or skip this question.</z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="progress">Progress made</z-questionnaire-choice>
          <z-questionnaire-choice zValue="decisions">Decisions taken</z-questionnaire-choice>
          <z-questionnaire-choice zValue="risks">Risks or blockers</z-questionnaire-choice>
          <z-questionnaire-choice zValue="next-step">Next step</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="start" zRequired>
        <z-questionnaire-title>When should work begin?</z-questionnaire-title>
        <z-questionnaire-description>Choose one option.</z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="now">Start now</z-questionnaire-choice>
          <z-questionnaire-choice zValue="next-cycle">Next development cycle</z-questionnaire-choice>
          <z-questionnaire-choice zValue="backlog">Add it to the backlog</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-actions>
        <button type="button" z-questionnaire-previous>Back</button>
        <button type="button" z-questionnaire-skip>Skip</button>
        <button type="button" z-questionnaire-next>Next</button>
        <button type="submit" z-questionnaire-submit>Finish</button>
      </z-questionnaire-actions>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireDefaultComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly items: ZardQuestionnaireItem[] = [
    { name: 'goal', required: true },
    { name: 'updates', multiple: true },
    { name: 'start', required: true },
  ];

  save(answers: ZardQuestionnaireAnswers): void {
    this.sonner.show('Agent plan saved', {
      description: [
        `Direction: ${this.formatDirection(answers['goal'])}`,
        `Progress signals: ${this.formatUpdates(answers['updates'])}`,
        `Timing: ${this.formatTiming(answers['start'])}`,
      ].join(' · '),
    });
  }

  private formatDirection(answer: ZardQuestionnaireAnswer | undefined): string {
    const value = this.singleValue(answer);
    return value ? (DIRECTION_LABELS[value] ?? value) : 'None';
  }

  private formatUpdates(answer: ZardQuestionnaireAnswer | undefined): string {
    const values = Array.isArray(answer) ? answer : answer ? [answer] : [];
    return values.length ? values.map(value => UPDATE_LABELS[value] ?? value).join(', ') : 'None';
  }

  private formatTiming(answer: ZardQuestionnaireAnswer | undefined): string {
    const value = this.singleValue(answer);
    return value ? (TIMING_LABELS[value] ?? value) : 'None';
  }

  private singleValue(answer: ZardQuestionnaireAnswer | undefined): string | undefined {
    return Array.isArray(answer) ? answer[0] : answer;
  }
}

Installation

Copy
npx zard-cli@latest add questionnaire

Usage

import { ZardQuestionnaireImports } from '@/shared/components/questionnaire/questionnaire.imports';
import type {
  ZardQuestionnaireAnswers,
  ZardQuestionnaireChoice,
  ZardQuestionnaireItem,
} from '@/shared/components/questionnaire/questionnaire.types';
Copy
interface QuestionnaireUsageItem extends ZardQuestionnaireItem {
  prompt: string;
  description: string;
  choices: readonly (ZardQuestionnaireChoice & { description?: string })[];
  input?: { label: string; placeholder: string };
}

readonly items: readonly QuestionnaireUsageItem[] = [
  {
    name: 'direction',
    required: true,
    prompt: 'What should we prototype next?',
    description: 'Choose a direction or write your own.',
    choices: [
      {
        value: 'delegation',
        label: 'Delegation',
        description: 'Show how work moves to a specialist.',
      },
      {
        value: 'questions',
        label: 'Question prompts',
        description: 'Show choices while the interface waits.',
      },
      { value: 'both', label: 'Both together' },
    ],
    input: { label: 'Another answer', placeholder: 'Type another answer…' },
  },
  {
    name: 'detail',
    required: false,
    prompt: 'How much detail should it include?',
    description: 'Skip this if you are not sure yet.',
    choices: [
      { value: 'focused', label: 'Focused' },
      { value: 'complete', label: 'Complete flow' },
    ],
  },
];
Copy

Define the collection once: pass it to z-questionnaire for progress, actions, and shortcuts, then project it into the parts.

<z-questionnaire [zItems]="items" (zSubmit)="handleSubmit($event)">
  <z-questionnaire-progress />

  @for (question of items; track question.name) {
    <z-questionnaire-item [zName]="question.name" [zRequired]="question.required">
      <z-questionnaire-title>{{ question.prompt }}</z-questionnaire-title>
      <z-questionnaire-description>{{ question.description }}</z-questionnaire-description>
      <z-questionnaire-choices>
        @for (choice of question.choices; track choice.value) {
          <z-questionnaire-choice [zValue]="choice.value">
            <span class="font-medium">{{ choice.label }}</span>
            @if (choice.description; as description) {
              <span data-slot="questionnaire-choice-description" class="text-muted-foreground">
                {{ description }}
              </span>
            }
          </z-questionnaire-choice>
        }

        @if (question.input; as input) {
          <z-questionnaire-input [aria-label]="input.label" [placeholder]="input.placeholder" />
        }
      </z-questionnaire-choices>
      <z-questionnaire-error />
    </z-questionnaire-item>
  }

  <z-questionnaire-actions>
    <button type="button" z-questionnaire-previous>Previous</button>
    <button type="button" z-questionnaire-skip>Skip</button>
    <button type="button" z-questionnaire-next>Next</button>
    <button type="submit" z-questionnaire-submit>Submit</button>
  </z-questionnaire-actions>
</z-questionnaire>
Copy
handleSubmit(answers: ZardQuestionnaireAnswers): void {
  // answers['direction'], answers['detail'], and answers from multiple-choice items.
}
Copy

Composition

Use the following composition to build a questionnaire:

z-questionnaire
└── z-questionnaire-progress
└── z-questionnaire-item
│   ├── z-questionnaire-title
│   ├── z-questionnaire-description
│   ├── z-questionnaire-choices
│   │   ├── z-questionnaire-choice
│   │   └── z-questionnaire-input
│   └── z-questionnaire-error
└── z-questionnaire-actions
    ├── z-questionnaire-previous
    ├── z-questionnaire-skip
    ├── z-questionnaire-next
    └── z-questionnaire-submit
Copy

Examples

multiple selection

Use zMultiple (and multiple in zItems) for an item that accepts more than one fixed answer.
What context should the agent inspect?
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

@Component({
  selector: 'z-demo-questionnaire-multiple-selection',
  imports: [ZardQuestionnaireImports],
  template: `
    <z-questionnaire [zItems]="items" zShortcuts="letters" (zSubmit)="save($event)">
      <z-questionnaire-item zName="stack" zRequired zMultiple>
        <z-questionnaire-title>What context should the agent inspect?</z-questionnaire-title>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="source">Relevant source files</z-questionnaire-choice>
          <z-questionnaire-choice zValue="tests">Existing tests</z-questionnaire-choice>
          <z-questionnaire-choice zValue="docs">Architecture documentation</z-questionnaire-choice>
          <z-questionnaire-choice zValue="history">Recent commit history</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-actions>
        <button type="submit" z-questionnaire-submit>Share context</button>
      </z-questionnaire-actions>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireMultipleSelectionComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly items: ZardQuestionnaireItem[] = [{ name: 'stack', required: true, multiple: true }];

  save(answers: ZardQuestionnaireAnswers): void {
    const context = answers['stack'];

    this.sonner.show('Context selected', {
      description: `Context: ${Array.isArray(context) ? context.join(', ') : context}`,
    });
  }
}

freeform answer

Compose z-questionnaire-input with fixed choices when the user can provide another answer.
How should the agent approach this refactor?

Choose a strategy or write a more specific instruction.

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

@Component({
  selector: 'z-demo-questionnaire-freeform-answer',
  imports: [ZardQuestionnaireImports],
  template: `
    <z-questionnaire [zItems]="items" (zSubmit)="save($event)">
      <z-questionnaire-item zName="approach" [zRequired]="true">
        <z-questionnaire-title>How should the agent approach this refactor?</z-questionnaire-title>
        <z-questionnaire-description>
          Choose a strategy or write a more specific instruction.
        </z-questionnaire-description>

        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="incremental">Make smallest safe change</z-questionnaire-choice>
          <z-questionnaire-choice zValue="module">Refactor one module at a time</z-questionnaire-choice>
          <z-questionnaire-choice zValue="rewrite">Replace the implementation completely</z-questionnaire-choice>
          <z-questionnaire-input placeholder="Describe another approach..." aria-label="Describe another approach" />
        </z-questionnaire-choices>

        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-actions>
        <button type="submit" z-questionnaire-submit>Use this approach</button>
      </z-questionnaire-actions>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoQuestionnaireFreeformAnswerComponent {
  private readonly sonner = inject(ZardSonnerService);
  readonly items: ZardQuestionnaireItem[] = [{ name: 'approach', required: true }];

  save(answers: ZardQuestionnaireAnswers): void {
    const context = answers['approach'];

    this.sonner.show('Approach selected', {
      description: `Context: ${Array.isArray(context) ? context.join(', ') : context}`,
    });
  }
}

explicit skip

Add z-questionnaire-skip when an optional item may be intentionally left unanswered.
Question 1 of 3
What kind of change is this?

Choose the category that best describes the work.

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswer, ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

const TASK_LABELS: Record<string, string> = {
  feature: 'new feature',
  bug: 'bug fix',
  refactor: 'refactor',
};

const CONSTRAINTS_LABELS: Record<string, string> = {
  'no-dependencies': 'do not add dependencies',
  'no-migrations': 'do not change the database',
  'preserve-api': 'preserve the public API',
};

const REVIEW_LABELS: Record<string, string> = {
  tests: 'run the test suite',
  diff: 'review the final diff',
  both: 'tests and diff review',
};

@Component({
  selector: 'z-demo-questionnaire-explicit-skip',
  imports: [ZardQuestionnaireImports],
  template: `
    <z-questionnaire [zItems]="items" zShortcuts="letters" (zSubmit)="save($event)">
      <z-questionnaire-progress />

      <z-questionnaire-item zName="task" zRequired>
        <z-questionnaire-title>What kind of change is this?</z-questionnaire-title>
        <z-questionnaire-description>Choose the category that best describes the work.</z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="feature">New feature</z-questionnaire-choice>
          <z-questionnaire-choice zValue="bug">Bug fix</z-questionnaire-choice>
          <z-questionnaire-choice zValue="refactor">Refactor</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="constraints">
        <z-questionnaire-title>Are there any implementation constraints?</z-questionnaire-title>
        <z-questionnaire-description>
          Answer if needed, or intentionally skip this question.
        </z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="no-dependencies">Do not add dependencies</z-questionnaire-choice>
          <z-questionnaire-choice zValue="no-migrations">Do not change the database</z-questionnaire-choice>
          <z-questionnaire-choice zValue="preserve-api">Preserve the public API</z-questionnaire-choice>
          <z-questionnaire-input
            placeholder="Describe another constraint..."
            aria-label="Another implementation constraint"
          />
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="review" zRequired>
        <z-questionnaire-title>How should the work be reviewed?</z-questionnaire-title>
        <z-questionnaire-description>
          Choose the checks the agent should complete before handoff.
        </z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="tests">Run the test suite</z-questionnaire-choice>
          <z-questionnaire-choice zValue="diff">Review the final diff</z-questionnaire-choice>
          <z-questionnaire-choice zValue="both">Tests and diff review</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-actions>
        <button type="button" z-questionnaire-previous>Back</button>
        <button type="button" z-questionnaire-skip>Skip</button>
        <button type="button" z-questionnaire-next>Next</button>
        <button type="submit" z-questionnaire-submit>Submit brief</button>
      </z-questionnaire-actions>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireExplicitSkipComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly items: ZardQuestionnaireItem[] = [
    { name: 'task', required: true },
    { name: 'constraints', required: false },
    { name: 'review', required: true },
  ];

  save(answers: ZardQuestionnaireAnswers): void {
    this.sonner.show('Agent brief submitted', {
      description: [
        `Task: ${this.formatTask(answers['task'])}`,
        `Constraints: ${this.formatConstraints(answers['constraints'])}`,
        `Review: ${this.formatReview(answers['review'])}`,
      ].join(' · '),
    });
  }

  private formatTask(answer: ZardQuestionnaireAnswer | undefined): string {
    const value = this.singleValue(answer);
    return value ? (TASK_LABELS[value] ?? value) : 'None';
  }

  private formatConstraints(answer: ZardQuestionnaireAnswer | undefined): string {
    const values = Array.isArray(answer) ? answer : answer ? [answer] : [];
    return values.length ? values.map(value => CONSTRAINTS_LABELS[value] ?? value).join(', ') : 'Skipped';
  }

  private formatReview(answer: ZardQuestionnaireAnswer | undefined): string {
    const value = this.singleValue(answer);
    return value ? (REVIEW_LABELS[value] ?? value) : 'None';
  }

  private singleValue(answer: ZardQuestionnaireAnswer | undefined): string | undefined {
    return Array.isArray(answer) ? answer[0] : answer;
  }
}

shortcuts

Assign a letter or number key to each answer with zShortcuts.
What should the agent do next?

Use the displayed shortcut or navigate with the keyboard.

import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';

import { ZardComboboxImports } from '@/shared/components/combobox';
import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type {
  ZardQuestionnaireAnswers,
  ZardQuestionnaireItem,
  ZardQuestionnaireShortcutMode,
} from '../questionnaire.types';

@Component({
  selector: 'z-demo-questionnaire-shortcuts',
  imports: [ZardQuestionnaireImports, ZardComboboxImports],
  template: `
    <div class="flex min-h-[415px] w-full flex-col justify-between">
      <z-combobox class="w-32 self-end" [searchable]="false" [(zValue)]="value">
        <z-combobox-input />
        <z-combobox-content>
          <z-combobox-list>
            <z-combobox-item zValue="letters">Letters</z-combobox-item>
            <z-combobox-item zValue="numbers">Numbers</z-combobox-item>
            <z-combobox-item zValue="none">None</z-combobox-item>
          </z-combobox-list>
        </z-combobox-content>
      </z-combobox>

      <z-questionnaire [zItems]="items" [zShortcuts]="shortcutMode()" (zSubmit)="save($event)">
        <z-questionnaire-item zName="next" zRequired>
          <z-questionnaire-title>What should the agent do next?</z-questionnaire-title>
          <z-questionnaire-description class="-mt-4 -mb-1">
            Use the displayed shortcut or navigate with the keyboard.
          </z-questionnaire-description>

          <z-questionnaire-choices>
            <z-questionnaire-choice zValue="inspect">Inspect the implementation</z-questionnaire-choice>
            <z-questionnaire-choice zValue="test">Run the relevant tests</z-questionnaire-choice>
            <z-questionnaire-choice zValue="patch">Prepare the patch</z-questionnaire-choice>
          </z-questionnaire-choices>

          <z-questionnaire-error />
        </z-questionnaire-item>

        <z-questionnaire-actions>
          <button type="submit" z-questionnaire-submit>Confirm action</button>
        </z-questionnaire-actions>
      </z-questionnaire>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireShortcutsComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly items: readonly ZardQuestionnaireItem[] = [{ name: 'next', required: true }];
  readonly value = signal<string | string[] | null>('letters');
  readonly shortcutMode = computed<ZardQuestionnaireShortcutMode>(() => {
    const value = this.value();
    return value === 'letters' || value === 'numbers' ? value : 'none';
  });

  save(answers: ZardQuestionnaireAnswers): void {
    const action = answers['next'];

    this.sonner.show('Action confirmed', {
      description: `Next: ${Array.isArray(action) ? action.join(', ') : action}`,
    });
  }
}

custom validation

Combine controlled navigation with an external schema such as Zod to return to an invalid item and present its error.
How much detail should the answer include?

Choose the response depth.

Question 1 of 2
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { z } from 'zod';

import { ZardCardImports } from '@/shared/components/card/card.imports';
import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

const questionnaireSchema = z
  .object({
    detail: z.enum(['summary', 'complete']),
    audience: z.enum(['team', 'public']),
  })
  .superRefine((answers, context) => {
    if (answers.audience === 'public' && answers.detail === 'summary') {
      context.addIssue({
        code: 'custom',
        message: 'Public answers need enough context. Choose a complete answer.',
        path: ['detail'],
      });
    }
  });

type QuestionnaireItemName = keyof z.infer<typeof questionnaireSchema>;
type QuestionnaireErrors = Partial<Record<QuestionnaireItemName, string>>;

@Component({
  selector: 'z-demo-questionnaire-custom-validation',
  imports: [ZardCardImports, ZardQuestionnaireImports],
  template: `
    <z-questionnaire class="mx-auto max-w-md" [zItems]="items" [(zActive)]="activeItem" (zSubmit)="validate($event)">
      <z-card class="w-full">
        <z-questionnaire-item zName="detail" zRequired [zInvalid]="hasError('detail')">
          <z-card-header>
            <z-questionnaire-title>How much detail should the answer include?</z-questionnaire-title>
            <z-questionnaire-description>Choose the response depth.</z-questionnaire-description>
            <z-card-action>
              <z-questionnaire-progress class="min-w-0" />
            </z-card-action>
          </z-card-header>
          <z-card-content>
            <z-questionnaire-choices>
              <z-questionnaire-choice zValue="summary" (change)="clearError('detail')">
                Concise summary
              </z-questionnaire-choice>
              <z-questionnaire-choice zValue="complete" (change)="clearError('detail')">
                Complete answer
              </z-questionnaire-choice>
            </z-questionnaire-choices>
            <z-questionnaire-error>{{ errorFor('detail') }}</z-questionnaire-error>
          </z-card-content>
        </z-questionnaire-item>

        <z-questionnaire-item zName="audience" zRequired [zInvalid]="hasError('audience')">
          <z-card-header>
            <z-questionnaire-title>Who will read the answer?</z-questionnaire-title>
            <z-questionnaire-description>Public answers require complete context.</z-questionnaire-description>
            <z-card-action>
              <z-questionnaire-progress class="min-w-0" />
            </z-card-action>
          </z-card-header>
          <z-card-content>
            <z-questionnaire-choices>
              <z-questionnaire-choice zValue="team" (change)="clearError('audience')">My team</z-questionnaire-choice>
              <z-questionnaire-choice zValue="public" (change)="clearError('audience')">
                Public audience
              </z-questionnaire-choice>
            </z-questionnaire-choices>
            <z-questionnaire-error>{{ errorFor('audience') }}</z-questionnaire-error>
          </z-card-content>
        </z-questionnaire-item>

        <z-card-footer>
          <z-questionnaire-actions>
            <button type="button" z-questionnaire-previous>Previous</button>
            <button type="button" z-questionnaire-next>Next</button>
            <button type="submit" z-questionnaire-submit>Validate answers</button>
          </z-questionnaire-actions>
        </z-card-footer>
      </z-card>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireCustomValidationComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly items: ZardQuestionnaireItem[] = [
    { name: 'detail', required: true },
    { name: 'audience', required: true },
  ];

  readonly activeItem = signal<QuestionnaireItemName>('detail');
  readonly errors = signal<QuestionnaireErrors>({});

  hasError(name: QuestionnaireItemName): boolean {
    return Boolean(this.errors()[name]);
  }

  errorFor(name: QuestionnaireItemName): string {
    return this.errors()[name] ?? 'Choose an answer to continue.';
  }

  clearError(name: QuestionnaireItemName): void {
    this.errors.update(currentErrors => {
      if (!currentErrors[name]) {
        return currentErrors;
      }

      const nextErrors = { ...currentErrors };
      delete nextErrors[name];
      return nextErrors;
    });
  }

  validate(answers: ZardQuestionnaireAnswers): void {
    const result = questionnaireSchema.safeParse(answers);

    if (result.success) {
      this.errors.set({});
      this.sonner.show('Agent response configured', {
        description: `Detail: ${result.data.detail} · Audience: ${result.data.audience}`,
      });
      return;
    }

    const nextErrors: QuestionnaireErrors = {};

    for (const issue of result.error.issues) {
      const name = issue.path[0];
      if ((name === 'detail' || name === 'audience') && !nextErrors[name]) {
        nextErrors[name] = issue.message;
      }
    }

    this.errors.set(nextErrors);

    const firstInvalidItem = result.error.issues[0]?.path[0];
    if (firstInvalidItem === 'detail' || firstInvalidItem === 'audience') {
      this.activeItem.set(firstInvalidItem);
    }
  }
}

controlled

Control the active item from host state, such as returning to an invalid step.

Current checkpoint: Change scope

Question 1 of 3
What may the agent change?

The host stores the active checkpoint while Questionnaire navigates.

import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';

import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

type QuestionnaireItemName = 'scope' | 'checks' | 'output';

const ITEM_LABELS: Record<QuestionnaireItemName, string> = {
  scope: 'Change scope',
  checks: 'Verification level',
  output: 'Output format',
};

@Component({
  selector: 'z-demo-questionnaire-controlled',
  imports: [ZardQuestionnaireImports],
  template: `
    <div class="relative mx-auto flex size-full max-w-md flex-col">
      <p class="text-muted-foreground absolute inset-e-0 top-0 text-sm" role="status">
        Current checkpoint: {{ activeItemLabel() }}
      </p>

      <z-questionnaire class="mt-auto" [zItems]="items" [(zActive)]="activeItem" (zSubmit)="saveWorkflow($event)">
        <z-questionnaire-progress />

        <z-questionnaire-item zName="scope" zRequired>
          <z-questionnaire-title>What may the agent change?</z-questionnaire-title>
          <z-questionnaire-description>
            The host stores the active checkpoint while Questionnaire navigates.
          </z-questionnaire-description>
          <z-questionnaire-choices>
            <z-questionnaire-choice zValue="component">Only the target component</z-questionnaire-choice>
            <z-questionnaire-choice zValue="tests">Component and related tests</z-questionnaire-choice>
            <z-questionnaire-choice zValue="feature">The complete feature area</z-questionnaire-choice>
          </z-questionnaire-choices>
          <z-questionnaire-error />
        </z-questionnaire-item>

        <z-questionnaire-item zName="checks" zRequired>
          <z-questionnaire-title>Which verification level should it use?</z-questionnaire-title>
          <z-questionnaire-choices>
            <z-questionnaire-choice zValue="targeted">Targeted tests</z-questionnaire-choice>
            <z-questionnaire-choice zValue="package">Package tests and typecheck</z-questionnaire-choice>
            <z-questionnaire-choice zValue="full">Full workspace verification</z-questionnaire-choice>
          </z-questionnaire-choices>
          <z-questionnaire-error />
        </z-questionnaire-item>

        <z-questionnaire-item zName="output" zRequired>
          <z-questionnaire-title>What should the agent return when finished?</z-questionnaire-title>
          <z-questionnaire-choices>
            <z-questionnaire-choice zValue="summary">Concise summary</z-questionnaire-choice>
            <z-questionnaire-choice zValue="diff">Summary with changed files</z-questionnaire-choice>
            <z-questionnaire-choice zValue="handoff">Detailed implementation handoff</z-questionnaire-choice>
          </z-questionnaire-choices>
          <z-questionnaire-error />
        </z-questionnaire-item>

        <z-questionnaire-actions>
          <button type="button" z-questionnaire-previous>Previous</button>
          <button type="button" z-questionnaire-next>Next</button>
          <button type="submit" z-questionnaire-submit>Save workflow</button>
        </z-questionnaire-actions>
      </z-questionnaire>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block h-full w-full' },
})
export class ZardDemoQuestionnaireControlledComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly items: ZardQuestionnaireItem[] = [
    { name: 'scope', required: true },
    { name: 'checks', required: true },
    { name: 'output', required: true },
  ];

  readonly activeItem = signal<QuestionnaireItemName>('scope');
  readonly activeItemLabel = computed(() => ITEM_LABELS[this.activeItem()]);

  saveWorkflow(answers: ZardQuestionnaireAnswers): void {
    this.sonner.show('Workflow saved', {
      description: `Scope: ${answers['scope']} · Verification: ${answers['checks']} · Output: ${answers['output']}`,
    });
  }
}

resume

Restore a saved active item and default answers, then reset changes back to that saved state.
Question 2 of 3
How should the migration be verified?

These checks were selected during the previous session.

import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswer, ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

@Component({
  selector: 'z-demo-questionnaire-resume',
  imports: [ZardQuestionnaireImports, ZardButtonComponent],
  template: `
    <z-questionnaire [zItems]="items" [(zActive)]="activeItem" (reset)="restoreDraft()" (zSubmit)="updateDraft($event)">
      <z-questionnaire-progress />

      <z-questionnaire-item zName="change" zRequired>
        <z-questionnaire-title>What kind of migration is this?</z-questionnaire-title>
        <z-questionnaire-description>This answer was saved during the previous session.</z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="incremental" checked>Incremental migration</z-questionnaire-choice>
          <z-questionnaire-choice zValue="cutover">Single cutover</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="verification" zMultiple zRequired>
        <z-questionnaire-title>How should the migration be verified?</z-questionnaire-title>
        <z-questionnaire-description>
          These checks were selected during the previous session.
        </z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="tests" checked>Run migration tests</z-questionnaire-choice>
          <z-questionnaire-choice zValue="typecheck" checked>Run the typecheck</z-questionnaire-choice>
          <z-questionnaire-choice zValue="manual">Perform a manual smoke test</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="notes">
        <z-questionnaire-title>Anything else the agent should remember?</z-questionnaire-title>
        <z-questionnaire-description>This note was saved with the draft.</z-questionnaire-description>
        <z-questionnaire-input aria-label="Saved migration note" value="Keep the existing public API stable." />
      </z-questionnaire-item>

      <z-questionnaire-actions class="grid-cols-[auto_minmax(0,1fr)_auto]">
        <button type="button" z-questionnaire-previous>Previous</button>
        <button type="reset" z-button zType="outline" class="justify-self-start">Reset changes</button>
        <button type="button" z-questionnaire-next>Next</button>
        <button type="submit" z-questionnaire-submit>Update draft</button>
      </z-questionnaire-actions>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireResumeComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly items: ZardQuestionnaireItem[] = [
    { name: 'change', required: true },
    { name: 'verification', multiple: true, required: true },
    { name: 'notes' },
  ];

  readonly activeItem = signal('verification');

  restoreDraft(): void {
    this.activeItem.set('verification');
    this.sonner.show('Saved answers restored');
  }

  updateDraft(answers: ZardQuestionnaireAnswers): void {
    this.sonner.show('Draft updated', {
      description: [
        `Migration: ${this.singleValue(answers['change']) ?? 'None'}`,
        `Verification: ${this.values(answers['verification']).join(', ') || 'None'}`,
        `Notes: ${this.singleValue(answers['notes']) || 'None'}`,
      ].join(' · '),
    });
  }

  private values(answer: ZardQuestionnaireAnswer | undefined): string[] {
    return Array.isArray(answer) ? answer : answer ? [answer] : [];
  }

  private singleValue(answer: ZardQuestionnaireAnswer | undefined): string | undefined {
    return this.values(answer)[0];
  }
}

conditional items

Disable items that do not apply to the user's earlier answers.
Question 1 of 2
Where should the agent run?

Cloud runs add an environment question to this flow.

import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';

import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswer, ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

type Runtime = 'local' | 'cloud';

@Component({
  selector: 'z-demo-questionnaire-conditional-items',
  imports: [ZardQuestionnaireImports],
  template: `
    <z-questionnaire [zItems]="items()" (zSubmit)="saveExecutionPlan($event)">
      <z-questionnaire-progress />

      <z-questionnaire-item zName="runtime" zRequired>
        <z-questionnaire-title>Where should the agent run?</z-questionnaire-title>
        <z-questionnaire-description>Cloud runs add an environment question to this flow.</z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="local" [checked]="runtime() === 'local'" (change)="runtime.set('local')">
            Local workspace
          </z-questionnaire-choice>
          <z-questionnaire-choice zValue="cloud" [checked]="runtime() === 'cloud'" (change)="runtime.set('cloud')">
            Cloud workspace
          </z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="environment" [zDisabled]="runtime() !== 'cloud'" zRequired>
        <z-questionnaire-title>Which cloud environment should it use?</z-questionnaire-title>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="preview">Preview</z-questionnaire-choice>
          <z-questionnaire-choice zValue="staging">Staging</z-questionnaire-choice>
          <z-questionnaire-choice zValue="isolated">Isolated sandbox</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="approval" zRequired>
        <z-questionnaire-title>When should the agent request approval?</z-questionnaire-title>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="writes">Before writing files</z-questionnaire-choice>
          <z-questionnaire-choice zValue="commands">Before running commands</z-questionnaire-choice>
          <z-questionnaire-choice zValue="sensitive">Only for sensitive actions</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-actions>
        <button type="button" z-questionnaire-previous>Previous</button>
        <button type="button" z-questionnaire-next>Next</button>
        <button type="submit" z-questionnaire-submit>Save execution plan</button>
      </z-questionnaire-actions>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireConditionalItemsComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly runtime = signal<Runtime>('local');
  readonly items = computed<readonly ZardQuestionnaireItem[]>(() => [
    { name: 'runtime', required: true },
    { name: 'environment', required: true, disabled: this.runtime() !== 'cloud' },
    { name: 'approval', required: true },
  ]);

  saveExecutionPlan(answers: ZardQuestionnaireAnswers): void {
    this.sonner.show('Execution plan saved', {
      description: [
        `Runtime: ${this.singleValue(answers['runtime']) ?? 'None'}`,
        `Environment: ${this.singleValue(answers['environment']) ?? 'Not applicable'}`,
        `Approval: ${this.singleValue(answers['approval']) ?? 'None'}`,
      ].join(' · '),
    });
  }

  private singleValue(answer: ZardQuestionnaireAnswer | undefined): string | undefined {
    return Array.isArray(answer) ? answer[0] : answer;
  }
}

navigation state

Use the active item status to disable navigation until the current question has an answer.
Question 1 of 2
What may the agent modify?

Next is intentionally disabled until an answer is selected.

import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';

import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

type QuestionnaireItemName = 'permission' | 'verification';
type QuestionnaireItemStatus = 'answered' | 'unanswered';

@Component({
  selector: 'z-demo-questionnaire-navigation-state',
  imports: [ZardQuestionnaireImports],
  template: `
    <z-questionnaire
      class="mx-auto max-w-md"
      [zItems]="items"
      [(zActive)]="activeItem"
      (zSubmit)="savePermissions($event)"
    >
      <z-questionnaire-progress />

      <z-questionnaire-item zName="permission" zRequired>
        <z-questionnaire-title>What may the agent modify?</z-questionnaire-title>
        <z-questionnaire-description>
          Next is intentionally disabled until an answer is selected.
        </z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="files" (change)="setStatus('permission', 'answered')">
            Project files
          </z-questionnaire-choice>
          <z-questionnaire-choice zValue="tests" (change)="setStatus('permission', 'answered')">
            Project files and tests
          </z-questionnaire-choice>
          <z-questionnaire-choice zValue="config" (change)="setStatus('permission', 'answered')">
            Files, tests, and configuration
          </z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="verification" zRequired>
        <z-questionnaire-title>What must pass before completion?</z-questionnaire-title>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="tests" (change)="setStatus('verification', 'answered')">
            Tests
          </z-questionnaire-choice>
          <z-questionnaire-choice zValue="types" (change)="setStatus('verification', 'answered')">
            Tests and types
          </z-questionnaire-choice>
          <z-questionnaire-choice zValue="all" (change)="setStatus('verification', 'answered')">
            Tests, types, and visual QA
          </z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-actions>
        <button type="button" z-questionnaire-previous>Previous</button>
        <button
          type="button"
          z-questionnaire-next
          zType="secondary"
          class="data-[status=unanswered]:opacity-50"
          [attr.data-status]="activeStatus()"
          [disabled]="unanswered()"
        >
          Next
        </button>
        <button type="submit" z-questionnaire-submit [disabled]="unanswered()">Save permissions</button>
      </z-questionnaire-actions>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireNavigationStateComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly items: ZardQuestionnaireItem[] = [
    { name: 'permission', required: true },
    { name: 'verification', required: true },
  ];

  readonly activeItem = signal<QuestionnaireItemName>('permission');
  readonly statuses = signal<Record<QuestionnaireItemName, QuestionnaireItemStatus>>({
    permission: 'unanswered',
    verification: 'unanswered',
  });

  readonly activeStatus = computed(() => this.statuses()[this.activeItem()]);
  readonly unanswered = computed(() => this.activeStatus() === 'unanswered');

  setStatus(name: QuestionnaireItemName, status: QuestionnaireItemStatus): void {
    this.statuses.update(current => ({ ...current, [name]: status }));
  }

  savePermissions(answers: ZardQuestionnaireAnswers): void {
    this.sonner.show('Permissions saved', {
      description: `Permission: ${answers['permission'] ?? 'None'} · Verification: ${answers['verification'] ?? 'None'}`,
    });
  }
}

custom progress

Replace the default progress label with a custom step indicator.
Checkpoint 1 of 4
How large is the change?
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

@Component({
  selector: 'z-demo-questionnaire-custom-progress',
  imports: [ZardQuestionnaireImports],
  template: `
    <z-questionnaire class="mx-auto max-w-md" [zItems]="items" zActive="scope" (zSubmit)="savePlan($event)">
      <z-questionnaire-progress #progress="zQuestionnaireProgress" class="w-full">
        <div class="mb-2 flex gap-1.5" aria-hidden="true">
          @for (item of items; track item.name; let index = $index) {
            <span
              class="h-1.5 flex-1 rounded-full"
              [class.bg-primary]="index < progress.current()"
              [class.bg-muted]="index >= progress.current()"
            ></span>
          }
        </div>
        <span>Checkpoint {{ progress.current() }} of {{ progress.total() }}</span>
      </z-questionnaire-progress>

      <z-questionnaire-item zName="scope" zRequired>
        <z-questionnaire-title>How large is the change?</z-questionnaire-title>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="small">Small patch</z-questionnaire-choice>
          <z-questionnaire-choice zValue="medium">Feature-sized change</z-questionnaire-choice>
          <z-questionnaire-choice zValue="large">Cross-package change</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="strategy" zRequired>
        <z-questionnaire-title>How should commits be organized?</z-questionnaire-title>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="single">Single commit</z-questionnaire-choice>
          <z-questionnaire-choice zValue="logical">Logical commits</z-questionnaire-choice>
          <z-questionnaire-choice zValue="squash">Squash before review</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="tests" zRequired>
        <z-questionnaire-title>Which tests should run?</z-questionnaire-title>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="targeted">Targeted tests</z-questionnaire-choice>
          <z-questionnaire-choice zValue="package">Package suite</z-questionnaire-choice>
          <z-questionnaire-choice zValue="workspace">Full workspace</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item zName="delivery" zRequired>
        <z-questionnaire-title>How should the work be delivered?</z-questionnaire-title>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="patch">Patch only</z-questionnaire-choice>
          <z-questionnaire-choice zValue="commit">Committed locally</z-questionnaire-choice>
          <z-questionnaire-choice zValue="branch">Push a review branch</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-actions>
        <button type="button" z-questionnaire-previous>Previous</button>
        <button type="button" z-questionnaire-next>Next</button>
        <button type="submit" z-questionnaire-submit>Finish plan</button>
      </z-questionnaire-actions>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireCustomProgressComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly items: ZardQuestionnaireItem[] = [
    { name: 'scope', required: true },
    { name: 'strategy', required: true },
    { name: 'tests', required: true },
    { name: 'delivery', required: true },
  ];

  savePlan(answers: ZardQuestionnaireAnswers): void {
    this.sonner.show('Pull request plan ready', {
      description: `Scope: ${answers['scope'] ?? 'None'} · Commits: ${answers['strategy'] ?? 'None'} · Tests: ${answers['tests'] ?? 'None'} · Delivery: ${answers['delivery'] ?? 'None'}`,
    });
  }
}

animated items

Animate the active item while keeping progress and navigation stationary.
Question 1 of 3
What should the agent do?

Choose the task for this run.

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

const ITEM_CLASS_NAME =
  'data-active:animate-in data-active:fade-in-0 data-active:slide-in-from-bottom-2 data-active:duration-300 motion-reduce:animate-none';

@Component({
  selector: 'z-demo-questionnaire-animated-items',
  imports: [ZardQuestionnaireImports],
  template: `
    <z-questionnaire class="mx-auto max-w-md" [zItems]="items" zActive="task" (zSubmit)="saveWorkflow($event)">
      <z-questionnaire-progress />

      <z-questionnaire-item [class]="itemClassName" zName="task" zRequired>
        <z-questionnaire-title>What should the agent do?</z-questionnaire-title>
        <z-questionnaire-description>Choose the task for this run.</z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="implement">Implement the requested change</z-questionnaire-choice>
          <z-questionnaire-choice zValue="debug">Debug the current behavior</z-questionnaire-choice>
          <z-questionnaire-choice zValue="review">Review the implementation</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item [class]="itemClassName" zName="review" zRequired>
        <z-questionnaire-title>How should the work be reviewed?</z-questionnaire-title>
        <z-questionnaire-description>Select the verification depth.</z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="targeted">Targeted checks</z-questionnaire-choice>
          <z-questionnaire-choice zValue="complete">Complete test suite</z-questionnaire-choice>
          <z-questionnaire-choice zValue="manual">Tests and manual QA</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-item [class]="itemClassName" zName="delivery" zRequired>
        <z-questionnaire-title>How should the result be delivered?</z-questionnaire-title>
        <z-questionnaire-description>Choose the final handoff format.</z-questionnaire-description>
        <z-questionnaire-choices>
          <z-questionnaire-choice zValue="summary">Concise summary</z-questionnaire-choice>
          <z-questionnaire-choice zValue="diff">Summary and changed files</z-questionnaire-choice>
          <z-questionnaire-choice zValue="handoff">Detailed review handoff</z-questionnaire-choice>
        </z-questionnaire-choices>
        <z-questionnaire-error />
      </z-questionnaire-item>

      <z-questionnaire-actions>
        <button type="button" z-questionnaire-previous>Previous</button>
        <button type="button" z-questionnaire-next>Next</button>
        <button type="submit" z-questionnaire-submit>Save workflow</button>
      </z-questionnaire-actions>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireAnimatedItemsComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly itemClassName = ITEM_CLASS_NAME;
  readonly items: ZardQuestionnaireItem[] = [
    { name: 'task', required: true },
    { name: 'review', required: true },
    { name: 'delivery', required: true },
  ];

  saveWorkflow(answers: ZardQuestionnaireAnswers): void {
    this.sonner.show('Agent workflow saved', {
      description: `Task: ${answers['task'] ?? 'None'} · Review: ${answers['review'] ?? 'None'} · Delivery: ${answers['delivery'] ?? 'None'}`,
    });
  }
}

card

Compose z-questionnaire items, progress, and navigation inside a card.
What should the agent work on?

Choose the task that should be handled next.

Question 1 of 2
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { ZardCardImports } from '@/shared/components/card/card.imports';
import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

@Component({
  selector: 'z-demo-questionnaire-card',
  imports: [ZardCardImports, ZardQuestionnaireImports],
  template: `
    <z-questionnaire class="mx-auto max-w-md" [zItems]="items" zShortcuts="numbers" (zSubmit)="createTask($event)">
      <z-card>
        <z-questionnaire-item zName="task" zRequired>
          <z-card-header>
            <z-questionnaire-title id="task-title">
              <z-card-title zTitle="What should the agent work on?" />
            </z-questionnaire-title>
            <z-questionnaire-description>
              <z-card-description zDescription="Choose the task that should be handled next." />
            </z-questionnaire-description>
            <z-card-action>
              <z-questionnaire-progress />
            </z-card-action>
          </z-card-header>
          <z-card-content>
            <z-questionnaire-choices>
              <z-questionnaire-choice zValue="fix">Fix the failing tests</z-questionnaire-choice>
              <z-questionnaire-choice zValue="refactor">Refactor the data layer</z-questionnaire-choice>
              <z-questionnaire-choice zValue="docs">Update the integration guide</z-questionnaire-choice>
            </z-questionnaire-choices>
            <z-questionnaire-error />
          </z-card-content>
        </z-questionnaire-item>

        <z-questionnaire-item zName="output" zRequired>
          <z-card-header>
            <z-questionnaire-title id="output-title">
              <z-card-title zTitle="What should the final handoff include?" />
            </z-questionnaire-title>
            <z-questionnaire-description>
              <z-card-description zDescription="Pick the level of detail needed for review." />
            </z-questionnaire-description>
            <z-card-action>
              <z-questionnaire-progress />
            </z-card-action>
          </z-card-header>
          <z-card-content>
            <z-questionnaire-choices>
              <z-questionnaire-choice zValue="summary">Summary only</z-questionnaire-choice>
              <z-questionnaire-choice zValue="files">Summary and changed files</z-questionnaire-choice>
              <z-questionnaire-choice zValue="review">Full review handoff</z-questionnaire-choice>
            </z-questionnaire-choices>
            <z-questionnaire-error />
          </z-card-content>
        </z-questionnaire-item>

        <z-card-footer>
          <z-questionnaire-actions class="w-full">
            <button type="button" z-questionnaire-previous>Previous</button>
            <button type="button" z-questionnaire-next>Next</button>
            <button type="submit" z-questionnaire-submit>Create task</button>
          </z-questionnaire-actions>
        </z-card-footer>
      </z-card>
    </z-questionnaire>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoQuestionnaireCardComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly items = [
    {
      choices: [{ value: 'fix' }, { value: 'refactor' }, { value: 'docs' }],
      name: 'task',
      required: true,
    },
    {
      choices: [{ value: 'summary' }, { value: 'files' }, { value: 'review' }],
      name: 'output',
      required: true,
    },
  ] as const satisfies readonly ZardQuestionnaireItem[];

  createTask(answers: ZardQuestionnaireAnswers): void {
    this.sonner.show('Agent task created', {
      description: `Task: ${answers['task'] ?? 'None'} · Handoff: ${answers['output'] ?? 'None'}`,
    });
  }
}

dialog

Compose z-questionnaire inside a z-dialog while keeping cancellation and dismissal host-owned.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardDialogImports } from '@/shared/components/dialog/dialog.imports';
import { ZardSonnerService } from '@/shared/components/sonner/sonner.service';

import { ZardQuestionnaireImports } from '../questionnaire.imports';
import type { ZardQuestionnaireAnswers, ZardQuestionnaireItem } from '../questionnaire.types';

@Component({
  selector: 'z-demo-questionnaire-dialog',
  imports: [ZardButtonComponent, ZardDialogImports, ZardQuestionnaireImports],
  template: `
    <button type="button" z-button zType="outline" (click)="open.set(true)">Open clarification</button>

    <z-dialog [(zVisible)]="open">
      <z-dialog-title class="sr-only">Clarification questionnaire</z-dialog-title>
      <z-dialog-description class="sr-only">
        Answer two questions about the requested scope and verification.
      </z-dialog-description>

      <z-questionnaire [zItems]="items" zActive="scope" (zSubmit)="sendClarification($event)">
        <z-questionnaire-item zName="scope" zRequired>
          <z-dialog-header>
            <z-questionnaire-progress />
            <z-questionnaire-title>Which files are in scope?</z-questionnaire-title>
            <z-questionnaire-description>
              Choose how broadly the agent can update the workspace.
            </z-questionnaire-description>
          </z-dialog-header>
          <z-questionnaire-choices>
            <z-questionnaire-choice zValue="component">Component only</z-questionnaire-choice>
            <z-questionnaire-choice zValue="feature">Complete feature directory</z-questionnaire-choice>
            <z-questionnaire-choice zValue="workspace">Any related workspace file</z-questionnaire-choice>
          </z-questionnaire-choices>
          <z-questionnaire-error />
        </z-questionnaire-item>

        <z-questionnaire-item zName="tests" zRequired>
          <z-dialog-header>
            <z-questionnaire-progress />
            <z-questionnaire-title>How much verification is needed?</z-questionnaire-title>
            <z-questionnaire-description>
              Choose the checks the agent should run before handoff.
            </z-questionnaire-description>
          </z-dialog-header>
          <z-questionnaire-choices>
            <z-questionnaire-choice zValue="targeted">Targeted tests</z-questionnaire-choice>
            <z-questionnaire-choice zValue="package">Package tests</z-questionnaire-choice>
            <z-questionnaire-choice zValue="full">Full workspace verification</z-questionnaire-choice>
          </z-questionnaire-choices>
          <z-questionnaire-error />
        </z-questionnaire-item>

        <z-dialog-footer>
          <button type="button" z-button zType="outline" z-dialog-close>Cancel</button>
          <z-questionnaire-actions>
            <button type="button" z-questionnaire-previous>Previous</button>
            <button type="button" z-questionnaire-next>Next</button>
            <button type="submit" z-questionnaire-submit>Send answer</button>
          </z-questionnaire-actions>
        </z-dialog-footer>
      </z-questionnaire>
    </z-dialog>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoQuestionnaireDialogComponent {
  private readonly sonner = inject(ZardSonnerService);

  readonly open = signal(false);
  readonly items = [
    {
      choices: [{ value: 'component' }, { value: 'feature' }, { value: 'workspace' }],
      name: 'scope',
      required: true,
    },
    {
      choices: [{ value: 'targeted' }, { value: 'package' }, { value: 'full' }],
      name: 'tests',
      required: true,
    },
  ] as const satisfies readonly ZardQuestionnaireItem[];

  sendClarification(answers: ZardQuestionnaireAnswers): void {
    this.open.set(false);
    this.sonner.show('Clarification sent', {
      description: `Scope: ${answers['scope'] ?? 'None'} · Verification: ${answers['tests'] ?? 'None'}`,
    });
  }
}

Accessibility

Each z-questionnaire-item renders a fieldset. Fixed choices use native, labelled radio or checkbox inputs, while progress exposes progressbar values and navigation uses native buttons. Inactive items and unavailable actions are hidden.

After successful navigation, focus moves to the first enabled input in the active item. Failed validation marks the item invalid, reveals its alert, and focuses the first enabled input.

Always give z-questionnaire-input an accessible name with aria-label or by wrapping it in a visible label. A placeholder is not a label. Description and visible error content are automatically associated with the item fieldset and built-in answer controls; custom projected controls can reference the generated description and error ids when needed.

API Reference

z-questionnaire

Multi-step questionnaire that works with Reactive Forms.

Prop
[zItems]

Item settings by name; projected item inputs win

Type
readonly ZardQuestionnaireItem[]
Default
[]
zActive

Two-way bindable active item name

Type
string | undefined
Default
—
zShortcuts

Keyboard shortcuts for the active choices

Type
'none' | 'letters' | 'numbers'
Default
'none'
class

Custom CSS classes

Type
ClassValue
Default
''
(zSubmit)

Emits the answers once all items validate

Type
EventEmitter<Record<string, string | string[]>>
Default
—

z-questionnaire-progress

Current step as text and a progress bar.

Prop
class

Custom CSS classes

Type
ClassValue
Default
''

z-questionnaire-item

One step, rendered as a fieldset.

Prop
zName

Unique name and answer key

Type
string
Default
required
zRequired

Requires an answer. Overrides zItems

Type
boolean
Default
false
zMultiple

Allows multiple answers. Overrides zItems

Type
boolean
Default
false
zDisabled

Disables and skips the item. Overrides zItems

Type
boolean
Default
false
zInvalid

Marks the item invalid

Type
boolean
Default
false
class

Custom CSS classes

Type
ClassValue
Default
''

z-questionnaire-title

Item prompt, rendered as the legend.

Prop
class

Custom CSS classes

Type
ClassValue
Default
''

z-questionnaire-description

Supporting text wired to the item controls.

Prop
class

Custom CSS classes

Type
ClassValue
Default
''

z-questionnaire-choices

Container for choices and the freeform input.

Prop
class

Custom CSS classes

Type
ClassValue
Default
''

z-questionnaire-choice

One fixed answer with a native radio or checkbox.

Prop
zValue

Submitted value

Type
string
Default
required
checked

Initial checked state

Type
boolean
Default
false
zDisabled

Disables this choice

Type
boolean
Default
false
class

Custom CSS classes

Type
ClassValue
Default
''

z-questionnaire-choice-description

Muted text under a choice label.

Prop
class

Custom CSS classes

Type
ClassValue
Default
''

z-questionnaire-input

Freeform answer for the active item.

Prop
type

Native input type

Type
string
Default
'text'
value

Initial value

Type
string
Default
''
placeholder

Placeholder text

Type
string
Default
''
aria-label

Accessible label

Type
string | null
Default
null
zDisabled

Disables the input

Type
boolean
Default
false
class

Custom CSS classes

Type
ClassValue
Default
''

z-questionnaire-error

Validation message for the item.

Prop
class

Custom CSS classes

Type
ClassValue
Default
''

z-questionnaire-actions

Row for the navigation buttons.

Prop
class

Custom CSS classes

Type
ClassValue
Default
''

button[z-questionnaire-previous]

Goes back; hidden when there is no previous item.

Prop
disabled

Disables the button

Type
boolean
Default
false
zType

Button visual style

Type
ZardButtonTypeVariants
Default
'outline'
zSize

Button size

Type
ZardButtonSizeVariants
Default
'default'
zShape

Button shape

Type
ZardButtonShapeVariants
Default
'default'
zLoading

Loading state; also disables the button

Type
boolean
Default
false
class

Custom CSS classes

Type
ClassValue
Default
''

button[z-questionnaire-skip]

Skips an optional item; hidden otherwise.

Prop
disabled

Disables the button

Type
boolean
Default
false
zType

Button visual style

Type
ZardButtonTypeVariants
Default
'outline'
zSize

Button size

Type
ZardButtonSizeVariants
Default
'default'
zShape

Button shape

Type
ZardButtonShapeVariants
Default
'default'
zLoading

Loading state; also disables the button

Type
boolean
Default
false
class

Custom CSS classes

Type
ClassValue
Default
''

button[z-questionnaire-next]

Validates and moves forward; hidden on the last item.

Prop
disabled

Disables the button

Type
boolean
Default
false
zType

Button visual style

Type
ZardButtonTypeVariants
Default
'default'
zSize

Button size

Type
ZardButtonSizeVariants
Default
'default'
zShape

Button shape

Type
ZardButtonShapeVariants
Default
'default'
zLoading

Loading state; also disables the button

Type
boolean
Default
false
class

Custom CSS classes

Type
ClassValue
Default
''

button[z-questionnaire-submit]

Submits; shown only on the last item.

Prop
disabled

Disables the button

Type
boolean
Default
false
zType

Button visual style

Type
ZardButtonTypeVariants
Default
'default'
zSize

Button size

Type
ZardButtonSizeVariants
Default
'default'
zShape

Button shape

Type
ZardButtonShapeVariants
Default
'default'
zLoading

Loading state; also disables the button

Type
boolean
Default
false
class

Custom CSS classes

Type
ClassValue
Default
''
github iconwhatsapp icondiscord iconX icon

Made with ❤ in Brazil. Open source and available on GitHub .