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-questionnaireComponent

A multi-step questionnaire with single-choice, multiple-choice, freeform, and skippable questions. Implements ControlValueAccessor for Reactive Forms.

PropertyDescriptionTypeDefault
[zItems] Optional item settings by name (required, multiple, disabled). They apply only where the matching z-questionnaire-item does not set the same input; the projected items define the order. Without projected items, this list defines the flow readonly ZardQuestionnaireItem[] []
zActive Active item name; supports two-way binding with [(zActive)] string | undefined undefined
zShortcuts Assigns answer shortcuts to the enabled choices of the active item. Only the focused questionnaire reacts; with nothing focused, the last one interacted with does 'none' | 'letters' | 'numbers' 'none'
class Custom CSS classes ClassValue ''
(zSubmit) Emits the answers once every enabled item validates EventEmitter<Record<string, string | string[]>> -

z-questionnaire-progressComponent

Displays the current question and total as text and an accessible progress bar.

PropertyDescriptionTypeDefault
class Custom CSS classes ClassValue ''

z-questionnaire-itemComponent

One questionnaire step rendered as a fieldset.

PropertyDescriptionTypeDefault
zName Unique item name used for navigation and the submitted answer key string required
zRequired Requires an answer before navigation or submission can continue. Wins over the item in [zItems]; when unset, [zItems] decides boolean false
zMultiple Allows multiple fixed answers and submits a string array. Wins over the item in [zItems]; when unset, [zItems] decides boolean false
zDisabled Disables the item and excludes it from progress, navigation, validation, and submission. Wins over the item in [zItems]; when unset, [zItems] decides boolean false
zInvalid Marks the item invalid from consumer-owned validation boolean false
class Custom CSS classes ClassValue ''

z-questionnaire-titleComponent

Item prompt rendered as the fieldset legend.

PropertyDescriptionTypeDefault
class Custom CSS classes ClassValue ''

z-questionnaire-descriptionComponent

Supporting text automatically wired to the item fieldset and built-in answer controls.

PropertyDescriptionTypeDefault
class Custom CSS classes ClassValue ''

z-questionnaire-choicesComponent

Layout container for fixed choices and the freeform input.

PropertyDescriptionTypeDefault
class Custom CSS classes ClassValue ''

z-questionnaire-choiceComponent

One fixed answer rendered as a label with a native radio or checkbox and an optional shortcut.

PropertyDescriptionTypeDefault
zValue Value submitted when the choice is selected string required
checked Initial checked state, including the state restored by a native form reset boolean false
zDisabled Disables this choice boolean false
class Custom CSS classes ClassValue ''

z-questionnaire-choice-descriptionComponent

Secondary text under a choice label, rendered in muted color.

PropertyDescriptionTypeDefault
class Custom CSS classes ClassValue ''

z-questionnaire-inputComponent

Freeform answer for the active item.

PropertyDescriptionTypeDefault
type Native input type string 'text'
value Initial value, including the value restored by a native form reset string ''
placeholder Placeholder text string ''
aria-label Accessible label for the freeform input string | null null
zDisabled Disables the freeform input boolean false
class Custom CSS classes ClassValue ''

z-questionnaire-errorComponent

Validation message for the item.

PropertyDescriptionTypeDefault
class Custom CSS classes ClassValue ''

z-questionnaire-actionsComponent

Layout row for questionnaire navigation buttons.

PropertyDescriptionTypeDefault
class Custom CSS classes ClassValue ''

button[z-questionnaire-previous]Component

Moves to the previous item and hides itself when no previous item is available.

PropertyDescriptionTypeDefault
disabled Disables the button in addition to its questionnaire-controlled disabled state boolean false
zType Button visual style ZardButtonTypeVariants 'outline'
zSize Button size ZardButtonSizeVariants 'default'
zShape Button shape ZardButtonShapeVariants 'default'
zLoading Shows the loading state and disables the button boolean false
class Custom CSS classes ClassValue ''

button[z-questionnaire-skip]Component

Skips the active optional item and hides itself when the item cannot be skipped.

PropertyDescriptionTypeDefault
disabled Disables the button in addition to its questionnaire-controlled disabled state boolean false
zType Button visual style ZardButtonTypeVariants 'outline'
zSize Button size ZardButtonSizeVariants 'default'
zShape Button shape ZardButtonShapeVariants 'default'
zLoading Shows the loading state and disables the button boolean false
class Custom CSS classes ClassValue ''

button[z-questionnaire-next]Component

Validates the active item, moves forward, and hides itself on the final item.

PropertyDescriptionTypeDefault
disabled Disables the button in addition to its questionnaire-controlled disabled state boolean false
zType Button visual style ZardButtonTypeVariants 'default'
zSize Button size ZardButtonSizeVariants 'default'
zShape Button shape ZardButtonShapeVariants 'default'
zLoading Shows the loading state and disables the button boolean false
class Custom CSS classes ClassValue ''

button[z-questionnaire-submit]Component

Submits the questionnaire and hides itself until the final item is active.

PropertyDescriptionTypeDefault
disabled Disables the button in addition to its questionnaire-controlled disabled state boolean false
zType Button visual style ZardButtonTypeVariants 'default'
zSize Button size ZardButtonSizeVariants 'default'
zShape Button shape ZardButtonShapeVariants 'default'
zLoading Shows the loading state and disables the button boolean false
class Custom CSS classes ClassValue ''
github iconwhatsapp icondiscord iconX icon

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