Skip to content

Create a Custom Tool

The ngx-dev-toolbar exports its UI components so you can build custom tools that integrate seamlessly with the toolbar. Each tool follows a consistent pattern: a model defining data structures, an internal service managing state with signals, and a component that wraps its content in ToolbarToolComponent.

All components are exported from ngx-dev-toolbar and can be imported directly:

ComponentSelectorPurpose
ToolbarToolComponentndt-toolbar-toolMain wrapper providing window management, positioning, and animations
ToolbarButtonComponentndt-buttonAction buttons with optional icon and label
ToolbarInputComponentndt-inputText inputs with two-way binding via [(value)]
ToolbarSelectComponentndt-selectDropdown selection with CDK overlay positioning
ToolbarCardComponentndt-cardContent container with hover effects
ToolbarClickableCardComponentndt-clickable-cardInteractive card with icon, title, and subtitle
ToolbarListComponentndt-listList container with empty states and no-results handling
ToolbarListItemComponentndt-list-itemList items with optional badge
ToolbarStepViewComponentndt-step-viewMulti-step view switcher with back navigation
ToolbarIconComponentndt-iconSVG icons with consistent sizing
ToolbarLinkButtonComponentndt-link-buttonExternal link button with icon

Create a TypeScript interface that describes the data your tool manages:

notes.models.ts
export interface Note {
id: string;
title: string;
content: string;
createdAt: Date;
}

Use Angular signals for state management. The service should expose readonly signals and update methods:

notes-internal.service.ts
import { Injectable, signal } from '@angular/core';
import { Note } from './notes.models';
@Injectable({ providedIn: 'root' })
export class NotesInternalService {
private readonly _notes = signal<Note[]>([]);
readonly notes = this._notes.asReadonly();
addNote(note: Note): void {
this._notes.update(notes => [...notes, note]);
}
removeNote(id: string): void {
this._notes.update(notes => notes.filter(n => n.id !== id));
}
updateNote(id: string, updates: Partial<Note>): void {
this._notes.update(notes =>
notes.map(n => n.id === id ? { ...n, ...updates } : n)
);
}
}

Wrap your tool’s content in ToolbarToolComponent. This provides the window management, overlay positioning, and animations automatically:

