Cookie Consent
blocklegalConsent trio covering the whole banner-to-dialog flow: a full-width banner with inline preferences, a bottom bar with equally weighted accept, reject, and manage actions, and a granular preferences dialog with per-category purpose, retention, and switches.
Installation
$pnpm dlx uipkge-ng@latest add cookie-consent$npx uipkge-ng@latest add cookie-consent$yarn dlx uipkge-ng@latest add cookie-consent$bunx uipkge-ng@latest add cookie-consentManifest:
https://uipkge.dev/r/angular/cookie-consent.jsonInstalls to:src/app/components/blocks/cookie-consent/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.
CookieCategoryinterface CookieCategory {
id: string
label: string
purpose: string
retention: string
locked: boolean
}Files installed (4)
src/app/components/blocks/cookie-consent/cookie-consent-banner.component.ts8.3 kB
// GDPR-style cookie consent banner with two variants: a full-width bottom bar and a compact // card. Customize expands inline preferences with Necessary (always on), Analytics, and // Marketing switches. Accept, Reject, and Save dismiss with a 150ms fade. // Port of the Vue/React block 1:1. import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core' import { Cookie, LucideAngularModule } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiButtonComponent } from '@/ui/button/button.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' export type CookieConsentBannerVariant = 'bottom-bar' | 'card' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-cookie-consent-banner, [ui-cookie-consent-banner]', standalone: true, // The block renders its own root <div>: keep this wrapper out of the layout. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiButtonComponent, UiSeparatorComponent, UiSwitchComponent], template: ` @if (visible()) { <div data-slot="cookie-consent-banner" role="dialog" aria-label="Cookie consent" [class]="rootClass"> @if (variant === 'bottom-bar') { <div class="bg-card border-border w-full border-t px-4 py-4 sm:px-6"> <div class="mx-auto flex max-w-5xl flex-col gap-4"> <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <div class="flex items-start gap-3"> <span class="bg-primary/10 text-primary mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full" > <lucide-icon [img]="Cookie" class="size-4" aria-hidden="true" /> </span> <p class="text-muted-foreground max-w-xl text-sm leading-relaxed"> We use cookies to improve your experience and analyze traffic. Read our <a href="#" class="text-foreground focus-visible:ring-ring rounded-sm font-medium underline underline-offset-4 outline-none focus-visible:ring-2" >Cookie Policy</a >. </p> </div> <div class="flex shrink-0 flex-wrap items-center gap-2"> <button ui-button variant="ghost" size="sm" (click)="showPreferences.set(!showPreferences())"> {{ showPreferences() ? 'Hide preferences' : 'Customize' }} </button> <button ui-button variant="outline" size="sm" (click)="dismiss()">Reject</button> <button ui-button size="sm" (click)="dismiss()">Accept all</button> </div> </div> @if (showPreferences()) { <div class="flex flex-col gap-3"> <ui-separator /> <div class="flex items-center justify-between gap-4 py-1"> <div> <p class="text-sm font-medium">Necessary</p> <p class="text-muted-foreground text-xs">Required for the site to function.</p> </div> <ui-switch [checked]="true" disabled aria-label="Necessary cookies" /> </div> <div class="flex items-center justify-between gap-4 py-1"> <div> <p class="text-sm font-medium">Analytics</p> <p class="text-muted-foreground text-xs">Helps us understand usage patterns.</p> </div> <ui-switch [checked]="analytics()" (checkedChange)="analytics.set($event)" aria-label="Analytics cookies" /> </div> <div class="flex items-center justify-between gap-4 py-1"> <div> <p class="text-sm font-medium">Marketing</p> <p class="text-muted-foreground text-xs">Used to personalize campaigns.</p> </div> <ui-switch [checked]="marketing()" (checkedChange)="marketing.set($event)" aria-label="Marketing cookies" /> </div> <div class="mt-1 flex justify-end"> <button ui-button size="sm" (click)="dismiss()">Save preferences</button> </div> </div> } </div> </div> } @else { <div class="bg-card border-border mx-auto max-w-md rounded-xl border p-6 shadow-xs"> <div class="flex flex-col gap-4"> <div class="flex items-start gap-3"> <span class="bg-primary/10 text-primary flex size-9 shrink-0 items-center justify-center rounded-full"> <lucide-icon [img]="Cookie" class="size-4" aria-hidden="true" /> </span> <div> <h3 class="text-sm font-semibold">We value your privacy</h3> <p class="text-muted-foreground mt-1 text-sm leading-relaxed"> We use cookies to improve your experience and analyze traffic. Read our <a href="#" class="text-foreground focus-visible:ring-ring rounded-sm font-medium underline underline-offset-4 outline-none focus-visible:ring-2" >Cookie Policy</a >. </p> </div> </div> @if (showPreferences()) { <ui-separator /> <div class="flex items-center justify-between gap-4 py-1"> <div> <p class="text-sm font-medium">Necessary</p> <p class="text-muted-foreground text-xs">Required for the site to function.</p> </div> <ui-switch [checked]="true" disabled aria-label="Necessary cookies" /> </div> <div class="flex items-center justify-between gap-4 py-1"> <div> <p class="text-sm font-medium">Analytics</p> <p class="text-muted-foreground text-xs">Helps us understand usage patterns.</p> </div> <ui-switch [checked]="analytics()" (checkedChange)="analytics.set($event)" aria-label="Analytics cookies" /> </div> <div class="flex items-center justify-between gap-4 py-1"> <div> <p class="text-sm font-medium">Marketing</p> <p class="text-muted-foreground text-xs">Used to personalize campaigns.</p> </div> <ui-switch [checked]="marketing()" (checkedChange)="marketing.set($event)" aria-label="Marketing cookies" /> </div> } <div class="flex flex-wrap items-center gap-2"> <button ui-button variant="ghost" size="sm" (click)="showPreferences.set(!showPreferences())"> {{ showPreferences() ? 'Hide preferences' : 'Customize' }} </button> <div class="ml-auto flex items-center gap-2"> <button ui-button variant="outline" size="sm" (click)="dismiss()">Reject</button> <button ui-button size="sm" (click)="dismiss()"> {{ showPreferences() ? 'Save preferences' : 'Accept all' }} </button> </div> </div> </div> </div> } </div> } `, }) export class UiCookieConsentBannerComponent { protected readonly Cookie = Cookie @Input() variant: CookieConsentBannerVariant = 'bottom-bar' @Input('class') className?: string readonly visible = signal(true) readonly showPreferences = signal(false) readonly analytics = signal(true) readonly marketing = signal(false) get rootClass(): string { return cn('animate-in fade-in duration-150', this.className) } dismiss(): void { this.visible.set(false) } }src/app/components/blocks/cookie-consent/cookie-consent-bar.component.ts3.3 kB
// Bottom consent bar offering accept, reject, and manage as equally weighted actions, with a // one-line statement of what is actually stored. Reject carries the same visual weight as // accept — a ghost "reject" beside a solid "accept" is a dark pattern with extra steps. // Port of the Vue/React block 1:1. import { ChangeDetectionStrategy, Component, Input, OnInit, signal } from '@angular/core' import { Cookie, LucideAngularModule } 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 type CookieConsentChoice = 'accepted' | 'rejected' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-cookie-consent-bar, [ui-cookie-consent-bar]', standalone: true, // The block renders its own fixed root <div>: keep this wrapper out of the layout. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiSeparatorComponent], template: ` @if (visible()) { <div data-slot="cookie-consent-bar" class="border-border bg-card/95 fixed inset-x-0 bottom-0 z-50 border-t backdrop-blur" role="region" aria-label="Cookie consent" > <div class="mx-auto flex max-w-6xl flex-col gap-4 px-6 py-4 lg:flex-row lg:items-center lg:gap-8"> <div class="flex min-w-0 items-start gap-3"> <lucide-icon [img]="Cookie" class="text-muted-foreground mt-0.5 size-4 shrink-0" aria-hidden="true" /> <div class="min-w-0"> <div class="flex flex-wrap items-center gap-2"> <p class="text-sm font-medium">We store two things</p> <span ui-badge variant="outline">No ad tracking</span> </div> <p class="text-muted-foreground mt-1 text-sm leading-relaxed"> A session cookie so you stay signed in, and an analytics cookie recording which pages get read. Rejecting keeps the first and drops the second. </p> </div> </div> <ui-separator orientation="vertical" class="hidden h-10 lg:block" /> <!-- Reject carries the same visual weight as accept. A ghost "reject" beside a solid "accept" is a dark pattern with extra steps. --> <div class="flex shrink-0 flex-wrap items-center gap-2"> <button ui-button variant="outline" (click)="decide('rejected')">Reject analytics</button> <button ui-button (click)="decide('accepted')">Accept</button> <button ui-button variant="ghost" size="sm">Manage</button> </div> </div> </div> } `, }) export class UiCookieConsentBarComponent implements OnInit { protected readonly Cookie = Cookie @Input() storageKey = 'uipkge:consent' readonly visible = signal(false) ngOnInit(): void { try { this.visible.set(!window.localStorage.getItem(this.storageKey)) } catch { this.visible.set(true) } } decide(choice: CookieConsentChoice): void { this.visible.set(false) try { window.localStorage.setItem(this.storageKey, choice) } catch { // Blocked storage: the choice holds for this page view only. } } }src/app/components/blocks/cookie-consent/cookie-consent-preferences.component.ts4.6 kB
// Granular cookie consent dialog: each category states its purpose and retention, with a // switch. Strictly-necessary is listed and shown locked rather than hidden — omitting it // implies there is nothing there, which is not true. Port of the Vue/React block 1:1. import { ChangeDetectionStrategy, Component, signal } from '@angular/core' import { Lock, LucideAngularModule } from 'lucide-angular' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' export interface CookieCategory { id: string label: string purpose: string retention: string locked: boolean } const categories: CookieCategory[] = [ { id: 'necessary', label: 'Strictly necessary', purpose: 'Session and sign-in state. Without these the app cannot keep you logged in.', retention: 'Session', locked: true, }, { id: 'analytics', label: 'Product analytics', purpose: 'Which pages are read and where people give up. Aggregated, never sold.', retention: '13 months', locked: false, }, { id: 'support', label: 'Support chat', purpose: 'Keeps a conversation open across page loads so you do not repeat yourself.', retention: '30 days', locked: false, }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-cookie-consent-preferences, [ui-cookie-consent-preferences]', standalone: true, // The block renders its own root <div>: keep this wrapper out of the layout. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiSeparatorComponent, UiSwitchComponent, ], template: ` <div data-slot="cookie-consent-preferences"> <div ui-dialog [open]="open()" (openChange)="open.set($event)"> <div ui-dialog-content class="sm:max-w-lg"> <div ui-dialog-header> <span ui-badge variant="secondary" class="w-fit">Privacy</span> <h2 ui-dialog-title class="mt-3 text-xl">Choose what we store</h2> <p ui-dialog-description> Each category says what it is for and how long it is kept. Nothing here is advertising. </p> </div> <ui-separator /> <ul class="space-y-4"> @for (category of categories; track category.id) { <li class="flex items-start gap-4"> <div class="min-w-0 flex-1"> <div class="flex flex-wrap items-center gap-2"> <p class="text-sm font-medium">{{ category.label }}</p> @if (category.locked) { <span ui-badge variant="outline" class="gap-1"> <lucide-icon [img]="Lock" class="size-2.5" aria-hidden="true" /> Required </span> } </div> <p class="text-muted-foreground mt-1 text-xs leading-relaxed">{{ category.purpose }}</p> <p class="text-muted-foreground/80 mt-1 font-mono text-xs">Kept {{ category.retention }}</p> </div> <ui-switch [checked]="choices()[category.id] ?? false" (checkedChange)="setChoice(category.id, $event)" [disabled]="category.locked" [attr.aria-label]="'Allow ' + category.label" class="mt-1 shrink-0" /> </li> } </ul> <div ui-dialog-footer class="sm:justify-start"> <button ui-button (click)="open.set(false)">Save choices</button> <button ui-button variant="outline" (click)="open.set(false)">Reject optional</button> </div> </div> </div> </div> `, }) export class UiCookieConsentPreferencesComponent { protected readonly Lock = Lock protected readonly categories = categories readonly open = signal(true) readonly choices = signal<Record<string, boolean>>({ necessary: true, analytics: false, support: false }) setChoice(id: string, value: boolean): void { this.choices.update((prev) => ({ ...prev, [id]: value })) } }src/app/components/blocks/cookie-consent/index.ts0.3 kB
export { UiCookieConsentBannerComponent, type CookieConsentBannerVariant } from './cookie-consent-banner.component' export { UiCookieConsentBarComponent, type CookieConsentChoice } from './cookie-consent-bar.component' export { UiCookieConsentPreferencesComponent, type CookieCategory } from './cookie-consent-preferences.component'
Raw manifest:https://uipkge.dev/r/angular/cookie-consent.json