prettier-modals-angular
v0.2.1
Published
Angular directives and service for Prettier Modals — beautiful <dialog> animations powered by GSAP Flip.
Downloads
526
Maintainers
Readme
prettier-modals-angular
Angular directives and an injectable service for Prettier Modals — beautiful open/close animations for native <dialog> elements, powered by GSAP Flip.
Standalone (no NgModule), SSR-safe, and runs animations outside the Angular zone.
Installation
npm install prettier-modals-angular prettier-modals gsapprettier-modals, gsap, and @angular/core/@angular/common are peer dependencies (Angular >=17).
Register the GSAP plugins once, e.g. in main.ts:
import gsap from 'gsap'
import { Flip } from 'gsap/Flip'
import { CustomEase } from 'gsap/CustomEase'
gsap.registerPlugin(Flip, CustomEase)Declarative usage (directives)
Import the directives you need — or PRETTY_MODAL_DIRECTIVES for all of them — into a standalone component:
import { Component } from '@angular/core'
import { PRETTY_MODAL_DIRECTIVES } from 'prettier-modals-angular'
@Component({
selector: 'app-settings',
standalone: true,
imports: [PRETTY_MODAL_DIRECTIVES],
template: `
<button [prettyModalTrigger]="'settings'" anchor="origin">Open</button>
<dialog id="settings" prettyModal anchor="origin"
(opened)="onOpen()" (closed)="onClose()">
<h1>Settings</h1>
<button prettyModalClose>Close</button>
</dialog>
`,
})
export class SettingsComponent {
onOpen() {}
onClose() {}
}No ViewChild, no NgZone boilerplate — the directives wire everything to the core.
Directives
| Directive | Selector | Description |
|---|---|---|
| PrettyModalDirective | dialog[prettyModal] | Marks the dialog (needs an id). Inputs: anchor, animateCancel. Outputs: (opened), (closed). |
| PrettyModalTriggerDirective | [prettyModalTrigger] | Opens the dialog whose id/element it's bound to, morphing from the host. Inputs: anchor, duration, openDuration, scale, originGap, respectReducedMotion. |
| PrettyModalCloseDirective | [prettyModalClose] | Closes the nearest ancestor <dialog> (or a given id/element). Inputs: duration, closeDuration. |
animateCancel (default true) intercepts the native Escape key so it closes with the animation instead of instantly.
Any input left unset falls back to the core instance defaults, so you only override what you need:
<button [prettyModalTrigger]="'menu'" anchor="origin" [originGap]="8" [openDuration]="0.6">Menu</button>Global defaults (providePrettyModal)
Some core options — ease and originEase (the CustomEase paths) — are compiled once when the core is built, so they're configured at the app level rather than per call. Use providePrettyModal to set them (and any other defaults) for the whole app:
import { bootstrapApplication } from '@angular/platform-browser'
import { providePrettyModal } from 'prettier-modals-angular'
bootstrapApplication(AppComponent, {
providers: [
providePrettyModal({
anchor: 'origin',
originGap: 8,
ease: 'M0,0 C0.25,0.1 0.25,1 1,1',
originEase: 'M0,0 C0.34,1.56 0.64,1 1,1',
}),
],
})The config accepts anchor, duration, openDuration, closeDuration, ease, originEase, scale, originGap and respectReducedMotion. Per-call options (on the directives or service) still override these defaults — except ease/originEase, which are only configurable here.
Trigger and dialog in separate components
The trigger and the <dialog> don't have to live in the same component. They're linked only by the dialog's id: the PrettyModalService is a root singleton shared by the whole app, and the core resolves the dialog through document.getElementById. So you can wrap each part in its own reusable component:
// button.component.ts — knows only the target dialog id
import { Component, Input } from '@angular/core'
import { PrettyModalTriggerDirective, type PrettyModalAnchor } from 'prettier-modals-angular'
@Component({
selector: 'app-modal-button',
standalone: true,
imports: [PrettyModalTriggerDirective],
template: `<button [prettyModalTrigger]="target" [anchor]="anchor"><ng-content /></button>`,
})
export class ModalButtonComponent {
@Input({ required: true }) target!: string
@Input() anchor?: PrettyModalAnchor
}// modal.component.ts — owns the dialog, declared independently
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { PrettyModalDirective, PrettyModalCloseDirective, type PrettyModalAnchor } from 'prettier-modals-angular'
@Component({
selector: 'app-modal',
standalone: true,
imports: [PrettyModalDirective, PrettyModalCloseDirective],
template: `
<dialog [id]="modalId" prettyModal [anchor]="anchor"
(opened)="opened.emit($event)" (closed)="closed.emit($event)">
<button prettyModalClose>Close</button>
<ng-content />
</dialog>
`,
})
export class ModalComponent {
// Don't name this input `id`: a native `id` would also be reflected onto the
// host <app-modal>, duplicating the id and breaking document.getElementById.
@Input({ required: true }) modalId!: string
@Input() anchor?: PrettyModalAnchor
@Output() opened = new EventEmitter<HTMLDialogElement>()
@Output() closed = new EventEmitter<HTMLDialogElement>()
}A parent just drops both in and matches the id:
<app-modal-button target="settings" anchor="origin">Open</app-modal-button>
<app-modal modalId="settings" anchor="origin">
<h1>Settings</h1>
</app-modal>Two things to keep in mind:
- The dialog
idmust be unique in the document — Angular's view encapsulation scopes CSS, notidattributes. Avoid exposing a wrapper input literally namedid: Angular reflects it onto the host element too, so it ends up duplicated (usemodalIdor similar, as above). - The
<dialog>must be rendered in the DOM when the trigger fires. Don't strip it with@if/*ngIf; it stays hidden until opened anyway.
Imperative usage (service)
import { Component, inject } from '@angular/core'
import { PrettyModalService } from 'prettier-modals-angular'
@Component({ /* … */ })
export class MyComponent {
private readonly modal = inject(PrettyModalService)
open(trigger: HTMLElement) {
this.modal.open('settings', { trigger, anchor: 'origin' })
}
close() {
this.modal.close('settings')
}
}PrettyModalService lazily creates the core in the browser only (SSR-safe) and runs every animation outside the Angular zone.
Updating
This wrapper and the vanilla core (prettier-modals) are two separate packages with independent versions. Update both so the wrapper runs against the matching core:
npm outdated prettier-modals-angular prettier-modals # any newer versions?
npm update prettier-modals-angular prettier-modals gsap # update within your "^" rangesFor a major jump, install the latest explicitly:
npm install prettier-modals-angular@latest prettier-modals@latestGet notified of new versions: every release is published as a GitHub Release. Click Watch → Custom → Releases on the repo to be notified whenever a new version ships, and check each release's notes for what changed.
License
MIT © Antonio Monreal Diaz
