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 shadcn-vue@latest add https://uipkge.dev/r/vue/cookie-consent.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/cookie-consent.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/cookie-consent.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/cookie-consent.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/cookie-consentInstalls to:app/components/blocks/cookie-consent/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'bottom-bar''card' | 'bottom-bar' | optional |
Files installed (3)
app/components/blocks/cookie-consent/CookieConsentBanner.vue6.3 kB
<script setup lang="ts"> import { ref } from 'vue' import { Cookie } from 'lucide-vue-next' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' import { Switch } from '@/components/ui/switch' interface Props { variant?: 'bottom-bar' | 'card' } withDefaults(defineProps<Props>(), { variant: 'bottom-bar', }) const visible = ref(true) const showPreferences = ref(false) const analytics = ref(true) const marketing = ref(false) function dismiss() { visible.value = false } </script> <template> <Transition enter-active-class="transition-opacity duration-150 ease-out" enter-from-class="opacity-0" leave-active-class="transition-opacity duration-150 ease-in" leave-to-class="opacity-0" > <div v-if="visible" data-slot="cookie-consent-banner" role="dialog" aria-label="Cookie consent"> <div v-if="variant === 'bottom-bar'" 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" > <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 variant="ghost" size="sm" @click="showPreferences = !showPreferences"> {{ showPreferences ? 'Hide preferences' : 'Customize' }} </Button> <Button variant="outline" size="sm" @click="dismiss">Reject</Button> <Button size="sm" @click="dismiss">Accept all</Button> </div> </div> <div v-if="showPreferences" class="flex flex-col gap-3"> <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> <Switch :model-value="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> <Switch v-model="analytics" 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> <Switch v-model="marketing" aria-label="Marketing cookies" /> </div> <div class="mt-1 flex justify-end"> <Button size="sm" @click="dismiss">Save preferences</Button> </div> </div> </div> </div> <div v-else 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"> <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> <template v-if="showPreferences"> <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> <Switch :model-value="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> <Switch v-model="analytics" 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> <Switch v-model="marketing" aria-label="Marketing cookies" /> </div> </template> <div class="flex flex-wrap items-center gap-2"> <Button variant="ghost" size="sm" @click="showPreferences = !showPreferences"> {{ showPreferences ? 'Hide preferences' : 'Customize' }} </Button> <div class="ml-auto flex items-center gap-2"> <Button variant="outline" size="sm" @click="dismiss">Reject</Button> <Button size="sm" @click="dismiss"> {{ showPreferences ? 'Save preferences' : 'Accept all' }} </Button> </div> </div> </div> </div> </div> </Transition> </template>app/components/blocks/cookie-consent/CookieConsentBar.vue2.5 kB
<script setup lang="ts"> import { onMounted, ref } from 'vue' import { Cookie } from 'lucide-vue-next' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' const props = withDefaults(defineProps<{ storageKey?: string }>(), { storageKey: 'uipkge:consent', }) const visible = ref(false) function decide(choice: 'accepted' | 'rejected') { visible.value = false try { window.localStorage.setItem(props.storageKey, choice) } catch { // Blocked storage: the choice holds for this page view only. } } onMounted(() => { try { visible.value = !window.localStorage.getItem(props.storageKey) } catch { visible.value = true } }) </script> <template> <Transition enter-active-class="transition duration-200 ease-out" enter-from-class="translate-y-full opacity-0" leave-active-class="transition duration-150 ease-in" leave-to-class="translate-y-full opacity-0" > <div v-if="visible" 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"> <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> <Badge variant="outline">No ad tracking</Badge> </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> <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 variant="outline" @click="decide('rejected')">Reject analytics</Button> <Button @click="decide('accepted')">Accept</Button> <Button variant="ghost" size="sm">Manage</Button> </div> </div> </div> </Transition> </template>app/components/blocks/cookie-consent/CookieConsentPreferences.vue3 kB
<script setup lang="ts"> import { reactive, ref } from 'vue' import { Lock } from 'lucide-vue-next' 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' // Strictly-necessary is listed and shown locked rather than hidden. Omitting it // implies there is nothing there, which is not true. 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, }, ] const open = ref(true) const choices = reactive<Record<string, boolean>>({ necessary: true, analytics: false, support: false }) </script> <template> <div data-slot="cookie-consent-preferences"> <Dialog v-model:open="open"> <DialogContent class="sm:max-w-lg"> <DialogHeader> <Badge variant="secondary" class="w-fit">Privacy</Badge> <DialogTitle class="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 class="space-y-4"> <li v-for="category in categories" :key="category.id" 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> <Badge v-if="category.locked" variant="outline" class="gap-1"> <Lock class="size-2.5" aria-hidden="true" /> Required </Badge> </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> <Switch v-model="choices[category.id]" :disabled="category.locked" :aria-label="`Allow ${category.label}`" class="mt-1 shrink-0" /> </li> </ul> <DialogFooter class="sm:justify-start"> <Button @click="open = false">Save choices</Button> <Button variant="outline" @click="open = false">Reject optional</Button> </DialogFooter> </DialogContent> </Dialog> </div> </template>
Raw manifest:https://uipkge.dev/r/vue/cookie-consent.json