Announcement — Countdown Bar
blockmarketingTop strip counting down to a deadline in days, hours, minutes and seconds, with the offer line, an inline CTA, and a dismiss control that keeps it closed for the session.
Installation
$pnpm dlx uipkge-ng@latest add announcement-countdown-bar$npx uipkge-ng@latest add announcement-countdown-bar$yarn dlx uipkge-ng@latest add announcement-countdown-bar$bunx uipkge-ng@latest add announcement-countdown-barManifest:
https://uipkge.dev/r/angular/announcement-countdown-bar.jsonInstalls to:src/app/components/blocks/announcement-countdown-bar/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
CountdownPartinterface CountdownPart {
label: string
value: number
}npm dependencies
Files installed (2)
src/app/components/blocks/announcement-countdown-bar/announcement-countdown-bar.component.ts5.2 kB
// Top strip counting down to `deadline` in days, hours, minutes and seconds, with // the offer line, an inline CTA, and a dismiss control that keeps it closed for the // session. Port of the Vue/React block 1:1. The digits only appear once the timer is // running (the server has no clock the client agrees with); tabular-nums plus // fixed-width cells keep the bar from reflowing each second. import { ChangeDetectionStrategy, Component, DestroyRef, Input, OnInit, afterNextRender, computed, inject, signal, } from '@angular/core' import { ArrowRight, LucideAngularModule, Timer, X } from 'lucide-angular' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' export interface CountdownPart { label: string value: number } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-announcement-countdown-bar, [ui-announcement-countdown-bar]', standalone: true, // The block renders its own root <div>: keep this wrapper out of the layout. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiSeparatorComponent], template: ` @if (!dismissed()) { <div data-slot="announcement-countdown-bar" class="border-border bg-muted/60 border-b backdrop-blur"> <div class="mx-auto flex min-h-11 max-w-6xl flex-wrap items-center gap-x-4 gap-y-2 px-6 py-2 text-sm"> <span ui-badge variant="secondary" class="shrink-0 gap-1.5"> <lucide-icon [img]="Timer" class="size-3" aria-hidden="true" /> Ends soon </span> <p class="min-w-0"> <span class="font-medium">Annual plans are 20% off</span> <span class="text-muted-foreground"> for teams that start before the quarter closes.</span> </p> <!-- tabular-nums + fixed-width cells keep the bar from reflowing each second. --> @if (remaining() !== null) { <div class="text-muted-foreground flex items-center gap-1 font-mono text-xs"> @for (part of parts(); track part.label) { <span class="tabular-nums"> <span class="text-foreground inline-block min-w-[2ch] text-right font-semibold">{{ pad2(part.value) }}</span >{{ part.label }} </span> } </div> } <div class="ml-auto flex shrink-0 items-center gap-1"> <button ui-button variant="ghost" size="sm" class="h-7"> Claim it <lucide-icon [img]="ArrowRight" [class]="'ml-1 size-3.5'" aria-hidden="true" /> </button> <ui-separator orientation="vertical" class="hidden h-5 sm:block" /> <button ui-button variant="ghost" size="icon" class="size-7" aria-label="Dismiss announcement" (click)="dismiss()" > <lucide-icon [img]="X" class="size-3.5" aria-hidden="true" /> </button> </div> </div> </div> } `, }) export class UiAnnouncementCountdownBarComponent implements OnInit { /** ISO timestamp the countdown runs to. */ @Input() deadline = '' /** sessionStorage key holding the dismissal. */ @Input() storageKey = 'uipkge:countdown-dismissed' // Null until the browser timer starts: the server has no clock the client agrees with. readonly remaining = signal<number | null>(null) readonly dismissed = signal(false) readonly targetMs = signal(0) protected readonly Timer = Timer protected readonly X = X protected readonly ArrowRight = ArrowRight private timer: ReturnType<typeof setInterval> | undefined constructor() { inject(DestroyRef).onDestroy(() => clearInterval(this.timer)) // Browser only, after the first render (the Vue onMounted / React useEffect). afterNextRender(() => { try { this.dismissed.set(window.sessionStorage.getItem(this.storageKey) === '1') } catch { this.dismissed.set(false) } this.tick() this.timer = setInterval(() => this.tick(), 1000) }) } ngOnInit(): void { const parsed = this.deadline ? Date.parse(this.deadline) : Number.NaN // Fall back to 72 hours out so the demo and a mis-set prop still read sensibly. this.targetMs.set(Number.isNaN(parsed) ? Date.now() + 72 * 60 * 60 * 1000 : parsed) } readonly parts = computed<CountdownPart[]>(() => { const total = Math.floor(Math.max(this.remaining() ?? 0, 0) / 1000) return [ { label: 'd', value: Math.floor(total / 86400) }, { label: 'h', value: Math.floor((total % 86400) / 3600) }, { label: 'm', value: Math.floor((total % 3600) / 60) }, { label: 's', value: total % 60 }, ] }) tick(): void { this.remaining.set(this.targetMs() - Date.now()) } pad2(n: number): string { return String(n).padStart(2, '0') } dismiss(): void { this.dismissed.set(true) try { window.sessionStorage.setItem(this.storageKey, '1') } catch { // Blocked storage: the dismissal still holds for this page view. } } }src/app/components/blocks/announcement-countdown-bar/index.ts0.1 kB
export { UiAnnouncementCountdownBarComponent, type CountdownPart } from './announcement-countdown-bar.component'
Raw manifest:https://uipkge.dev/r/angular/announcement-countdown-bar.json