TeqBench

Show your first bottom sheet


4 min readupdated April 2026

Bottom sheets are the third message surface in the @teqbench family — heavier than a notification, more focused than a banner. They slide up from the bottom edge of the viewport, take focus, and stay until the user picks an action. Familiar on mobile, unobtrusive on desktop. Use one for confirmations, short input prompts, and small projected forms that need a modal frame around them.

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

Install the package

npm install @teqbench/tbx-mat-bottom-sheets

Provide the bottom-sheet config at bootstrap

Bottom sheets need a severity-icon resolver to draw the leading tier icon. The package ships two: TbxMatBottomSheetSeverityFontIconService (Material Symbols ligatures) and TbxMatBottomSheetSeveritySvgIconService (inline SVGs). Wire one via the required TBX_MAT_BOTTOM_SHEET_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_BOTTOM_SHEET_PROVIDER_CONFIG,
  TbxMatBottomSheetSeverityFontIconService,
} from '@teqbench/tbx-mat-bottom-sheets';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    provideAnimationsAsync(),
    provideTbxMatSeverityTheme({ invert: false }),
    {
      provide: TBX_MAT_BOTTOM_SHEET_PROVIDER_CONFIG,
      useFactory: () => ({
        severityIconResolverService: new TbxMatBottomSheetSeverityFontIconService(
          '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 bottom-sheet styles in your application's stylesheet:

@use '@teqbench/tbx-mat-bottom-sheets/styles/tbx-mat-bottom-sheets';

Fire your first bottom sheet

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

import { Component, inject } from '@angular/core';
import { TbxMatBottomSheetService } from '@teqbench/tbx-mat-bottom-sheets';

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

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

The other tiers work the same way:

await this.bottomSheet.error({ title: 'Save Failed', message: 'Could not save changes.' });
await this.bottomSheet.warning({ title: 'Caution', message: 'This may take a while.' });
await this.bottomSheet.information({ title: 'FYI', message: 'New version available.' });
await this.bottomSheet.help({ title: 'How it works', message: 'Tap any control for details.' });
await this.bottomSheet.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 {
  TbxMatBottomSheetService,
  TbxMatBottomSheetDismissReason,
} from '@teqbench/tbx-mat-bottom-sheets';

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

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

Reasons cover every way a bottom sheet 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 TbxMatBottomSheetData<T> with two signals: isValid drives whether the affirm button is enabled, and value is what the bottom sheet 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 { TbxMatBottomSheetData } from '@teqbench/tbx-mat-bottom-sheets';

@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 TbxMatBottomSheetData<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.bottomSheet.input<string>({
  title: 'Rename',
  content: RenameFormComponent,
});

if (output.result === TbxMatBottomSheetDismissReason.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.bottomSheet.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: TbxMatBottomSheetDismissReason.Deny, align: 'end' },
    {
      key: 'yes',
      type: 'button',
      label: 'Yes',
      result: TbxMatBottomSheetDismissReason.Affirm,
      emphasis: 'primary',
      align: 'end',
    },
  ],
});

if (output.result === TbxMatBottomSheetDismissReason.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.bottomSheet.show({
  title: 'Custom Bottom Sheet',
  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: TbxMatBottomSheetDismissReason.Cancel, align: 'end' },
    {
      key: 'proceed',
      type: 'button',
      label: 'Proceed',
      result: TbxMatBottomSheetDismissReason.Affirm,
      emphasis: 'primary',
      align: 'end',
    },
  ],
});

What to try next

  • Dialogs — for an even heavier surface that centers in the viewport rather than anchoring to the bottom edge, see Show your first dialog. 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 bottom sheets with the inverted palette across all tiers at once; see the end-to-end section on the severity-theme guide.