Create a Custom Tool
Overview
Section titled “Overview”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.
Available Components
Section titled “Available Components”All components are exported from ngx-dev-toolbar and can be imported directly:
| Component | Selector | Purpose |
|---|---|---|
ToolbarToolComponent | ndt-toolbar-tool | Main wrapper providing window management, positioning, and animations |
ToolbarButtonComponent | ndt-button | Action buttons with optional icon and label |
ToolbarInputComponent | ndt-input | Text inputs with two-way binding via [(value)] |
ToolbarSelectComponent | ndt-select | Dropdown selection with CDK overlay positioning |
ToolbarCardComponent | ndt-card | Content container with hover effects |
ToolbarClickableCardComponent | ndt-clickable-card | Interactive card with icon, title, and subtitle |
ToolbarListComponent | ndt-list | List container with empty states and no-results handling |
ToolbarListItemComponent | ndt-list-item | List items with optional badge |
ToolbarStepViewComponent | ndt-step-view | Multi-step view switcher with back navigation |
ToolbarIconComponent | ndt-icon | SVG icons with consistent sizing |
ToolbarLinkButtonComponent | ndt-link-button | External link button with icon |
Step-by-Step Guide
Section titled “Step-by-Step Guide”1. Define Your Model
Section titled “1. Define Your Model”Create a TypeScript interface that describes the data your tool manages:
export interface Note { id: string; title: string; content: string; createdAt: Date;}2. Create the Internal Service
Section titled “2. Create the Internal Service”Use Angular signals for state management. The service should expose readonly signals and update methods:
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) ); }}3. Build the Component
Section titled “3. Build the Component”Wrap your tool’s content in ToolbarToolComponent. This provides the window
management, overlay positioning, and animations automatically:
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', };}4. Register Your Tool
Section titled “4. Register Your Tool”Add your tool component to the toolbar’s template in your application. Custom tools appear alongside the built-in tools in the toolbar strip.
Window Options
Section titled “Window Options”Configure how your tool’s window appears using ToolbarWindowOptions:
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 spaceAvailable Icons
Section titled “Available Icons”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
Multi-Step Tools
Section titled “Multi-Step Tools”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:
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', };}How it Works
Section titled “How it Works”[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 buttonngtStep— Structural directive marking each step templatestepTitle— Optional title shown in the header for each step
Styling
Section titled “Styling”Use the toolbar’s design tokens for consistent styling. All tokens use the --ndt- prefix:
/* 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);}Design Tokens Reference
Section titled “Design Tokens Reference”| Category | Token | Usage |
|---|---|---|
| Colors | --ndt-background-primary | Primary background |
| Colors | --ndt-background-secondary | Secondary background |
| Colors | --ndt-text-primary | Primary text |
| Colors | --ndt-text-secondary | Secondary text |
| Colors | --ndt-text-muted | Muted text |
| Colors | --ndt-border-primary | Border color |
| Spacing | --ndt-spacing-xs | Extra-small spacing |
| Spacing | --ndt-spacing-sm | Small spacing |
| Spacing | --ndt-spacing-md | Medium spacing |
| Spacing | --ndt-spacing-lg | Large spacing |
| Radius | --ndt-border-radius-small | Small border radius |
| Radius | --ndt-border-radius-medium | Medium border radius |
| Radius | --ndt-border-radius-large | Large border radius |
| Font | --ndt-font-size-xs | Extra-small font |
| Font | --ndt-font-size-sm | Small font |
| Font | --ndt-font-size-md | Medium font |
| Font | --ndt-font-size-lg | Large font |
Defensive CSS
Section titled “Defensive CSS”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: 0on headers/footers to prevent collapse - Use
min-height: 0on scrollable content areas
Best Practices
Section titled “Best Practices”- 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
ToolbarStorageServiceif you need to persist state across page reloads
Complete Example
Section titled “Complete Example”Here’s a full Notes Tool that combines a model, service, and component with multi-step navigation:
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'); } }}