TeqBench

Show your first dialog


4 min readupdated April 2026

Dialogs are the heaviest message surface in the @teqbench family — wider than a bottom sheet, centered in the viewport, and reserved for focused interactions: long copy, multi-step input, complex confirmation flows, or arbitrary projected content. Use a dialog when a notification or banner would be stretched past its envelope and a bottom sheet feels too constrained.

This guide assumes you already followed Install & authenticate and Configure the severity theme. The dialog package peer-depends on the severity theme and picks up the same six-tier color palette.

Install the package

npm install @teqbench/tbx-mat-dialogs

Provide the dialog config at bootstrap

Dialogs need a severity-icon resolver to draw the leading tier icon. The package ships two: TbxMatDialogSeverityFontIconService (Material Symbols ligatures) and TbxMatDialogSeveritySvgIconService (inline SVGs). Wire one via the required TBX_MAT_DIALOG_PROVIDER_CONFIG injection token in app.config.ts:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideTbxMatSeverityTheme } from '@teqbench/tbx-mat-severity-theme';
import {
  TBX_MAT_DIALOG_PROVIDER_CONFIG,
  TbxMatDialogSeverityFontIconService,
} from '@teqbench/tbx-mat-dialogs';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    provideAnimationsAsync(),
    provideTbxMatSeverityTheme({ invert: false }),
    {
      provide: TBX_MAT_DIALOG_PROVIDER_CONFIG,
      useFactory: () => ({
        severityIconResolverService: new TbxMatDialogSeverityFontIconService(
          'material-symbols-rounded',
        ),
      }),
    },
  ],
});

The 'material-symbols-rounded' argument picks a fontSet. If your app already provides MAT_ICON_DEFAULT_OPTIONS with a fontSet, you can drop the constructor argument and the service picks it up from there.

Import the global dialog styles in your application's stylesheet:

@use '@teqbench/tbx-mat-dialogs/styles/tbx-mat-dialogs';

Fire your first dialog

Inject TbxMatDialogService and call a severity method. Each takes a config object with title and message, and returns a Promise<TbxMatDialogResult> you can await — no subscription management:

import { Component, inject } from '@angular/core';
import { TbxMatDialogService } from '@teqbench/tbx-mat-dialogs';

@Component({
  selector: 'app-save-button',
  template: `<button (click)="save()">Save</button>`,
})
export class SaveButtonComponent {
  private readonly dialog = inject(TbxMatDialogService);

  async save(): Promise<void> {
    await this.dialog.success({
      title: 'Saved',
      message: 'Your changes are saved.',
    });
  }
}

The other tiers work the same way:

await this.dialog.error({ title: 'Save Failed', message: 'Could not save changes.' });
await this.dialog.warning({ title: 'Caution', message: 'This may take a while.' });
await this.dialog.information({ title: 'FYI', message: 'New version available.' });
await this.dialog.help({ title: 'How it works', message: 'Tap any control for details.' });
await this.dialog.default({ title: 'Notice', message: 'Neutral surface.' });

Each tier resolves its icon, panel color, and CSS class from the severity theme you wired up. The header, body, and footer share the severity background and on-severity text color.

Confirm — Yes/No flow

confirm() layers a Yes/No interaction on top of severity. The result's result field carries the dismiss reason:

import { TbxMatDialogService, TbxMatDialogDismissReason } from '@teqbench/tbx-mat-dialogs';

async onDelete(): Promise<void> {
  const output = await this.dialog.confirm({
    title: 'Delete Project?',
    message: 'This action cannot be undone.',
  });

  if (output.result === TbxMatDialogDismissReason.Affirm) {
    await this.deleteProject();
  }
}

Reasons cover every way a dialog can leave the screen: Affirm, Deny, Cancel, Close, Backdrop, Escape, ProgrammaticDismiss. Use them to branch on whether the user confirmed, denied, or backed out.

Input — projected form content

input() projects a consumer-defined component into the body. The component implements TbxMatDialogData<T> with two signals: isValid drives whether the affirm button is enabled, and value is what the dialog returns when the user confirms:

import { Component, computed, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { TbxMatDialogData } from '@teqbench/tbx-mat-dialogs';

@Component({
  selector: 'app-rename-form',
  imports: [FormsModule, MatFormFieldModule, MatInputModule],
  template: `
    <mat-form-field>
      <input matInput cdkFocusInitial [ngModel]="name()" (ngModelChange)="name.set($event)" />
    </mat-form-field>
  `,
})
export class RenameFormComponent implements TbxMatDialogData<string> {
  readonly name = signal('');
  readonly isValid = computed(() => this.name().trim().length > 0);
  readonly value = computed(() => this.name().trim());
}

Open it from anywhere:

const output = await this.dialog.input<string>({
  title: 'Rename',
  content: RenameFormComponent,
});

if (output.result === TbxMatDialogDismissReason.Affirm) {
  console.log(output.data); // typed: string
}

The affirm button stays disabled until isValid returns true, so the user can't confirm an empty rename.

Collect footer values

Every method accepts a footer array of buttons and form controls. Form controls (checkbox, toggle, radio-group, toggle-group) don't dismiss — only buttons do. When a button dismisses, every control's current value is snapshotted into output.footerValues, keyed by key:

const output = await this.dialog.confirm<{ dontAskAgain: boolean }>({
  title: 'Enable Notifications?',
  message: 'Would you like to receive notifications for this project?',
  footer: [
    { key: 'dontAskAgain', type: 'checkbox', label: "Don't ask again", align: 'start' },
    { key: 'no', type: 'button', label: 'No', result: TbxMatDialogDismissReason.Deny, align: 'end' },
    {
      key: 'yes',
      type: 'button',
      label: 'Yes',
      result: TbxMatDialogDismissReason.Affirm,
      emphasis: 'primary',
      align: 'end',
    },
  ],
});

if (output.result === TbxMatDialogDismissReason.Affirm) {
  const dontAskAgain = output.footerValues.dontAskAgain;
}

Checkbox and toggle values are boolean. Radio-group and single-select toggle-group values are string | undefined. Multi-select toggle-group values are string[].

Full control via show()

When the opinionated methods don't fit, show() takes the full configuration:

import { TbxMatSeverityLevel } from '@teqbench/tbx-mat-severity-theme';

const output = await this.dialog.show({
  title: 'Custom Dialog',
  icon: 'build',
  type: TbxMatSeverityLevel.Warning,
  subtitle: 'Optional secondary line',
  contextBadge: 'Beta',
  message: 'Full control over every option.',
  footer: [
    { key: 'cancel', type: 'button', label: 'Cancel', result: TbxMatDialogDismissReason.Cancel, align: 'end' },
    {
      key: 'proceed',
      type: 'button',
      label: 'Proceed',
      result: TbxMatDialogDismissReason.Affirm,
      emphasis: 'primary',
      align: 'end',
    },
  ],
});

What to try next

  • Bottom sheets — for a lighter modal that anchors to the bottom edge instead of the viewport center, see Show your first bottom sheet. Same severity API and footer model; different anchor.
  • Banners — for a persistent, in-flow message that doesn't take focus, see Show your first banner.
  • Inversion — provide the severity theme with invert: true to render dialogs with the inverted palette across all tiers at once; see the end-to-end section on the severity-theme guide.