ng-bootstrap
ng-bootstrap is a set of native Angular components built on top of the Bootstrap CSS framework. It re-implements Bootstrap’s interactive widgets — modals, tooltips, date pickers, accordions, and more — as pure Angular components, so you get Bootstrap’s familiar look and feel without pulling in jQuery or Bootstrap’s own JavaScript bundle. If your team already knows Bootstrap markup and wants idiomatic, change-detection-friendly Angular widgets, ng-bootstrap is the natural bridge.
Why ng-bootstrap instead of plain Bootstrap
Vanilla Bootstrap ships interactive behavior in bootstrap.bundle.js, which manipulates the DOM directly and historically depended on jQuery. That fights Angular’s rendering model and breaks change detection. ng-bootstrap solves this by providing the behavior as Angular components and directives while leaving the styling to the official Bootstrap CSS.
| Concern | Plain Bootstrap JS | ng-bootstrap |
|---|---|---|
| jQuery dependency | Legacy versions only | None |
| DOM manipulation | Imperative, outside Angular | Inside Angular’s render tree |
| Templating | HTML strings / data-attributes | Angular templates with bindings |
| Change detection | Manual / fragile | First-class |
| Tree-shaking | Whole bundle | Per-component imports |
ng-bootstrap supplies only the widgets and their behavior. You still install and import the Bootstrap CSS yourself — the library deliberately does not bundle styles so you control the exact Bootstrap version and theme.
Installation
Install the library and Bootstrap CSS, then wire the stylesheet into your build.
ng add @ng-bootstrap/ng-bootstrap
The schematic adds @ng-bootstrap/ng-bootstrap, installs bootstrap, and registers bootstrap.css in angular.json. If you prefer to do it by hand:
npm install @ng-bootstrap/ng-bootstrap bootstrap
Then add the CSS to the styles array in angular.json:
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.css"
]
Standalone components and imports
ng-bootstrap exports each widget as a standalone directive or component, so in a modern Angular app you import exactly what a component uses — no NgbModule. Import them directly into a standalone component’s imports array.
import { Component, signal } from '@angular/core';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-faq',
standalone: true,
imports: [NgbAccordionModule],
templateUrl: './faq.component.html',
})
export class FaqComponent {
panels = signal([
{ title: 'Shipping', body: 'We ship worldwide within 3 days.' },
{ title: 'Returns', body: '30-day no-questions-asked returns.' },
]);
}
<div ngbAccordion>
@for (panel of panels(); track panel.title) {
<div ngbAccordionItem>
<h2 ngbAccordionHeader>
<button ngbAccordionButton>{{ panel.title }}</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>{{ panel.body }}</ng-template>
</div>
</div>
</div>
}
</div>
Notice the @for control flow and signals driving the panel list — ng-bootstrap markup is just Angular templates, so all the modern features work unchanged.
Modals
Modals are opened imperatively through the injectable NgbModal service. Open returns an NgbModalRef whose result is a promise that resolves when the modal is closed and rejects when dismissed.
import { Component, inject, TemplateRef } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-confirm',
standalone: true,
templateUrl: './confirm.component.html',
})
export class ConfirmComponent {
private modal = inject(NgbModal);
open(content: TemplateRef<unknown>): void {
this.modal.open(content, { centered: true }).result.then(
(reason) => console.log('Closed:', reason),
() => console.log('Dismissed'),
);
}
}
<button class="btn btn-primary" (click)="open(dialog)">Delete account</button>
<ng-template #dialog let-modal>
<div class="modal-header">
<h5 class="modal-title">Confirm deletion</h5>
<button class="btn-close" (click)="modal.dismiss()"></button>
</div>
<div class="modal-body">This action cannot be undone.</div>
<div class="modal-footer">
<button class="btn btn-secondary" (click)="modal.dismiss()">Cancel</button>
<button class="btn btn-danger" (click)="modal.close('deleted')">Delete</button>
</div>
</ng-template>
Output:
Closed: deleted
Tooltips and popovers
Tooltips and popovers are directives — no service, no manual teardown. They position themselves and clean up automatically when the host element is destroyed.
<button class="btn btn-outline-secondary"
ngbTooltip="Saved 2 minutes ago"
placement="top">
Save
</button>
<button class="btn btn-info"
ngbPopover="Edit access lets users change content."
popoverTitle="Permissions"
triggers="mouseenter:mouseleave">
Editor
</button>
Date picker with reactive forms
The date picker integrates with Angular forms through NgbDateStruct, a plain { year, month, day } object rather than a native Date.
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NgbDatepickerModule, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-booking',
standalone: true,
imports: [FormsModule, NgbDatepickerModule],
template: `
<input class="form-control" placeholder="yyyy-mm-dd"
[(ngModel)]="date" ngbDatepicker #d="ngbDatepicker"
(click)="d.toggle()" />
`,
})
export class BookingComponent {
date: NgbDateStruct | null = null;
}
Global configuration
Each widget ships a config service you can override once, via DI, to set app-wide defaults — handy for consistent tooltip placement or modal sizing.
import { ApplicationConfig } from '@angular/core';
import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: NgbTooltipConfig,
useFactory: () => {
const config = new NgbTooltipConfig();
config.placement = 'right';
config.container = 'body';
return config;
},
},
],
};
Best Practices
- Import Bootstrap’s CSS yourself and keep its version aligned with the ng-bootstrap version you install — check the compatibility table in the release notes before upgrading.
- Import per-widget modules (e.g.
NgbAccordionModule) into standalone components instead of the umbrellaNgbModule, so unused widgets tree-shake away. - Open modals through the
NgbModalservice and always handle both branches ofresult— resolve for close, reject for dismiss — to avoid unhandled promise rejections. - Use the per-widget config services to set defaults once rather than repeating
placement/containerattributes across templates. - Pass an
injectortoNgbModal.openwhen the modal content needs your component’s DI context. - Set
container="body"on tooltips and popovers inside scrollable oroverflow:hiddencontainers to prevent clipping.