Angular Utility Library | ng-hub-ui-utils

Angular utility library with pipes, focus management, overlays, transitions and translation helpers shared across reusable UI components.

Last updated Sep 6, 2026

Overview

Why teams search for this library

Use this Angular utility library to avoid rewriting the low-level pieces behind overlays, focus traps, translation helpers and common template utilities.

Install

npm install ng-hub-ui-utils

Jump to

Ideal for

  • shared UI infrastructure
  • overlay systems
  • focus management
  • template helpers

About utils

ng-hub-ui-utils is the shared foundation behind many reusable UI patterns. It is useful when Angular teams want to centralize the lower-level pieces that keep overlay systems, focus management, pipes and translation helpers consistent across projects.

Feature guides

Focus Management

Trap and manage keyboard focus within elements

Examples:
Focus Trap

Keep focus contained within a modal or dialog using hubFocusTrap()

Focus Trap Demo

Click "Enable Focus Trap" and try to Tab outside the blue box. Focus will stay trapped inside.

Focus Trap Area

Status: 🔓 Free

Focusable Elements

The selector FOCUSABLE_ELEMENTS_SELECTOR finds all focusable elements.

getFocusableBoundaryElements(container)→ [first, last] elements
Focusable Elements

Query boundary focusable elements with getFocusableBoundaryElements()

Focus Trap Demo

Click "Enable Focus Trap" and try to Tab outside the blue box. Focus will stay trapped inside.

Focus Trap Area

Status: 🔓 Free

Focusable Elements

The selector FOCUSABLE_ELEMENTS_SELECTOR finds all focusable elements.

getFocusableBoundaryElements(container)→ [first, last] elements

Internationalization (i18n)

Translation services and pipes for multi-language support

Examples:
Translation Service

HubTranslationService for managing translations

TranslatePipe - Basic usage

'welcome' | translate→ Welcome to ng-hub-ui

TranslatePipe - With parameters

'greeting' | translate:{ name: 'Carlos' }→ Hello, Carlos!

TranslatePipe - Nested keys

'buttons.save' | translate→ Save
'buttons.cancel' | translate→ Cancel

Programmatic access

translationService.getTranslation('buttons.delete')→ Delete
Translate Pipe

TranslatePipe for template-based translations

TranslatePipe - Basic usage

'welcome' | translate→ Welcome to ng-hub-ui

TranslatePipe - With parameters

'greeting' | translate:{ name: 'Carlos' }→ Hello, Carlos!

TranslatePipe - Nested keys

'buttons.save' | translate→ Save
'buttons.cancel' | translate→ Cancel

Programmatic access

translationService.getTranslation('buttons.delete')→ Delete
Translation Provider

provideHubTranslations() for app configuration

TranslatePipe - Basic usage

'welcome' | translate→ Welcome to ng-hub-ui

TranslatePipe - With parameters

'greeting' | translate:{ name: 'Carlos' }→ Hello, Carlos!

TranslatePipe - Nested keys

'buttons.save' | translate→ Save
'buttons.cancel' | translate→ Cancel

Programmatic access

translationService.getTranslation('buttons.delete')→ Delete
External i18n adapter

Configure one reactive translation bridge in app.config.ts for all Hub UI libraries.

Configure one reactive translation bridge in app.config.ts for all Hub UI libraries.

Code
Import:
Template:
Component:

Overlay System

Create positioned overlays and floating elements

Examples:
Overlay Service

HubOverlayService for programmatic overlay creation

A connected overlay positioned below the trigger, with a backdrop that closes it on outside click.

Overlay Reference

OverlayRef for managing overlay lifecycle

A connected overlay positioned below the trigger, with a backdrop that closes it on outside click.

Positioning

ConnectionPositionPair for flexible positioning

A connected overlay positioned below the trigger, with a backdrop that closes it on outside click.

Popup Service

Simplified popup management

Examples:
Popup Creation

HubPopupService for quick popup creation

Nothing open.

Pipes

Utility pipes for templates

Examples:
Type Checking Pipes

IsStringPipe, IsObjectPipe, IsObservablePipe

GetPipe - Access nested properties

user | get:'profile.name'→ John Doe
user | get:'profile.address.city'→ New York

IsStringPipe - Type checking

'Hello' | isString→ true
123 | isString→ false

IsObjectPipe - Object detection

{ a: 1 } | isObject→ true
'string' | isObject→ false

UcfirstPipe - Capitalize first letter

'hello world' | ucfirst→ Hello world

