Angular 模态框组件 | ng-hub-ui-modal

无障碍的 Angular 模态框组件,支持堆叠、全屏模式、模板、键盘处理和 CSS 变量,适用于 standalone 应用。

最后更新 2026年9月5日

概览

团队为何搜索这个库

这个 Angular 模态框组件专为需要在 standalone Angular 应用中实现无障碍对话框、可组合内容、服务驱动控制以及对设计系统友好样式的团队而打造。

安装

npm install ng-hub-ui-modal

跳转到

适用于

  • 确认对话框
  • overlay 中的表单
  • 详情面板
  • 堆叠工作流

关于 modal

ng-hub-ui-modal 解决了 Angular 产品中最常见的 UI 需求之一:打开在生产环境中表现稳健的对话框。它覆盖了堆叠模态框、全屏流程、键盘处理、内容投影以及基于服务的用法,从而避免让模态框逻辑成为每个项目的难题。

功能指南

打开 modals(内容类型)

演示如何使用 TemplateRef、Component 和 String 内容打开 modals。

配置与选项

自定义 modal 的外观和行为

Modal 引用(HubModalRef)

控制并访问已打开 modals 的状态

活动 modal(HubActiveModal)

从 modal 的内容内部与其交互

堆栈管理

管理多个堆叠的 modals

核心特性

最近更改

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.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.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 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.

Version 1.2.0 - 1/7/25, 12:00 AM

added: Documentation and examples update