notes-tool.component.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
ToolbarToolComponent,
ToolbarWindowOptions,
ToolbarButtonComponent,
ToolbarInputComponent,
ToolbarListComponent,
ToolbarListItemComponent,
} from 'ngx-dev-toolbar';
import { NotesInternalService } from './notes-internal.service';
@Component({
selector: 'app-notes-tool',
standalone: true,
imports: [
ToolbarToolComponent,
ToolbarButtonComponent,
ToolbarInputComponent,
ToolbarListComponent,
ToolbarListItemComponent,
],
template: `
<ndt-toolbar-tool
[options]="windowOptions"
title="Notes"
icon="edit"
>
<ndt-list
[hasItems]="notesService.notes().length > 0"
emptyMessage="No notes yet"
>
@for (note of notesService.notes(); track note.id) {
<ndt-list-item [label]="note.title" />
}
</ndt-list>
</ndt-toolbar-tool>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NotesToolComponent {
protected readonly notesService = inject(NotesInternalService);
readonly windowOptions: ToolbarWindowOptions = {
id: 'notes',
title: 'Notes',
description: 'Quick notes for development',
size: 'medium',
};
}

Add your tool component to the toolbar’s template in your application. Custom tools appear alongside the built-in tools in the toolbar strip.

Configure how your tool’s window appears using ToolbarWindowOptions:

window-options.ts
const windowOptions: ToolbarWindowOptions = {
id: 'my-tool', // Unique tool identifier
title: 'My Tool', // Window title
description: 'Tool info', // Optional description shown in header
isClosable: true, // Show close button (default: true)
placement: 'bottom-center', // Window position relative to toolbar
size: 'medium', // 'small' | 'medium' | 'tall' | 'large'
isBeta: false, // Show beta badge
};
// Available placements:
// 'bottom-left' | 'bottom-center' | 'bottom-right'
// 'top-left' | 'top-center' | 'top-right'
// Available sizes:
// 'small' - compact tools
// 'medium' - standard tools (default)
// 'tall' - tools with long lists
// 'large' - tools needing more space

The ToolbarIconComponent supports the following icon names via the IconName type:

angular, bolt, bug, code, database, discord, docs, edit, export, filter, flag, gauge, gear, git-branch, import, layout, lighting, lightbulb, lock, moon, network, puzzle, refresh, star, sun, terminal, toggle-left, translate, trash, user

For tools that need multiple views (e.g., list/create/edit), use the ToolbarStepViewComponent with the ngtStep directive. It handles back navigation and step switching automatically:

multi-step-tool.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
ToolbarToolComponent,
ToolbarWindowOptions,
ToolbarStepViewComponent,
ToolbarStepDirective,
ToolbarButtonComponent,
} from 'ngx-dev-toolbar';
@Component({
selector: 'app-multi-step-tool',
standalone: true,
imports: [
ToolbarToolComponent,
ToolbarStepViewComponent,
ToolbarStepDirective,
ToolbarButtonComponent,
],
template: `
<ndt-toolbar-tool [options]="windowOptions" title="Items" icon="star">
<ndt-step-view
[currentStep]="viewMode()"
defaultStep="list"
(back)="viewMode.set('list')"
>
<ng-template ngtStep="list" stepTitle="All Items">
<p>Your list content here...</p>
<ndt-button (click)="viewMode.set('create')">
Create New
</ndt-button>
</ng-template>
<ng-template ngtStep="create" stepTitle="Create Item">
<p>Your create form here...</p>
</ng-template>
<ng-template ngtStep="edit" stepTitle="Edit Item">
<p>Your edit form here...</p>
</ng-template>
</ndt-step-view>
</ndt-toolbar-tool>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MultiStepToolComponent {
viewMode = signal<'list' | 'create' | 'edit'>('list');
readonly windowOptions: ToolbarWindowOptions = {
id: 'items',
title: 'Items',
size: 'medium',
};
}
  • [currentStep] — Bind to a signal that tracks the active step name
  • [defaultStep] — The “home” step (back button is hidden on this step)
  • (back) — Emitted when the user clicks the back button
  • ngtStep — Structural directive marking each step template
  • stepTitle — Optional title shown in the header for each step

Use the toolbar’s design tokens for consistent styling. All tokens use the --ndt- prefix:

notes-tool.component.scss
/* Use design tokens for consistent styling */
.notes-container {
display: flex;
flex-direction: column;
gap: var(--ndt-spacing-md);
padding-top: var(--ndt-spacing-md);
}
.note-item {
padding: var(--ndt-spacing-sm) var(--ndt-spacing-md);
border-radius: var(--ndt-border-radius-medium);
background: var(--ndt-background-secondary);
color: var(--ndt-text-primary);
border: 1px solid var(--ndt-border-primary);
}
.note-item__title {
font-size: var(--ndt-font-size-md);
font-weight: 600;
color: var(--ndt-text-primary);
}
.note-item__content {
font-size: var(--ndt-font-size-sm);
color: var(--ndt-text-secondary);
margin-top: var(--ndt-spacing-xs);
}
CategoryTokenUsage
Colors--ndt-background-primaryPrimary background
Colors--ndt-background-secondarySecondary background
Colors--ndt-text-primaryPrimary text
Colors--ndt-text-secondarySecondary text
Colors--ndt-text-mutedMuted text
Colors--ndt-border-primaryBorder color
Spacing--ndt-spacing-xsExtra-small spacing
Spacing--ndt-spacing-smSmall spacing
Spacing--ndt-spacing-mdMedium spacing
Spacing--ndt-spacing-lgLarge spacing
Radius--ndt-border-radius-smallSmall border radius
Radius--ndt-border-radius-mediumMedium border radius
Radius--ndt-border-radius-largeLarge border radius
Font--ndt-font-size-xsExtra-small font
Font--ndt-font-size-smSmall font
Font--ndt-font-size-mdMedium font
Font--ndt-font-size-lgLarge font

When building custom tools, follow these defensive CSS practices to ensure your tool works correctly in any host application:

  • Always use explicit spacing (padding-top, margin-top, gap) instead of relying on browser defaults
  • Use flex-shrink: 0 on headers/footers to prevent collapse
  • Use min-height: 0 on scrollable content areas
  • Use OnPush change detection for all components
  • Use Angular signals for state management (signal(), computed())
  • Keep state transformations pure and immutable (spread operators, .filter(), .map())
  • Follow the component property order: injects, inputs, outputs, signals, computed, methods
  • Use standalone: true for all components
  • Add aria-label attributes to interactive elements for accessibility
  • Use the ToolbarStorageService if you need to persist state across page reloads

Here’s a full Notes Tool that combines a model, service, and component with multi-step navigation:

notes-tool-complete.component.ts
import { ChangeDetectionStrategy, Component, inject, signal, computed } from '@angular/core';
import { Injectable } from '@angular/core';
import {
ToolbarToolComponent,
ToolbarWindowOptions,
ToolbarButtonComponent,
ToolbarInputComponent,
ToolbarListComponent,
ToolbarListItemComponent,
ToolbarStepViewComponent,
ToolbarStepDirective,
} from 'ngx-dev-toolbar';
// --- Model ---
interface Note {
id: string;
title: string;
content: string;
}
// --- Service ---
@Injectable({ providedIn: 'root' })
class NotesService {
private readonly _notes = signal<Note[]>([]);
readonly notes = this._notes.asReadonly();
add(title: string, content: string): void {
this._notes.update(notes => [
...notes,
{ id: crypto.randomUUID(), title, content }
]);
}
remove(id: string): void {
this._notes.update(notes => notes.filter(n => n.id !== id));
}
}
// --- Component ---
@Component({
selector: 'app-notes-tool',
standalone: true,
imports: [
ToolbarToolComponent,
ToolbarButtonComponent,
ToolbarInputComponent,
ToolbarListComponent,
ToolbarListItemComponent,
ToolbarStepViewComponent,
ToolbarStepDirective,
],
template: `
<ndt-toolbar-tool [options]="windowOptions" title="Notes" icon="edit">
<ndt-step-view
[currentStep]="viewMode()"
defaultStep="list"
(back)="viewMode.set('list')"
>
<ng-template ngtStep="list" stepTitle="All Notes">
<ndt-button (click)="viewMode.set('create')" icon="edit">
Add Note
</ndt-button>
<ndt-list
[hasItems]="notesService.notes().length > 0"
emptyMessage="No notes yet"
emptyHint="Click 'Add Note' to create one"
>
@for (note of notesService.notes(); track note.id) {
<ndt-list-item [label]="note.title">
<ndt-button
variant="icon"
icon="trash"
ariaLabel="Delete"
(click)="notesService.remove(note.id)"
/>
</ndt-list-item>
}
</ndt-list>
</ng-template>
<ng-template ngtStep="create" stepTitle="New Note">
<div class="form">
<ndt-input
[(value)]="newTitle"
placeholder="Note title"
ariaLabel="Note title"
/>
<ndt-input
[(value)]="newContent"
placeholder="Note content"
ariaLabel="Note content"
/>
<ndt-button (click)="onCreate()" label="Save" />
</div>
</ng-template>
</ndt-step-view>
</ndt-toolbar-tool>
`,
styles: `
.form {
display: flex;
flex-direction: column;
gap: var(--ndt-spacing-sm);
padding-top: var(--ndt-spacing-sm);
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NotesToolComponent {
protected readonly notesService = inject(NotesService);
viewMode = signal<'list' | 'create'>('list');
newTitle = signal('');
newContent = signal('');
readonly windowOptions: ToolbarWindowOptions = {
id: 'notes',
title: 'Notes',
description: 'Quick development notes',
size: 'medium',
};
onCreate(): void {
if (this.newTitle()) {
this.notesService.add(this.newTitle(), this.newContent());
this.newTitle.set('');
this.newContent.set('');
this.viewMode.set('list');
}
}
}