UnwrapAsyncPipe - Observable or plain value, same template

status$ | unwrapAsync→ nothing emitted yet
'not an observable' | unwrapAsync→ not an observable
GetPipe

Access nested object properties with dot notation

GetPipe - Access nested properties

user | get:'profile.name'→ John Doe
user | get:'profile.address.city'→ New York

IsStringPipe - Type checking

'Hello' | isString→ true
123 | isString→ false

IsObjectPipe - Object detection

{ a: 1 } | isObject→ true
'string' | isObject→ false

UcfirstPipe - Capitalize first letter

'hello world' | ucfirst→ Hello world

UnwrapAsyncPipe - Observable or plain value, same template

status$ | unwrapAsync→ nothing emitted yet
'not an observable' | unwrapAsync→ not an observable
UcfirstPipe

Capitalize the first letter of a string

GetPipe - Access nested properties

user | get:'profile.name'→ John Doe
user | get:'profile.address.city'→ New York

IsStringPipe - Type checking

'Hello' | isString→ true
123 | isString→ false

IsObjectPipe - Object detection

{ a: 1 } | isObject→ true
'string' | isObject→ false

UcfirstPipe - Capitalize first letter

'hello world' | ucfirst→ Hello world

UnwrapAsyncPipe - Observable or plain value, same template

status$ | unwrapAsync→ nothing emitted yet
'not an observable' | unwrapAsync→ not an observable
UnwrapAsyncPipe

Unwrap observables and promises in templates

GetPipe - Access nested properties

user | get:'profile.name'→ John Doe
user | get:'profile.address.city'→ New York

IsStringPipe - Type checking

'Hello' | isString→ true
123 | isString→ false

IsObjectPipe - Object detection

{ a: 1 } | isObject→ true
'string' | isObject→ false

UcfirstPipe - Capitalize first letter

'hello world' | ucfirst→ Hello world

UnwrapAsyncPipe - Observable or plain value, same template

status$ | unwrapAsync→ nothing emitted yet
'not an observable' | unwrapAsync→ not an observable

Utility Functions

General-purpose helper functions

Examples:
Type Guards

isString(), isNumber(), isDefined(), isPromise()

Type Guards

isString('hello')→ true
isNumber(42)→ true
isDefined(null)→ false
isDefined('value')→ true

Deep Equality

equals({a:1}, {a:1})→ true
equals([1,2], [1,2])→ true

Object Access

getValue(user, 'profile.name')→ John Doe

String Interpolation

interpolateString('Hello {{name}}', {name: 'World'})→ Hello World

String Utilities

removeAccents('Ñoño café')→ Nono cafe
padNumber(5)→ 05
String Utilities

removeAccents(), interpolateString(), regExpEscape()

Type Guards

isString('hello')→ true
isNumber(42)→ true
isDefined(null)→ false
isDefined('value')→ true

Deep Equality

equals({a:1}, {a:1})→ true
equals([1,2], [1,2])→ true

Object Access

getValue(user, 'profile.name')→ John Doe

String Interpolation

interpolateString('Hello {{name}}', {name: 'World'})→ Hello World

String Utilities

removeAccents('Ñoño café')→ Nono cafe
padNumber(5)→ 05
Object Utilities

equals() for deep comparison, getValue() for dot notation

Type Guards

isString('hello')→ true
isNumber(42)→ true
isDefined(null)→ false
isDefined('value')→ true

Deep Equality

equals({a:1}, {a:1})→ true
equals([1,2], [1,2])→ true

Object Access

getValue(user, 'profile.name')→ John Doe

String Interpolation

interpolateString('Hello {{name}}', {name: 'World'})→ Hello World

String Utilities

removeAccents('Ñoño café')→ Nono cafe
padNumber(5)→ 05
DOM Utilities

closest(), reflow(), getActiveElement()

closest()

Click anywhere inside; the helper walks up to the nearest panel.

outer
inner

closest(target, '[data-panel]') → —

reflow()

Replaying an animation means removing the class and adding it back. Without a forced reflow the browser coalesces both into no change at all.

replay me
getActiveElement()

Focus either control. The second one lives inside a shadow root.

document.activeElement → —

getActiveElement() → —

runInZone()

Both streams are created with runOutsideAngular; only one is piped through the operator.

plain subscriberisInAngularZone() → —
piped through runInZone(zone)isInAngularZone() → —
ticks0
RxJS Utilities

runInZone() operator for NgZone integration

closest()

