
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Consent 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.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/cookie-consent.json$npx shadcn@latest add https://uipkge.dev/r/react/cookie-consent.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/cookie-consent.json$bunx shadcn@latest add https://uipkge.dev/r/react/cookie-consent.jsonnpx shadcn@latest add @uipkge-react/cookie-consentInstalls to:components/blocks/cookie-consent/| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'bottom-bar' | 'card' | — | optional |
className | string | — | optional |
'use client'
import * as React from 'react'
import { Cookie } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { cn } from '@/lib/utils'
export interface CookieConsentBannerProps {
variant?: 'bottom-bar' | 'card'
className?: string
}
export function CookieConsentBanner({ variant = 'bottom-bar', className }: CookieConsentBannerProps) {
const [visible, setVisible] = React.useState(true)
const [showPreferences, setShowPreferences] = React.useState(false)
const [analytics, setAnalytics] = React.useState(true)
const [marketing, setMarketing] = React.useState(false)
const dismiss = () => setVisible(false)
const preferences = (
<>
<Separator />
<div className="flex items-center justify-between gap-4 py-1">
<div>
<p className="text-sm font-medium">Necessary</p>
<p className="text-muted-foreground text-xs">Required for the site to function.</p>
</div>
<Switch checked disabled aria-label="Necessary cookies" />
</div>
<div className="flex items-center justify-between gap-4 py-1">
<div>
<p className="text-sm font-medium">Analytics</p>
<p className="text-muted-foreground text-xs">Helps us understand usage patterns.</p>
</div>
<Switch checked={analytics} onCheckedChange={setAnalytics} aria-label="Analytics cookies" />
</div>
<div className="flex items-center justify-between gap-4 py-1">
<div>
<p className="text-sm font-medium">Marketing</p>
<p className="text-muted-foreground text-xs">Used to personalize campaigns.</p>
</div>
<Switch checked={marketing} onCheckedChange={setMarketing} aria-label="Marketing cookies" />
</div>
</>
)
if (!visible) return null
return (
<div
data-slot="cookie-consent-banner"
role="dialog"
aria-label="Cookie consent"
className={cn('animate-in fade-in duration-150', className)}
>
{variant === 'bottom-bar' ? (
<div className="bg-card border-border w-full border-t px-4 py-4 sm:px-6">
<div className="mx-auto flex max-w-5xl flex-col gap-4">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex items-start gap-3">
<span className="bg-primary/10 text-primary mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full">
<Cookie className="size-4" aria-hidden="true" />
</span>
<p className="text-muted-foreground max-w-xl text-sm leading-relaxed">
We use cookies to improve your experience and analyze traffic. Read our{' '}
<a
href="#"
className="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 className="flex shrink-0 flex-wrap items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => setShowPreferences((v) => !v)}>
{showPreferences ? 'Hide preferences' : 'Customize'}
</Button>
<Button variant="outline" size="sm" onClick={dismiss}>
Reject
</Button>
<Button size="sm" onClick={dismiss}>
Accept all
</Button>
</div>
</div>
{showPreferences && (
<div className="flex flex-col gap-3">
{preferences}
<div className="mt-1 flex justify-end">
<Button size="sm" onClick={dismiss}>
Save preferences
</Button>
</div>
</div>
)}
</div>
</div>
) : (
<div className="bg-card border-border mx-auto max-w-md rounded-xl border p-6 shadow-xs">
<div className="flex flex-col gap-4">
<div className="flex items-start gap-3">
<span className="bg-primary/10 text-primary flex size-9 shrink-0 items-center justify-center rounded-full">
<Cookie className="size-4" aria-hidden="true" />
</span>
<div>
<h3 className="text-sm font-semibold">We value your privacy</h3>
<p className="text-muted-foreground mt-1 text-sm leading-relaxed">
We use cookies to improve your experience and analyze traffic. Read our{' '}
<a
href="#"
className="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>
{showPreferences && preferences}
<div className="flex flex-wrap items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => setShowPreferences((v) => !v)}>
{showPreferences ? 'Hide preferences' : 'Customize'}
</Button>
<div className="ml-auto flex items-center gap-2">
<Button variant="outline" size="sm" onClick={dismiss}>
Reject
</Button>
<Button size="sm" onClick={dismiss}>
{showPreferences ? 'Save preferences' : 'Accept all'}
</Button>
</div>
</div>
</div>
</div>
)}
</div>
)
}
'use client'
import { useEffect, useState } from 'react'
import { Cookie } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
export function CookieConsentBar({ storageKey = 'uipkge:consent' }: { storageKey?: string }) {
const [visible, setVisible] = useState(false)
useEffect(() => {
try {
setVisible(!window.localStorage.getItem(storageKey))
} catch {
setVisible(true)
}
}, [storageKey])
function decide(choice: 'accepted' | 'rejected') {
setVisible(false)
try {
window.localStorage.setItem(storageKey, choice)
} catch {
// Blocked storage: the choice holds for this page view only.
}
}
if (!visible) return null
return (
<div
data-slot="cookie-consent-bar"
className="border-border bg-card/95 fixed inset-x-0 bottom-0 z-50 border-t backdrop-blur"
role="region"
aria-label="Cookie consent"
>
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-6 py-4 lg:flex-row lg:items-center lg:gap-8">
<div className="flex min-w-0 items-start gap-3">
<Cookie className="text-muted-foreground mt-0.5 size-4 shrink-0" aria-hidden="true" />
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<p className="text-sm font-medium">We store two things</p>
<Badge variant="outline">No ad tracking</Badge>
</div>
<p className="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>
<Separator orientation="vertical" className="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 className="flex shrink-0 flex-wrap items-center gap-2">
<Button variant="outline" onClick={() => decide('rejected')}>
Reject analytics
</Button>
<Button onClick={() => decide('accepted')}>Accept</Button>
<Button variant="ghost" size="sm">
Manage
</Button>
</div>
</div>
</div>
)
}
'use client'
import { useState } from 'react'
import { Lock } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
const categories = [
{
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,
},
]
export function CookieConsentPreferences() {
const [open, setOpen] = useState(true)
const [choices, setChoices] = useState<Record<string, boolean>>({
necessary: true,
analytics: false,
support: false,
})
return (
<div data-slot="cookie-consent-preferences">
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<Badge variant="secondary" className="w-fit">
Privacy
</Badge>
<DialogTitle className="mt-3 text-xl">Choose what we store</DialogTitle>
<DialogDescription>
Each category says what it is for and how long it is kept. Nothing here is advertising.
</DialogDescription>
</DialogHeader>
<Separator />
<ul className="space-y-4">
{categories.map((category) => (
<li key={category.id} className="flex items-start gap-4">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<p className="text-sm font-medium">{category.label}</p>
{category.locked && (
<Badge variant="outline" className="gap-1">
<Lock className="size-2.5" aria-hidden="true" />
Required
</Badge>
)}
</div>
<p className="text-muted-foreground mt-1 text-xs leading-relaxed">{category.purpose}</p>
<p className="text-muted-foreground/80 mt-1 font-mono text-xs">Kept {category.retention}</p>
</div>
<Switch
checked={choices[category.id]}
onCheckedChange={(value: boolean) => setChoices((c) => ({ ...c, [category.id]: value }))}
disabled={category.locked}
aria-label={`Allow ${category.label}`}
className="mt-1 shrink-0"
/>
</li>
))}
</ul>
<DialogFooter className="sm:justify-start">
<Button onClick={() => setOpen(false)}>Save choices</Button>
<Button variant="outline" onClick={() => setOpen(false)}>
Reject optional
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
Raw manifest:https://uipkge.dev/r/react/cookie-consent.json