This Astro cookie banner is a single component with no dependencies. It asks once, stores the answer in localStorage, and only loads your analytics script after the visitor accepts. Declining is as easy as accepting, the banner doesn’t block the page, and a “Cookie settings” link lets people change their mind. It’s a starting point; whether it meets your legal obligations depends on your jurisdiction and what you track.
Key Takeaways
- Don’t load tracking scripts until the visitor consents.
- Make “Decline” as prominent as “Accept”.
- Store the choice locally; no server needed.
- If you use cookieless analytics, you may not need a banner at all. Check the rules that apply to you.
The component
---
// src/components/CookieBanner.astro
interface Props { analyticsSrc: string; policyHref?: string }
const { analyticsSrc, policyHref = '/privacy/' } = Astro.props;
---
<div class="cookie-banner" role="region" aria-label="Cookie consent" hidden data-analytics-src={analyticsSrc}>
<p>
We use analytics cookies to understand how the site is used.
<a href={policyHref}>Privacy policy</a>
</p>
<div class="cookie-actions">
<button type="button" data-consent="declined">Decline</button>
<button type="button" data-consent="accepted">Accept</button>
</div>
</div>
<style>
.cookie-banner {
position: fixed; inset: auto 1rem 1rem 1rem; z-index: 50;
max-width: 36rem; margin-inline: auto; padding: 1rem 1.25rem;
background: var(--color-bg, #fff); border: 1px solid var(--color-border, #e5e4df);
border-radius: 0.75rem; box-shadow: 0 8px 30px rgb(0 0 0 / 0.12);
}
.cookie-banner p { margin: 0 0 0.75rem; }
.cookie-actions { display: flex; gap: 0.5rem; justify-content: flex-end; }
.cookie-actions button { padding: 0.5rem 1rem; border-radius: 0.5rem; border: 1px solid currentColor; background: none; cursor: pointer; font: inherit; }
</style>
<script>
const KEY = 'cookie-consent';
const banner = document.querySelector<HTMLElement>('.cookie-banner');
const read = () => { try { return localStorage.getItem(KEY); } catch { return null; } };
const write = (v: string) => { try { localStorage.setItem(KEY, v); } catch {} };
const loadAnalytics = () => {
const src = banner?.dataset.analyticsSrc;
if (!src || document.querySelector(`script[src="${src}"]`)) return;
const s = document.createElement('script');
s.src = src;
s.defer = true;
document.head.append(s);
};
if (banner) {
const choice = read();
if (choice === 'accepted') loadAnalytics();
if (!choice) banner.hidden = false;
banner.querySelectorAll<HTMLButtonElement>('[data-consent]').forEach((btn) =>
btn.addEventListener('click', () => {
write(btn.dataset.consent!);
banner.hidden = true;
if (btn.dataset.consent === 'accepted') loadAnalytics();
}),
);
// Any element with data-cookie-settings reopens the banner
document.querySelectorAll('[data-cookie-settings]').forEach((el) =>
el.addEventListener('click', (e) => {
e.preventDefault();
banner.hidden = false;
}),
);
}
</script>
Using it
---
import CookieBanner from '../components/CookieBanner.astro';
---
<CookieBanner analyticsSrc="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX" />
And somewhere in the footer:
<a href="#" data-cookie-settings>Cookie settings</a>
Google Analytics also needs its configuration snippet after the script loads; put it in a small file and load that instead of the raw tag URL.
Withdrawing consent
Reopening the banner and choosing “Decline” stops the script loading on the next page view. Cookies already set by the analytics tool stay until they expire or are deleted; to remove them immediately, clear the specific cookie names your provider uses when the visitor declines.
You might not need it
Several analytics tools work without cookies and without collecting personal data, which can reduce or remove the need for a consent banner, depending on the law that applies to you. The privacy-friendly analytics tip compares them. This isn’t legal advice; check your obligations for the regions you serve.
Notes
localStorageaccess is wrapped in try/catch, because it can throw in private modes or when storage is blocked.- The banner starts
hidden, so visitors who already chose never see it flash. - With
<ClientRouter />, the banner persists across navigations if you addtransition:persistto it.
Third-party script performance is covered in the Astro performance guide, and more components in the Astro components guide. The UK ICO’s guidance on cookies is a useful plain-language reference.
FAQ
Does this banner block cookies set by embeds like YouTube?
No. It only controls the analytics script you pass in. Use privacy-enhanced embed modes or load embeds after consent too.
Can I add categories (analytics, marketing)?
Yes. Store an object such as { analytics: true, marketing: false } instead of a string, and load each script based on its category.