Click anywhere inside; the helper walks up to the nearest panel.

outer
inner

closest(target, '[data-panel]') → —

reflow()

Replaying an animation means removing the class and adding it back. Without a forced reflow the browser coalesces both into no change at all.

replay me
getActiveElement()

Focus either control. The second one lives inside a shadow root.

document.activeElement → —

getActiveElement() → —

runInZone()

Both streams are created with runOutsideAngular; only one is piped through the operator.

plain subscriberisInAngularZone() → —
piped through runInZone(zone)isInAngularZone() → —
ticks0

Scrollbar Utilities

Measure and compensate for scrollbar width

Examples:
Scrollbar Width

scrollbarWidth() and scrollbarPadding() functions

ScrollBar Service

The ScrollBar service helps manage scrollbar visibility and compensate for layout shifts when hiding scrollbars (e.g., when opening modals).

Use Case: Modal Body Lock

When opening a modal, you typically hide the body scrollbar. The ScrollBar.hide() method handles this automatically and returns a reverter function.

Status: 🔓 Normal scrolling

Code Example

import { inject } from '@angular/core';
import { ScrollBar } from 'ng-hub-ui-utils';

export class ModalService {
  private scrollBar = inject(ScrollBar);
  private revertScrollbar: (() => void) | null = null;

  openModal() {
    // Hide scrollbar and get reverter function
    this.revertScrollbar = this.scrollBar.hide();
  }

  closeModal() {
    // Restore scrollbar
    if (this.revertScrollbar) {
      this.revertScrollbar();
      this.revertScrollbar = null;
    }
  }
}

Transitions

CSS transition helpers

Examples:
Run Transition

hubRunTransition() for programmatic CSS transitions

transition: height 600ms · measured at 0 ms
  1. Run a transition to see when the observable completes.

Tooltip

Lightweight, themeable tooltips for any element via the [hubTooltip] directive.

Examples:
Tooltip Directive

Apply [hubTooltip] with hubTooltipPlacement to show a positioned label on hover or focus, themed with --hub-tooltip-* variables. The older [tooltip] still works but is deprecated: its bare input names collide with other directives on the same element.

Placements (hover the buttons)
Themed with --hub-tooltip-* variables

Colour

Parse, measure and convert CSS colours without a DOM

Examples:
Colour parsing

parseColor(), toHex() and isValidColor() resolve hex, rgb(), hsl(), oklch(), oklab() and the 148 named colours — server-side rendering included

Try one:

#0b6eff
parsed
parseColor(){ r: 11, g: 110, b: 255, a: 1 }
toHex()#0b6eff
rgbToOklch(){ l: 0.58, c: 0.23, h: 260 }
isValidColor()true
toRgb()already parsed — same object back
HUB_NAMED_COLORSnot a bareword — 148 in the table
Contrast and readability

contrastRatio() (WCAG 2), contrastAPCA() and readableOn(), which picks the ink that matches the --hub-sys-color-*-on token

readableOn(accent, metric)
Agrees with the design-system token on all nine
primary
#0d6efd

ink#ffffff
WCAG4.5:1
APCA-75.8
secondary
#6c757d

ink#ffffff
WCAG4.69:1
APCA-78
success
#198754

ink#ffffff
WCAG4.53:1
APCA-76.5
danger
#dc3545

ink#ffffff
WCAG4.53:1
APCA-75.4
warning
#ffc107

ink#000000
WCAG12.88:1
APCA76
info
#0dcaf0

ink#000000
WCAG10.72:1
APCA66.9
neutral
#6c757d

ink#ffffff
WCAG4.69:1
APCA-78
light
#f8f9fa

ink#000000
WCAG19.92:1
APCA102.4
dark
#212529

ink#ffffff
WCAG15.43:1
APCA-105

A highlighted border marks a chip whose ink differs from what --hub-sys-color-*-on paints in CSS.


Translucent ink has to be composited before it is measured

relativeLuminance() ignores alpha, because a translucent colour has no luminance of its own until something is behind it. Measure the ink as written and you are scoring solid black; compositeOver() blends it onto the surface first, which is what the eye is reading.

Sample text
relativeLuminance(surface)0.1833
contrastRatio(ink, surface)4.67:1
…with compositeOver(ink, surface)2.75:1
OKLCh conversions

rgbToOklch(), oklchToRgb(), maxSrgbChroma() and clampToSrgbGamut() for palette work in the space the design system mixes in

