Angular Modal Component | ng-hub-ui-modal
Accessible Angular modal component with stacking, fullscreen mode, templates, keyboard handling and CSS variables for standalone apps.
Last updated Sep 6, 2026
Overview
Why teams search for this library
This Angular modal component is built for teams that need accessible dialogs, composable content, service-driven control and design-system friendly styling in standalone Angular apps.
Install
npm install ng-hub-ui-modalJump to
Ideal for
- confirmation dialogs
- forms in overlays
- detail panels
- stacked workflows
About modal
ng-hub-ui-modal solves one of the most common UI needs in Angular products: opening dialogs that feel robust in production. It covers stacked modals, fullscreen flows, keyboard handling, projections and service-based usage without turning modal logic into a per-project problem.
Feature guides
Opening Modals (Content Types)
Demonstrates opening modals with TemplateRef, Component, and String content.
Configuration & Options
Customize modal appearance and behavior
Modal Reference (HubModalRef)
Control and access the state of opened modals
Active Modal (HubActiveModal)
Interact with the modal from within its content
Stack Management
Managing multiple stacked modals
Key features
Recent changes
Version 22.11.0 - 9/6/26, 12:00 AM
added: closeAriaLabel names the dismiss button the library draws, so an application can finally translate it. That button carries no text, because its glyph is painted by CSS, which makes its aria-label the entire name a screen reader reads out, and it was the literal Close written into the template: a localized application shipped one control it had no way to translate, and not a rare one, since that header is what the library builds whenever headerSelector or footerSelector is set. The option follows the shape every other option here already has, sitting on HubModalOptions with its default on HubModalConfig, so it can be set once for the whole application or per call. It still defaults to Close, so nothing changes for an application that says nothing.
changed: modal-stack.ts no longer dresses the component host with component-host-scrollable. That element is only a query root: splitIntoSlots takes its children into the window and the host itself never enters the document, and this library ships no rule for the class either, so the line could not affect anything a consumer can see. scrollable still reaches the dialog through hub-modal__dialog--scrollable, set in HubModalWindow, and keeps working exactly as before. The old FIXME that asked for this is replaced by a note saying why the host cannot be styled, so the class does not come back.
changed: Internal hygiene in the same file, with no change in behaviour: the JSDoc of extractAndRemoveNodesBySelector moved from above splitIntoSlots, where it described a function it did not belong to, down onto its own; a const bound to the void return of addEventListener was dropped; a selector that was queried twice in a row to remove what had just been read now reuses the single result; and the two remaining Spanish comments, which only restated the line under them, are gone.
fixed: The documentation described an API this library does not have. Both READMEs opened with zero external dependencies while four files import from ng-hub-ui-utils and the manifest declares it as a peer, so the install instructions left a reader one unresolved import short of a build. offcanvas was missing from the English options table and from both HubModalUpdatableOptions lists, which reads as if a drawer could not be toggled on an open dialog; the Spanish table was missing ariaLabelledBy, ariaDescribedBy and bodySelector on top of that, and the Spanish API reference had no HubActiveModal, ModalDismissReasons, HubModalConfig or BEM class sections at all. All of it now matches modal-config.ts.
fixed: The zindex rename shipped in 22.2.0 had no migration entry. --hub-modal-z-index became --hub-modal-zindex and --hub-modal-backdrop-z-index became --hub-modal-backdrop-zindex, and a custom property nobody reads raises no error, so a host that had set the old names lost them in silence, with a dialog sliding behind its own chrome as the only symptom. BREAKING_CHANGES.md now carries that section, where it belonged since June.
deprecated: HubModalModule is deprecated and will be removed in 23.0.0. Its whole body is providers: [HubModal], and HubModal is providedIn: root, so importing the module never enabled the service; it only added a redundant second instance in whichever injector declared the import, delegating to the same root HubModalStack and HubModalConfig. Inject HubModal and drop the import. The class carried no @deprecated tag until now, so neither an editor nor the build could warn anyone it was on its way out. See BREAKING_CHANGES.md.
Version 22.10.0 - 9/5/26, 12:00 AM
fixed: A custom property is substituted where it is DECLARED, and the library declared its whole default block on :root. Every token derived from another --hub-modal-* token therefore resolved against the root's values and reached the dialog already cooked, which is why assigning a shorthand did so little: --hub-modal-padding-x: 3rem left the header at 16px, --hub-modal-margin-y: 5rem left the dialog at 28px, a dark --hub-modal-bg kept a white footer, --hub-modal-color left the title at #212529, --hub-modal-border-radius never reached the placement corners, and a variant re-basing --hub-modal-accent recoloured the top bar and the title over a tint still mixed from the host's own brand colour — which is the symptom that started this: a warning dialog on an olive-green product painted its bar yellow and kept a green background and green header and footer rules. The defaults now sit under :where(.hub-modal). It matches the same element and contributes zero specificity, so a consumer's own .hub-modal rule still wins — which is what 22.8.0 moved to :root to achieve — and every link of every chain now resolves against the value that won ON THAT ELEMENT, whether it came from a variant class, a windowClass or the consumer's own rule. A custom variant is finally what the stylesheet always claimed: one rule re-bases the accent and the tint, the borders and the title re-derive from it.
fixed: HubModal.open() with a plain string opened an empty dialog the keyboard could not close. Every other kind of content passes through splitIntoSlots, which returns the three slots attachContent destructures — header, body, footer. The string path returned one, so the text was appended to the HEADER and the body arrived undefined; appending it threw a TypeError part-way through the open sequence, before the window armed its Escape listener. The dialog rendered blank, with no content and no close button, and Escape did nothing because no listener existed — the backdrop click was the only way out. The string now goes into the body slot like everything else, pinned by three specs. This is the second time Escape has stopped working for a reason unrelated to the keyboard; 22.4.1 was the first, when NgZone.onStable never emitted in a zoneless app and the listener was likewise never attached.
fixed: --hub-modal-title-color reached a title classed hub-modal__title. The rule that reads it matched .modal-title alone, the name from when this library sat on top of Bootstrap and the name every example here still writes. A consumer who never used Bootstrap wrote the house name, read the token in the README and got nothing. Both class names are matched now, and neither is going away: the heading is authored by the caller, not by the library, so it cannot simply be renamed.
changed: The accent bar above a variant dialog is off by default: --hub-modal-accent-bar-width ships at 0 instead of 0.25rem. The bar was the loudest half of a variant and the half nobody asked for — the dialog already reads as danger through its tint, its two borders and its title, and the stripe mostly competed with whatever the host had put at the top of its own chrome. The bar is now a layer on the content rather than its top border: as a border it replaced the dialog's own top edge, so switching it off left a tinted box with three sides and an open top. A variant dialog is 3px shorter as a result, and turning the bar back on no longer costs the frame. It is one assignment away, and it goes on the dialog: .hub-modal reaches every dialog in the application, a windowClass only some. Not :root, because the token is declared on the element and an inherited value loses to it, which is the same rule the rest of this release is about.
changed: On a warning or danger dialog the footer background now follows the accent-tinted surface instead of staying white. --hub-modal-footer-bg derives from --hub-modal-bg, a variant sets --hub-modal-bg to the accent tint, and on :root that derivation never saw the variant. It is the same defect being corrected, and the only rendering this release changes that nobody chose. Measured rather than inspected: 19,470 computed properties across 81 dialog configurations plus the backdrop, old stylesheet against new, and exactly 64 differed — 18 this footer, and 46 the accent bar standing down (16 top-border widths, 16 top-border colours and 14 heights). The default dialog, every size, centered, scrollable, fullscreen, every placement, offcanvas and the backdrop are byte-identical.
changed: Breaking: the defaults are no longer declared on :root. 22.8.0 put them there three days ago so that the runtime-injected stylesheet would stop beating a consumer's own .hub-modal rule on source order alone; :where() buys the same thing without costing the derivations. Anyone who took that opportunity to set a --hub-modal-* token on :root, html or body now loses to the library's element-level default and has to move the declaration onto the dialog. The documented routes are unaffected and work better than before: a .hub-modal rule, a windowClass, the hub-modal-theme() mixin. What a SIBLING reads stays on :root — --hub-modal-zindex, --hub-modal-backdrop-zindex and the four backdrop tokens — because .hub-modal__backdrop is not a descendant of .hub-modal and cannot inherit from it. That leaves one pair that still will not follow the dialog, and it is better said than discovered: raising --hub-modal-zindex on .hub-modal moves the window and leaves the backdrop behind, measured at 3000 against 1054. No declaration site fixes it, because two siblings cannot share a value through inheritance. Theme the stacking order on :root, or give the backdrop its own token through backdropClass. See BREAKING_CHANGES.md.
Version 22.9.0 - 9/3/26, 12:00 AM
fixed: The dialog no longer opens with focus on its own close button. Being first in the DOM, it took focus on every open: a destructive confirm opened with the caret on «cancel this dialog» rather than on what the dialog asks. It is skipped when choosing where focus lands; hubAutofocus still names it explicitly for anyone who wants it there.
fixed: The close button's focus ring follows the dialog accent instead of the browser default, and is :focus-visible so a mouse click leaves no ring. Four new slots: --hub-modal-close-focus-ring-width, -color, -offset and -radius.
Version 22.8.0 - 9/2/26, 12:00 AM
added: offcanvas: a dialog that touches the edge it slid out of. placement always slid a dialog in from an edge, but never assumed one anchored to an edge wants to touch it: it kept the margins of a floating dialog, which left a strip of page showing along the bottom, kept the rounding on the side it was attached to, was sized by its content rather than reaching the floor, and took its width from the size scale, where lg is 800px and covers the document the drawer is meant to be read against. offcanvas: true settles all four, with its own width through --hub-modal-offcanvas-width. Separate from placement rather than implied by it, so an existing consumer of placement sees exactly what it saw.
changed: The library's default custom properties are declared on :root rather than on .hub-modal. This stylesheet is injected at runtime, so on .hub-modal every default beat a consumer's own .hub-modal rule at equal specificity by source order alone, and assigning a token did nothing unless you out-specified the primitive. On :root the values arrive by inheritance and any declaration closer to the element wins. Breaking: a .hub-modal rule of yours now applies where it was silently ignored. See BREAKING_CHANGES.md.
Version 22.7.1 - 9/1/26, 12:00 AM
changed: The homepage in the manifest points at this library's own documentation page rather than at the site root. It is the link a registry shows beside the package, and landing on a front page you then have to search is a worse answer than landing on the reference for the package you were already looking at. Metadata only — no code, no types and no styles change.
Version 22.7.0 - 8/21/26, 12:00 AM
added: bodySelector: the body becomes a slot with a name. It was the one part of a modal with no way to point at it — the body was whatever survived the header and the footer being taken out, which holds only while the three parts are written in order and the content contains nothing else. A comment, a stray text node or an ng-container holding state joined the body, and moving a block in the template changed the result. Adding the option to content that already works cannot lose anything: whatever the selector matches goes into the body first, and everything unclaimed by any of the three slots follows it. Fixed alongside it, because naming the body is what exposed it: the body used to be read BETWEEN the header and footer extractions, so the footer marker was still a child when the body was captured and an emptied div rode along into it.
fixed: Every dialog is bounded by the viewport, and its body is what scrolls. This was scrollable's job alone, which put the decision in the wrong hands: whether a dialog outgrows the screen depends on its content, on the length of the translation and on the height of the window, and the caller knows none of the three when it opens the thing. A dialog that outgrew the screen simply extended past it, and what falls off the bottom is the footer — in a wizard, the "Next" button, unreachable. The cap is stated against the viewport (100dvh minus --hub-modal-dialog-inset), because the dialog's own height is auto and a percentage against it is not a definite reference. This changes the default: a consumer that deliberately let a dialog run past the viewport now gets a capped dialog with a scrolling body. scrollable keeps its own meaning — it is what pins the dialog itself.
Version 22.6.0 - 8/17/26, 12:00 AM
added: The dialog now travels between heights instead of jumping. A modal is sized by whatever it holds, so a wizard step or an async panel changing would snap the box to its new height in a single frame. The obvious repair does not work and was measured rather than assumed: the specified height is auto before the change and auto after it, and a CSS transition only fires when the specified value changes — the content moved, the property did not. interpolate-size does not help either; it interpolates to a keyword, it does not notice a box growing underneath one. So both heights are measured and animated explicitly, which also behaves identically in every browser. Tuned with --hub-modal-resize-duration and --hub-modal-resize-easing, disabled by [animation]="false" and by prefers-reduced-motion. Width already animated and is untouched.
Version 22.5.1 - 8/8/26, 12:00 AM
fixed: Documentation links now point at the canonical localized URLs. The README linked to https://hubui.dev/<path> with no locale prefix and no trailing slash, and both forms are 301-redirected, so every reader arriving from npm or GitHub landed on a redirect instead of the canonical page.
Version 22.5.0 - 7/27/26, 12:00 AM
changed: Typed modal flows: open<C, R, D>(content, options) infers the content component type, so HubModalRef.componentInstance is the component instance (no more as unknown as casts); R types the result end to end (close(result?), result: Promise<R>, closed: Observable<R>, HubActiveModal<D, R>) and HubModalOptions<D> types the data payload. All generics default to the previous loose types, so existing call sites compile unchanged.
Version 22.4.2 - 7/26/26, 12:00 AM
fixed: Declared the real ng-hub-ui-utils peer range (>=22.0.0); the previous >=1.0.0 floor allowed resolving an incompatible utils major.
Version 22.4.1 - 7/10/26, 12:00 AM
fixed: Escape and backdrop click did nothing in a zoneless application. The window and the backdrop deferred their entry work to NgZone.onStable, and under provideZonelessChangeDetection() the injected zone is a NoopNgZone whose onStable never emits, so that work never ran: _show() — and with it _enableEventHandling() — never executed, leaving every modal unclosable by Escape or by clicking the backdrop, because no listener had ever been attached. Both defer with afterNextRender now, which fires in zoneful and zoneless apps alike. No API change.
Version 22.4.0 - 7/7/26, 12:00 AM
changed: BREAKING (packaging): the SCSS ships at ng-hub-ui-modal/styles. The theme mixin now builds to dist/modal/styles/… (was dist/modal/src/lib/styles/…), so @use 'ng-hub-ui-modal/styles' resolves. Update any @use that reached into src/lib/styles. See BREAKING_CHANGES.md.
Version 22.3.0 - 7/5/26, 12:00 AM
added: Typed modal payload: inject the new HUB_MODAL_DATA token or use inject(HubActiveModal).data (HubActiveModal is now generic, HubActiveModal<D>) instead of the old untyped data field (now deprecated).
Version 22.2.1 - 7/2/26, 12:00 AM
fixed: Docs: the default values in docs/css-variables-reference.md were resynchronized with the actual code declarations, now guarded by the repo-level tokens-parity check.
Version 22.2.0 - 6/26/26, 12:00 AM
changed: BREAKING: canonical zindex token names — --hub-modal-z-index becomes --hub-modal-zindex and --hub-modal-backdrop-z-index becomes --hub-modal-backdrop-zindex, with no hyphen, matching the --hub-sys-zindex-* convention. A host that set the old names lost them silently, since a custom property nobody reads raises no error. See BREAKING_CHANGES.md.
changed: The accent system moved to the open-set local accent slot: a variant re-bases a single --hub-modal-accent, and the role family (--hub-modal-accent-emphasis, -subtle, -border and the new -on) derives locally from it with color-mix(in oklch, …), mirroring the ng-hub-ui-ds engine. The built-in list grew from five to the nine canonical accents (primary, secondary, success, danger, warning, info, neutral, light, dark), and any custom accent recolours the whole dialog with one rule that re-bases the slot.
added: New tokens --hub-modal-accent-on (a grayscale contrast flip driven by the accent's own lightness, for accent-filled surfaces) and --hub-modal-accent-emphasis.
fixed: The accent color-mix() derivations (--hub-modal-accent-subtle / -border) moved from srgb to oklch for perceptually uniform tints, matching ng-hub-ui-ds. The subtle tint derives at 12% (was 8%).
Version 22.1.2 - 6/26/26, 12:00 AM
fixed: Corrected both peer dependency ranges. Angular moved to >=18.0.0: the library uses signal input()/output(), the @if control flow and signal queries, so the previous >=16.0.0 let it install on versions it cannot run on. ng-hub-ui-utils moved to >=1.0.0: the previous caret range resolved to >=1 <2, which excluded the current utils (22.x) and made the peer impossible to satisfy.
Version 22.1.1 - 6/25/26, 12:00 AM
fixed: Design-token consistency pass: the inline fallback defaults were aligned with the canonical ng-hub-ui-ds values, and hardcoded literals (z-index, font-weight, line-height, radii and theme-aware colours) were routed through their --hub-sys-* / --hub-ref-* tokens so they follow the active theme. No visual change when the ds tokens are loaded.
Version 22.1.0 - 6/24/26, 12:00 AM
added: The variant option: a semantic accent for meaningful dialogs. 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. It is updatable through HubModalRef.update() / HubActiveModal.update(), and can be applied directly with windowClass: 'hub-modal--<variant>'.
added: The hub-modal-theme() Sass mixin: accent, surfaces, colour, title, borders, radius, shadow, header/body/footer padding and gaps, and the backdrop, in one call. Every parameter is optional and defaults to null, so only what you pass is emitted as a --hub-modal-* override. New tokens alongside it: --hub-modal-accent, --hub-modal-accent-subtle, --hub-modal-accent-border, --hub-modal-accent-bar-width and --hub-modal-title-color. No visual change for a neutral modal.
changed: BREAKING: the uniform --hub-modal-close-padding and --hub-modal-title-margin shorthands were replaced by the canonical directional -x / -y tokens. Set those instead. No visual change; the dialog per-side margin system is unchanged. See BREAKING_CHANGES.md.
Version 22.0.0 - 6/17/26, 12:00 AM
changed: Aligned with Angular 22, and the README documentation standardized.
Version 21.0.3 - 6/14/26, 12:00 AM
fixed: parentNode is guarded when the window and backdrop elements are removed during teardown, so an element already detached no longer raises Cannot read properties of null (reading 'removeChild').
Version 21.0.2 - 3/31/26, 12:00 AM
changed: Standardized padding variables and improved fullscreen layout responsiveness.
Version 21.0.1 - 3/19/26, 12:00 AM
changed: Removed hardcoded design system token defaults from stylesheet; all --hub-modal-* variables retain literal fallback values.
fixed: Fixed modal-backdrop and modal unit test reliability.
Version 21.0.0 - 3/10/26, 12:00 AM
added: HubModalPlacement configuration for anchored modal positioning.
changed: BREAKING: Standardized CSS class names to hub-modal BEM convention.