ng-hub-ui-modal
v22.4.1
Published
Standalone, signal-based modal/dialog service and components for Angular (ng-hub-ui family).
Maintainers
Readme
ng-hub-ui-modal
Español | English
A standalone, fully-featured Angular modal library with flexible content projection, placement support, and full CSS variable theming. No Bootstrap or ng-bootstrap dependency required.
⚠️ WARNING: BREAKING CHANGES IN VERSION 21.0.0 If you are upgrading from
1.x.xto21.x.xand you have overridden the.modalor.modal-dialogCSS classes in your global stylesheets, please review the BREAKING_CHANGES.md document to migrate your styles to the newhub-modalBEM classes.
Documentation and Live Examples
This package is part of Hub UI, a collection of Angular component libraries for standalone apps.
- Docs: https://hubui.dev/modal/overview/
- Live examples: https://hubui.dev/modal/examples/
- Hub UI: https://hubui.dev/
🧩 Library Family ng-hub-ui
This library is part of the ng-hub-ui ecosystem:
- ng-hub-ui-accordion (deprecated — use ng-hub-ui-panels)
- ng-hub-ui-action-sheet
- ng-hub-ui-avatar
- ng-hub-ui-board
- ng-hub-ui-breadcrumbs
- ng-hub-ui-calendar
- ng-hub-ui-dropdown
- ng-hub-ui-ds
- ng-hub-ui-forms
- ng-hub-ui-history
- ng-hub-ui-milestones
- ➡️ ng-hub-ui-modal ← you are here
- ng-hub-ui-nav
- ng-hub-ui-paginable
- ng-hub-ui-panels
- ng-hub-ui-portal
- ng-hub-ui-skeleton
- ng-hub-ui-sortable
- ng-hub-ui-stepper
- ng-hub-ui-utils
📋 Table of Contents
Features
- Zero external dependencies: No ng-bootstrap, no Bootstrap JS.
- Three content types: Open modals with a
TemplateRef, aComponentclass, or a plainstring. - Flexible content projection: Use CSS selectors to route content to
header,body, andfooterslots. - Placement support: Anchor modals to any viewport edge —
start,end,top,bottom— or keep themcenter. - Modal stacking: Open multiple modals; focus management and aria-hidden are handled automatically.
- Programmatic dismiss/close guards: The
beforeDismisscallback lets you intercept and prevent dismissal. - Full keyboard & backdrop interaction: ESC key, static backdrop, backdrop click — all configurable.
- CSS Variable theming: Deep customization without overriding internal classes.
- BEM class architecture: All structural classes use the
hub-modal__*prefix to avoid conflicts. - Lifecycle Observables:
shown,hidden,closed,dismissedstreams for precise reactive flow. - Global defaults: Inject
HubModalConfigto set application-wide defaults.
Installation
npm install ng-hub-ui-modalQuick Start
Standalone (recommended)
import { Component, inject, TemplateRef } from '@angular/core';
import { HubModal } from 'ng-hub-ui-modal';
@Component({
selector: 'app-root',
standalone: true,
template: `
<button (click)="open(tpl)">Open Modal</button>
<ng-template #tpl let-close="close">
<div class="hub-modal__header"><h5>Hello!</h5></div>
<div class="hub-modal__body">Modal content goes here.</div>
<div class="hub-modal__footer">
<button (click)="close('done')">Close</button>
</div>
</ng-template>
`
})
export class AppComponent {
private modal = inject(HubModal);
open(tpl: TemplateRef<unknown>) {
this.modal
.open(tpl, { headerSelector: '.hub-modal__header', footerSelector: '.hub-modal__footer' })
.result.catch(() => {});
}
}NgModule (classic)
import { HubModalModule } from 'ng-hub-ui-modal';
@NgModule({
imports: [HubModalModule]
})
export class AppModule {}Examples
Open with TemplateRef
Open a modal whose content is defined inline as a template.
The template context exposes close and dismiss functions.
import { Component, inject, TemplateRef } from '@angular/core';
import { HubModal } from 'ng-hub-ui-modal';
@Component({
selector: 'app-example',
standalone: true,
template: `
<button (click)="open(tpl)">Open Template Modal</button>
<ng-template #tpl let-close="close" let-dismiss="dismiss">
<div class="hub-modal__body">
<p>This is a template modal.</p>
<button (click)="dismiss('cancel')">Cancel</button>
<button (click)="close('ok')">OK</button>
</div>
</ng-template>
`
})
export class TemplateModalComponent {
private modal = inject(HubModal);
open(tpl: TemplateRef<unknown>) {
this.modal
.open(tpl)
.result.then((result) => console.log('Closed with', result))
.catch((reason) => console.log('Dismissed:', reason));
}
}Open with Component
Pass any Angular component class to display it inside the modal.
The component can inject HubActiveModal to close or dismiss the modal from within.
import { Component, inject } from '@angular/core';
import { HubModal, HubActiveModal } from 'ng-hub-ui-modal';
/** Content component displayed inside the modal */
@Component({
selector: 'app-confirm-dialog',
standalone: true,
template: `
<div class="hub-modal__header"><h5>Confirm action</h5></div>
<div class="hub-modal__body">Are you sure you want to proceed?</div>
<div class="hub-modal__footer">
<button (click)="activeModal.dismiss('no')">Cancel</button>
<button (click)="activeModal.close(true)">Confirm</button>
</div>
`
})
export class ConfirmDialogComponent {
activeModal = inject(HubActiveModal);
}
/** Host component that opens the modal */
@Component({ selector: 'app-host', standalone: true, template: `<button (click)="openConfirm()">Delete</button>` })
export class HostComponent {
private modal = inject(HubModal);
openConfirm() {
this.modal
.open(ConfirmDialogComponent, {
headerSelector: '.hub-modal__header',
footerSelector: '.hub-modal__footer'
})
.result.then((confirmed) => {
if (confirmed) {
/* perform deletion */
}
})
.catch(() => {});
}
}Open with String
Display a quick text message without any additional component or template.
this.modal.open('This is a simple string modal.');Placement
Anchor the modal to any edge of the viewport using HubModalPlacement.
import { HubModal, HubModalPlacement } from 'ng-hub-ui-modal';
// Right side panel
this.modal.open(MyComponent, { placement: HubModalPlacement.End });
// Bottom sheet
this.modal.open(MyComponent, { placement: HubModalPlacement.Bottom });
// Left drawer, vertically centered
this.modal.open(MyComponent, {
placement: HubModalPlacement.Start,
centered: true
});| Value | Effect |
| -------------------------- | ----------------------------- |
| HubModalPlacement.Center | Centred in viewport (default) |
| HubModalPlacement.Start | Left-anchored drawer |
| HubModalPlacement.End | Right-anchored drawer |
| HubModalPlacement.Top | Top sheet |
| HubModalPlacement.Bottom | Bottom sheet |
Size and Fullscreen
// Predefined sizes
this.modal.open(MyComponent, { size: 'sm' }); // 'sm' | 'lg' | 'xl'
// Always fullscreen
this.modal.open(MyComponent, { fullscreen: true });
// Fullscreen only below 'md' breakpoint
this.modal.open(MyComponent, { fullscreen: 'md' });Scrollable Content
When the modal content overflows, enable internal scrolling.
this.modal.open(LongContentComponent, { scrollable: true });Static Backdrop
Prevent dismissal when clicking outside the modal.
this.modal.open(MyComponent, { backdrop: 'static' });
// Also disable ESC key
this.modal.open(MyComponent, { backdrop: 'static', keyboard: false });Before Dismiss Guard
Use beforeDismiss to prevent or delay modal closure, e.g. to show a confirmation first.
this.modal.open(MyFormComponent, {
beforeDismiss: () => {
if (this.formIsDirty) {
return confirm('You have unsaved changes. Really close?');
}
return true;
}
});
// Async guard using a Promise
this.modal.open(MyComponent, {
beforeDismiss: () => this.confirmService.ask('Discard changes?')
});HubActiveModal in Content Component
Inject HubActiveModal into any component used as modal content to control it from within.
import { Component, inject } from '@angular/core';
import { HubActiveModal, HubModalUpdatableOptions } from 'ng-hub-ui-modal';
@Component({
selector: 'app-my-modal',
standalone: true,
template: `
<div class="hub-modal__body">
<button (click)="save()">Save</button>
<button (click)="cancel()">Cancel</button>
</div>
`
})
export class MyModalComponent {
activeModal = inject(HubActiveModal);
save() {
this.activeModal.close({ saved: true });
}
cancel() {
this.activeModal.dismiss('user_cancelled');
}
}Typed Data Payload
Pass a data payload when opening and read it back typed inside the content component via HUB_MODAL_DATA (or HubActiveModal<D>.data):
import { inject } from '@angular/core';
import { HubModal, HUB_MODAL_DATA, HubActiveModal } from 'ng-hub-ui-modal';
interface EditUserData {
userId: string;
}
// Opening the modal:
inject(HubModal).open(EditUserComponent, { data: { userId: '42' } });
// Inside EditUserComponent:
export class EditUserComponent {
protected readonly data = inject<EditUserData>(HUB_MODAL_DATA);
// or: private readonly ref = inject<HubActiveModal<EditUserData>>(HubActiveModal); → this.ref.data
}Dismiss and Close Selectors
Automatically bind dismiss/close behaviour to DOM elements inside the modal content using CSS selectors.
this.modal.open(MyComponent, {
dismissSelector: '[data-dismiss="modal"]',
closeSelector: '[data-close="modal"]'
});<!-- Inside MyComponent template -->
<button data-dismiss="modal">Cancel</button>
<button data-close="modal">OK</button>Multiple Stacked Modals
Open modals from within a modal — the stack is managed automatically and focus is trapped to the topmost one.
@Component({ ... })
export class ParentModalComponent {
private modal = inject(HubModal);
openNested() {
this.modal.open(ChildModalComponent);
}
}Observables: dismissAll and hasOpenModals
Use the service methods to interact with the entire modal stack.
import { HubModal } from 'ng-hub-ui-modal';
export class AppComponent {
private modal = inject(HubModal);
closeAll() {
this.modal.dismissAll('route_change');
}
get anyOpen(): boolean {
return this.modal.hasOpenModals();
}
}Listen to activeInstances for reactive updates:
this.modal.activeInstances.subscribe((refs) => {
console.log(`${refs.length} modals open`);
});API Reference
HubModal Service
The main entry point for opening and managing modals.
| Method | Signature | Description |
| ----------------- | -------------------------------------- | ----------------------------------------------------- |
| open | open(content, options?): HubModalRef | Opens a new modal with the given content and options. |
| dismissAll | dismissAll(reason?): void | Dismisses all currently open modals. |
| hasOpenModals | hasOpenModals(): boolean | Returns true if at least one modal is open. |
| activeInstances | EventEmitter<HubModalRef[]> | Emits whenever the stack of open modals changes. |
HubModalRef
A reference to an open modal returned by HubModal.open().
| Member | Type | Description |
| ------------------- | ------------------ | ----------------------------------------------------------- |
| result | Promise<any> | Resolves on close(), rejects on dismiss(). |
| componentInstance | T \| void | Instance of the content component (if used). |
| close(result?) | void | Closes the modal and resolves result. |
| dismiss(reason?) | void | Dismisses the modal and rejects result. |
| update(options) | void | Updates modal options after opening. |
| closed | Observable<any> | Emits when the modal is closed via close(). |
| dismissed | Observable<any> | Emits when dismissed via dismiss() or user interaction. |
| shown | Observable<void> | Emits once the open animation finishes. |
| hidden | Observable<void> | Emits once the close animation finishes and DOM is removed. |
HubActiveModal
Inject into your content component to control the modal from within. Generic in the
payload type — HubActiveModal<D = unknown>.
| Member | Description |
| ------------------ | ---------------------------------------------------- |
| data | Read-only payload passed through the data option, typed D. Equivalent to inject(HUB_MODAL_DATA); resolves to null when no data was supplied. |
| close(result?) | Closes the modal with an optional result. |
| dismiss(reason?) | Dismisses the modal with an optional reason. |
| update(options) | Updates live options (same as HubModalRef.update). |
Typed payload. Prefer
inject(HUB_MODAL_DATA)orinject(HubActiveModal<MyData>).dataover reading adatafield off the component instance. The legacyObject.assign(instance, { data })monkey-patch is deprecated and kept for one release only.
HubModalOptions
All options accepted by HubModal.open().
| Option | Type | Default | Description |
| ------------------ | ------------------------------------------------------------ | ------------------------ | ----------------------------------------------------------- |
| animation | boolean | true | Enables fade in/out transitions. |
| ariaLabelledBy | string | — | ID of the element that labels the modal. |
| ariaDescribedBy | string | — | ID of the element that describes the modal. |
| backdrop | boolean \| 'static' | true | false = no backdrop, 'static' = click does not close. |
| beforeDismiss | () => boolean \| Promise<boolean> | — | Guard called before dismissal. Return false to cancel. |
| centered | boolean | false | Centers modal on the cross-axis for side placements. |
| placement | HubModalPlacement | Center | Viewport anchor for the modal. |
| container | string \| HTMLElement | body | CSS selector or element to which modals are appended. |
| fullscreen | boolean \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl' \| string | false | Fullscreen always or below a specific breakpoint. |
| injector | Injector | — | Custom injector for content component dependencies. |
| keyboard | boolean | true | Whether ESC key dismisses the modal. |
| scrollable | boolean | false | Makes the modal body scroll internally. |
| size | 'sm' \| 'lg' \| 'xl' \| string | — | Controls the width of the modal dialog. |
| variant | 'primary' \| 'success' \| 'danger' \| 'warning' \| 'info' \| string | — | Semantic accent for meaningful dialogs: top accent bar + accent-tinted surface, borders and title. Any custom string reads --hub-sys-color-<variant> from the host. |
| windowClass | string | — | Extra class added to the hub-modal host element. |
| modalDialogClass | string | — | Extra class added to the hub-modal__dialog element. |
| backdropClass | string | — | Extra class added to the hub-modal__backdrop element. |
| headerSelector | string | — | CSS selector for nodes to project into the header slot. |
| footerSelector | string | — | CSS selector for nodes to project into the footer slot. |
| dismissSelector | string | [data-dismiss="modal"] | Selector for elements that auto-dismiss the modal on click. |
| closeSelector | string | [data-close="modal"] | Selector for elements that auto-close the modal on click. |
| data | any | — | Typed payload delivered to the content component via inject(HUB_MODAL_DATA) or inject(HubActiveModal).data (the legacy instance-field monkey-patch is deprecated). |
HubModalUpdatableOptions
A subset of HubModalOptions that can be updated on an already-open modal via HubModalRef.update().
ariaLabelledBy, ariaDescribedBy, centered, placement, fullscreen, backdropClass, size, variant, windowClass, modalDialogClass.
HubModalPlacement
import { HubModalPlacement } from 'ng-hub-ui-modal';| Value | CSS class applied | Description |
| -------------------------- | ----------------------------- | ---------------------------------------- |
| HubModalPlacement.Center | (none) | Modal centred in the viewport (default). |
| HubModalPlacement.Start | hub-modal--placement-start | Left edge anchor. |
| HubModalPlacement.End | hub-modal--placement-end | Right edge anchor. |
| HubModalPlacement.Top | hub-modal--placement-top | Top edge anchor. |
| HubModalPlacement.Bottom | hub-modal--placement-bottom | Bottom edge anchor. |
ModalDismissReasons
Built-in dismiss reason constants.
import { ModalDismissReasons } from 'ng-hub-ui-modal';
modalRef.dismissed.subscribe((reason) => {
if (reason === ModalDismissReasons.ESC) {
/* ESC key */
}
if (reason === ModalDismissReasons.BACKDROP_CLICK) {
/* backdrop */
}
});HubModalConfig
Inject HubModalConfig to provide application-wide default options.
import { HubModalConfig } from 'ng-hub-ui-modal';
@Injectable({ providedIn: 'root' })
export class AppModalDefaults {
constructor(config: HubModalConfig) {
config.animation = true;
config.keyboard = false;
config.backdrop = 'static';
}
}Styling
The library publishes a self-contained stylesheet. Import it once in your application:
@import 'ng-hub-ui-modal/src/lib/modal.scss';CSS Variables
All visual aspects are controlled via --hub-modal-* tokens.
Full reference: docs/css-variables-reference.md
Quick reference (most common tokens):
| Variable | Default | Description |
| ------------------------------ | ------------------ | ------------------------- |
| --hub-modal-max-width | 500px | Max dialog width |
| --hub-modal-border-radius | 0.5rem | Dialog corner radius |
| --hub-modal-bg | system surface | Background color |
| --hub-modal-color | system text | Text color |
| --hub-modal-padding-x | 1rem | Content horizontal padding|
| --hub-modal-padding-y | 1rem | Content vertical padding |
| --hub-modal-backdrop-opacity | 0.5 | Backdrop opacity |
| --hub-modal-transition | 0.2s ease-in-out | Animation speed |
Customization Example
/* Override at the host element level */
hub-modal-window {
--hub-modal-max-width: 720px;
--hub-modal-border-radius: 1rem;
--hub-modal-backdrop-opacity: 0.7;
}Semantic Variants
Set variant to give a dialog a semantic accent (a destructive confirm, a success notice…). A variant recolours the whole dialog: a top accent bar, an accent-tinted background, accent-tinted borders (outer + header/footer rules) and an accent title.
this.modal.open(ConfirmDialogComponent, { variant: 'danger' });The built-in values (primary · success · danger · warning · info) map to the design-system colours, but any string is accepted — the modal reads --hub-sys-color-<variant> from the host application. The variant is also updatable via HubModalRef.update() / HubActiveModal.update(), and can be applied directly with windowClass: 'hub-modal--<variant>'.
These tokens drive the accent system:
| Variable | Default | Description |
| ------------------------------ | -------------------------------------- | --------------------------------------------------------------------- |
| --hub-modal-accent | var(--hub-sys-color-primary) | Base accent colour; a variant re-bases it from --hub-sys-color-<v>. |
| --hub-modal-accent-subtle | color-mix(accent 8%, surface) | Accent-tinted dialog background used under a variant. |
| --hub-modal-accent-border | color-mix(accent 35%, surface) | Accent-tinted border colour (outer + header/footer rules). |
| --hub-modal-accent-bar-width | var(--hub-ref-space-1, 4px) | Thickness of the top accent bar. |
| --hub-modal-title-color | neutral (--hub-modal-color) | Title colour; a variant re-points it to the accent. |
Sass theme mixin
For full one-call theming, use the hub-modal-theme() mixin. Every parameter is optional and defaults to null, so only the ones you pass are emitted as --hub-modal-* overrides; the rest keep their defaults. Apply it to the class you pass as windowClass (or to .hub-modal to theme every dialog).
@use 'ng-hub-ui-modal/styles/mixins/modal-theme' as *;
.branded-dialog {
@include hub-modal-theme(
$accent: var(--hub-sys-color-success),
$bg: #f6fff9,
$border-color: #b7e4c7,
$border-radius: 0.75rem,
$box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.2)
);
}
// this.modal.open(MyDialog, { windowClass: 'branded-dialog' });It covers accent, surfaces, colour, title, borders/radius/shadow, header/body/footer padding & gaps and the backdrop — token-based, with no Bootstrap dependency.
Bootstrap Integration (optional)
hub-modal-window {
--hub-modal-bg: var(--bs-body-bg);
--hub-modal-color: var(--bs-body-color);
--hub-modal-border-color: var(--bs-border-color);
}BEM Class Reference
| Class | Element |
| -------------------------------- | --------------------- |
| .hub-modal | Modal window host |
| .hub-modal__backdrop | Backdrop overlay |
| .hub-modal__dialog | Dialog container |
| .hub-modal__content | Content wrapper |
| .hub-modal__header | Header region |
| .hub-modal__body | Body region |
| .hub-modal__footer | Footer region |
| .hub-modal__close | Built-in close button |
| .hub-modal--placement-{value} | Placement modifier |
| .hub-modal__dialog--centered | Vertical centering |
| .hub-modal__dialog--scrollable | Scrollable body |
| .hub-modal__dialog--fullscreen | Fullscreen modifier |
Contributing
Development Setup
git clone https://github.com/carlos-morcillo/ng-hub-ui-modal.git
cd ng-hub-ui-modal
npm installBuild the library in watch mode:
ng build modal --watchServe the demo application:
ng serveTesting
ng test modalCommit Guidelines
Commits follow the Conventional Commits format:
feat(modal): add new placement option
fix(modal): correct backdrop z-index
docs(modal): update CSS variable tableSupport & License
If this library saves you time, consider supporting further development:
MIT License — © Carlos Morcillo