red 21°
0.232
amber 85°
0.118
green 157°
0.138
cyan 218°
0.104
blue 260°
0.232
purple 320°
0.277

clampToSrgbGamut()

Asking for chroma 0.35 on the amber hue at the current lightness.

clipped per channel#d24900
chroma reduced#9a7300
in gamut as asked: false
chroma kept: 0.118

Key features

Recent changes

Version 22.12.1 - 9/6/26, 12:00 AM

changed: Every row of FUNCTIONALITIES.md now points at a runnable demo. Fifteen features (the popup service, the transition helpers, UnwrapAsyncPipe, the DOM and RxJS helpers and five of the colour functions) were listed as having no example, which left the unit tests as the only executable use of them. Each of those now has a demo on the documentation site, so the table reports coverage instead of a wishlist. No API changed.

fixed: The tooltip and overlay stylesheets resolve at the path the documentation gives. The manifest declared no exports map, so ng-packagr synthesised one for the published package, and a synthesised map lists only "." and "./package.json". A package that declares exports closes every subpath outside that map, so @use "ng-hub-ui-utils/styles/tooltip"; resolved to nothing even though the sheet shipped in styles/. Both sheets are now named in the manifest, extensionless and with the .scss suffix, the way every sibling package in the family already does, and ng-packagr merges those entries into the map it generates instead of replacing them.

fixed: The tooltip is announced to assistive technology and can be dismissed without a mouse. The bubble was a bare span with no id and no role, and the host was never pointed at it, so on an icon-only button, the case the tooltip is written for, a screen reader had nothing to read and the only workaround was an aria-label repeating the same text. The bubble is now role="tooltip" with an id the host is aria-describedby while it is on screen, and the attribute is put back exactly as it was found, so a description a consumer wrote is neither replaced nor left behind. The same change closes WCAG 1.4.13 for it: Escape dismisses the label without moving the pointer or the focus, and the label waits out a short grace period and stays put once the pointer lands on it, which is the only way to read one longer than its box. styles/tooltip.scss therefore ships pointer-events: auto instead of none, and the controller disables them again the moment it starts fading, so an invisible bubble never catches a click meant for what is under it. It is reached through HubTooltipController, so it arrives at all four entry points at once: [hubTooltip], the deprecated [tooltip], [hubOverflowTooltip] and hubTooltipAdapter.

Version 22.12.0 - 9/3/26, 12:00 AM

added: Colour utilities — parseColor(), toHex(), isValidColor(), relativeLuminance(), contrastRatio(), contrastAPCA(), readableOn(), compositeOver() and the OKLCh helpers rgbToOklch(), oklchToRgb(), maxSrgbChroma(), isInSrgbGamut() and clampToSrgbGamut(). The parser resolves hex (3/4/6/8 digits), rgb(), hsl(), oklch(), oklab() and the 148 CSS named colours in both modern and legacy syntax, with no DOM involved, so it runs under server-side rendering. It returns null rather than throwing on anything it cannot resolve.

added: readableOn() picks black or white by OKLCh perceptual lightness, the same decision the --hub-sys-color-*-on token computes in CSS, so a component that resolves its ink in TypeScript cannot disagree with the stylesheet. The alternative metrics are available: maximising the WCAG 2 ratio puts black text on the design system's own blue, green and red accents, which is why it is not the default.

Version 22.11.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, so the link a registry shows beside the package lands on the reference for the package the reader was already looking at. Metadata only.

Version 22.11.0 - 8/26/26, 12:00 AM

added: The overlay follows its origin. While attached it listens for scroll and resize in the capture phase and recomputes its position, coalesced into an animation frame. Before this it computed coordinates once and never again: a panel opened and then scrolled sat 122px away from the field it belonged to.

added: start and end are logical. They resolved to left and right whatever the direction, so an overlay opened from a field inside an RTL container hung off the wrong edge. The direction is read from the origin element; OverlayPosition.withDirection() overrides it.

added: OverlayRef.onKeydown(), and with it a document-level dispatcher that tells only the topmost open overlay. An overlay rarely holds focus, so a component listening on its own host never heard Escape and the panel that took over the screen could not be dismissed with the key everyone reaches for.

added: HUB_DROPDOWN_POSITIONS — the four-position fallback chain a dropdown wants, below the origin and flipping above when there is no room, expressed logically so one list serves both text directions.

fixed: Tearing an overlay down twice no longer throws. dispose() and detach() called removeChild on nodes something else may already have removed — a test teardown, a router navigation — and the DOMException took the whole destroy path with it.

