Modal
The <smart-modal> component provides a dialog overlay with title, description, an arbitrary projected body, and a list of action buttons. It follows the Base + Standard + Wrapper pattern with an InjectionToken-based extension mechanism. The abstract ModalBaseComponent defines the shared API — two-way open (ModelSignal<boolean>), optional title and description, actions (InputSignal<IModalAction[]>), optional IModalOptions, cssClass (alias class), actionClick and closed outputs, and invokeAction(actionId) / close() methods. ModalStandardComponent is a barebones placeholder concrete implementation. ModalComponent is the public wrapper that renders ModalStandardComponent by default and accepts a custom replacement via MODAL_STANDARD_COMPONENT_TOKEN.
Usage
<button type="button" class="docs-modal-trigger" (click)="open.set(true)">
Deactivate account
</button>
<smart-modal
[(open)]="open"
title="Deactivate account"
description="Once the account is deactivated all of its data will be permanently removed."
[actions]="actions"
[options]="options"
(actionClick)="onActionClick($event)"
(closed)="onClosed()"
/>
Components
ModalComponent (<smart-modal>)
Main wrapper component. Renders ModalStandardComponent by default. When MODAL_STANDARD_COMPONENT_TOKEN is provided, renders the injected component via NgComponentOutlet.
ModalStandardComponent (<smart-modal-standard>)
Barebones placeholder concrete implementation. Renders a native <dialog [open] role="dialog" aria-modal="true"> containing an optional <h2 id="smart-modal-title">, an optional <p> description, an <ng-content> slot for arbitrary body content, an optional dismiss <button> (when options.withDismiss is true), and a <footer> with one <button data-variant> per action. It does not include Tailwind UI styling — it exists solely as the default structural placeholder until a custom implementation is registered through the token.
ModalPresetComponent (<smart-modal-preset>)
Styled variation that extends ModalBaseComponent and is a drop-in replacement for ModalStandardComponent. Register it via MODAL_STANDARD_COMPONENT_TOKEN to restyle every <smart-modal>, or use the <smart-modal-preset> selector directly. It renders a translated Preline overlay — a full-screen scrollable backdrop hosting a rounded-xl card with a header (title + optional dismiss ×), a body (description + projected content), and a footer of action buttons. The look is selected through options.variant (default 'centered'): centered/alert are vertically centered (alert is narrower), wide is a top-aligned large dialog, and left-aligned-buttons left-aligns the footer. options.footerStyle: 'gray' tints the footer, and action buttons are styled by their variant (primary/secondary/danger). All classes are smart:-prefixed Tailwind with explicit dark: variants. The class recipes live in preset/preset-classes.util.ts.
Preline JS is NOT used. Open/close is driven entirely by the existing
openmodel signal +@if(noHSOverlay, nodata-hs-*). The component binds its own dismiss UX: clicking the backdrop and pressingEscapeboth callclose()(which setsopentofalseand emitsclosed). The dialog ARIA contract is preserved (role="dialog",aria-modal="true",aria-labelledbywhentitleis set, elsearia-labelfromoptions.ariaLabel). The header dismiss×button is only shown whenoptions.withDismissistrue.Because
ModalComponentrenders injected components viaNgComponentOutlet(which passes inputs by canonical name),ModalPresetComponentoverridescssClassasinput<string>('')without theclassalias (applied to the centering wrapper). Bind it as[cssClass]when using the<smart-modal-preset>selector directly, or just passclasson<smart-modal>(the wrapper forwards it).
ModalBaseComponent (abstract)
Abstract base directive for extending custom modal implementations. Exposes open as a two-way ModelSignal<boolean>, title / description as optional InputSignal<string | undefined>, actions as InputSignal<IModalAction[]> (default []), options as InputSignal<IModalOptions | undefined>, cssClass as InputSignal<string> (with alias class), actionClick (payload IModalActionClick, exported from @smartsoft001/angular) and closed outputs, plus invokeAction(actionId) (emits actionClick) and close() (sets open to false and emits closed).
API
Inputs
| Input | Type | Default | Description |
|---|---|---|---|
open | ModelSignal<boolean> | false | Visibility state (two-way bindable) |
title | InputSignal<string | undefined> | - | Dialog heading (also sets aria-labelledby) |
description | InputSignal<string | undefined> | - | Optional secondary description text |
actions | InputSignal<IModalAction[]> | [] | Action buttons rendered in the footer |
options | InputSignal<IModalOptions | undefined> | - | Optional configuration |
class | InputSignal<string> | '' | External CSS classes (alias for cssClass) |
Outputs
| Output | Payload | Description |
|---|---|---|
actionClick | IModalActionClick ({ actionId: string }) | Fired when a footer action is clicked |
closed | void | Fired when the dialog is dismissed/closed |
IModalOptions
The default ModalStandardComponent consumes withDismiss (renders a dismiss × button) and ariaLabel (used as aria-label when title is not provided). The variant and footerStyle selectors are reserved for custom implementations registered through MODAL_STANDARD_COMPONENT_TOKEN.
MODAL_STANDARD_COMPONENT_TOKEN
InjectionToken that allows replacing the default ModalStandardComponent with a custom implementation. Provide a Type<ModalBaseComponent> to override.
Content projection
ModalStandardComponent accepts arbitrary body content via <ng-content> and the wrapper forwards its own <ng-content> into the standard. However, NgComponentOutlet does not propagate projected content — when a custom component is registered via the token, the wrapper renders the custom component as a sibling and the consumer's <ng-content> is dropped. Custom implementations should rely on inputs (title, description, structured data extension) or expose their own template inputs rather than <ng-content>.
Extending the base class
import {
ChangeDetectionStrategy,
Component,
computed,
input,
ViewEncapsulation,
} from '@angular/core';
import {
IModalAction,
IModalOptions,
MODAL_STANDARD_COMPONENT_TOKEN,
ModalBaseComponent,
ModalComponent,
} from '@smartsoft001/angular';
/**
* A custom modal built on `ModalBaseComponent`.
*
* The base contributes the `open` model plus the `invokeAction(id)` and
* `close()` helpers, which already emit `actionClick` and `closed`. The base
* binds no keyboard or backdrop listeners, so the implementation wires the
* backdrop click itself.
*/
@Component({
selector: 'docs-custom-modal',
template: `
@if (open()) {
<div class="docs-modal__backdrop" (click)="close()"></div>
<div
role="dialog"
aria-modal="true"
[attr.aria-label]="options()?.ariaLabel ?? title()"
[class]="containerClasses()"
>
@if (options()?.withDismiss) {
<button
type="button"
class="docs-modal__dismiss"
aria-label="Close"
(click)="close()"
>
×
</button>
}
@if (title()) {
<h2 class="docs-modal__title">{{ title() }}</h2>
}
@if (description()) {
<p class="docs-modal__description">{{ description() }}</p>
}
<!--
smart-modal renders a custom implementation through NgComponentOutlet,
which does not forward projected content. Only open, title,
description, actions, options and cssClass arrive here, so a custom
modal owns its body instead of relying on ng-content.
-->
<footer [class]="footerClasses()">
@for (action of actions(); track action.id) {
<button
type="button"
class="docs-modal__action"
[attr.data-variant]="action.variant ?? 'primary'"
(click)="invokeAction(action.id)"
>
{{ action.label }}
</button>
}
</footer>
</div>
}
`,
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomModalComponent extends ModalBaseComponent {
// NgComponentOutlet passes 'cssClass' by canonical name, not the 'class' alias.
override cssClass = input<string>('');
containerClasses = computed(() => {
const classes = ['docs-modal'];
const variant = this.options()?.variant;
if (variant) classes.push(`docs-modal--${variant}`);
const extra = this.cssClass();
if (extra) classes.push(extra);
return classes.join(' ');
});
footerClasses = computed(() =>
[
'docs-modal__footer',
`docs-modal__footer--${this.options()?.footerStyle ?? 'default'}`,
].join(' '),
);
}
/**
* Registering the implementation against `MODAL_STANDARD_COMPONENT_TOKEN`
* makes every `<smart-modal>` in this injector render it instead of the
* standard variation.
*/
@Component({
selector: 'docs-modal-custom-example',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ModalComponent],
providers: [
{ provide: MODAL_STANDARD_COMPONENT_TOKEN, useValue: CustomModalComponent },
],
template: `
<smart-modal
[open]="true"
title="Deactivate account"
description="Once the account is deactivated all of its data will be permanently removed."
[actions]="actions"
[options]="options"
/>
`,
})
export class ModalCustomExampleComponent {
actions: IModalAction[] = [
{ id: 'cancel', label: 'Cancel', variant: 'secondary' },
{ id: 'deactivate', label: 'Deactivate', variant: 'danger' },
];
options: IModalOptions = {
variant: 'centered',
footerStyle: 'gray',
withDismiss: true,
};
}
Source
The component lives in packages/shared/angular/src/lib/components/modal and is documented for Claude Code by the angular-components-modal skill.