Version 22.10.0 - 8/22/26, 12:00 AM

added: --hub-tooltip-white-space and --hub-tooltip-text-align, so how a label breaks and sits can be asked for per tooltip instead of through a global rule that changes every tooltip in the product. Both default to what was hard-coded, so nothing moves for anyone who says nothing.

Version 22.9.3 - 8/19/26, 12:00 AM

fixed: A tooltip whose stylesheet was never imported no longer moves the page. The element now takes position: absolute inline at creation — the same value the sheet ships — so it stops landing in normal flow at the end of the document and growing the page a scrollbar that appeared and vanished as the pointer crossed a label.

Version 22.9.2 - 8/17/26, 12:00 AM

fixed: The stylesheets are published under styles/, so @use 'ng-hub-ui-utils/styles/tooltip' names a real path instead of reaching through the package's internal folder layout.

Version 22.9.1 - 8/17/26, 12:00 AM

fixed: The published package declares its licence. An absent license field is not neutral — a registry reports it as unlicensed, which legally reads as all rights reserved. The intent was always MIT.

Version 22.9.0 - 8/17/26, 12:00 AM

added: [hubTooltip], a tooltip directive that can share an element. Its inputs are hubTooltip, hubTooltipPlacement, hubTooltipDelay and hubTooltipOffset: an attribute named for its owner cannot be claimed by anyone else, which is what the bare names could not promise next to [hubDropdown] (its own placement is typed over eight values) or <hub-badge> (which declares a tooltip input and drew two).

deprecated: TooltipDirective / [tooltip]. Kept working, unchanged — both directives are thin shells over the same HubTooltipController. Migration is attribute for attribute: tooltip → hubTooltip, placement → hubTooltipPlacement, delay → hubTooltipDelay, offset → hubTooltipOffset.

Version 22.8.1 - 8/15/26, 12:00 AM

fixed: A content-sized overlay no longer clips its own content into invisibility. Created with no intrinsic size, it computed to a 0×0 box whenever its content was absolutely positioned — which is exactly what a connected-position dropdown is — and the stylesheet's overflow: auto then hid what the overlay existed to display. An overlay created without an explicit width or height now opts out of clipping.

Version 22.8.0 - 8/14/26, 12:00 AM

added: provideHubTranslationAdapter() — the application-wide reactive bridge from an external translation service (transloco, ngx-translate, i18next…) into HubTranslationService. Register it once at bootstrap and every ng-hub-ui library picks up the host dictionary, re-emitting on every language change. Supports optional namespacing and deliberate per-label reactive overrides.

added: HUB_TRANSLATION_PREFIX — injection token that scopes the lookups of a library to a collision-safe HUBUI.<LIBRARY>.* namespace. TranslatePipe resolves the prefixed key first and falls back to the bare key, so existing flat dictionaries keep working untouched.

Version 22.7.2 - 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.7.1 - 7/27/26, 12:00 AM

fixed: --hub-overlay-zindex / --hub-overlay-backdrop-zindex actually work now: OverlayRef resolves its inline z-index through var(--hub-overlay-zindex, 1000) / var(--hub-overlay-backdrop-zindex, 999) instead of literal values, so re-stacking an overlay no longer requires !important. Defaults are unchanged.

added: OverlayConfig.zIndex — optional explicit layer for a single overlay instance; when set it takes precedence over the token.

Version 22.7.0 - 7/7/26, 12:00 AM

added: resolveHubAccent(value) — the canonical "any colour" accent resolver shared across the ng-hub-ui family: barewords map to var(--hub-sys-color-<name>, <name>), literal #hex / rgb() / oklch() / var() values pass through unchanged, empty values yield null.

Version 22.6.1 - 7/2/26, 12:00 AM

fixed: CSS variable fallbacks realigned to the ds light defaults (e.g. --hub-ref-font-family-base falls back to the system-ui stack instead of inherit); fallbacks only apply when ng-hub-ui-ds is not loaded.

Version 22.6.0 - 6/30/26, 12:00 AM

added: HubOverflowTooltipDirective ([hubOverflowTooltip]) — shows a tooltip only while the host label is actually truncated, with live tracking via ResizeObserver + MutationObserver.

added: Agnostic tooltip token — HUB_TOOLTIP_ADAPTER plus provideHubTooltip(adapter) let any tooltip implementation back [hubOverflowTooltip], app-wide or per subtree; defaults to the built-in hubTooltipAdapter.