Banner Rails Components
Banners are used to display important announcements, promotions, cookie consent notices, and time-sensitive messages to users. Built with Stimulus and Tailwind CSS, these banners include cookie management and countdown timers.
Installation
1. Stimulus Controller Setup
Start by adding the banner controller to your project:
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="banner"
export default class extends Controller {
static targets = ["days", "hours", "minutes", "seconds"];
static values = {
cookieName: { type: String, default: "banner_dismissed" }, // The name of the cookie to store the banner dismissal
cookieDays: { type: Number, default: 0 }, // How long the cookie should persist (-1 = no cookie, 0 = session only, >0 = days)
countdownEndTime: { type: String, default: "" }, // ISO 8601 date string (e.g., "2024-12-31T23:59:59")
autoHide: { type: Boolean, default: false }, // Auto hide after countdown expires (I recommend setting this to true by default)
};
connect() {
// Check if banner was previously dismissed - do this BEFORE showing anything
if (this.isBannerDismissed()) {
// Don't show the banner at all, just remove from DOM
return;
}
// Check if countdown has already expired (only if countdown is configured)
if (this.countdownEndTimeValue) {
const endTime = new Date(this.countdownEndTimeValue);
if (!isNaN(endTime.getTime()) && endTime <= new Date()) {
// Countdown already expired, don't show banner
return;
}
}
// Banner should be shown - remove hidden class and animate in
this.element.classList.remove("hidden");
requestAnimationFrame(() => {
this.element.classList.remove("opacity-0");
// Remove only the y-axis translations, preserve x-axis (for centered banners)
this.element.classList.remove("-translate-y-full", "translate-y-full");
this.element.classList.add("opacity-100", "translate-y-0");
});
// Initialize countdown if we have a target end time
if (this.countdownEndTimeValue) {
this.initializeCountdown();
}
}
disconnect() {
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
}
}
initializeCountdown() {
// Parse the end time as ISO 8601 date string
const endTime = new Date(this.countdownEndTimeValue);
// Validate the date
if (isNaN(endTime.getTime())) {
console.error("Invalid countdown end time. Please provide a valid ISO 8601 date string (e.g., '2024-12-31T23:59:59'):", this.countdownEndTimeValue);
return;
}
this.endTime = endTime;
// Update immediately
this.updateCountdown();
// Update every second
this.countdownTimer = setInterval(() => {
this.updateCountdown();
}, 1000);
}
updateCountdown() {
const now = new Date();
const timeRemaining = this.endTime - now;
if (timeRemaining <= 0) {
// Countdown expired
this.handleCountdownExpired();
return;
}
// Calculate days, hours, minutes, seconds
const days = Math.floor(timeRemaining / (1000 * 60 * 60 * 24));
const hours = Math.floor((timeRemaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((timeRemaining % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeRemaining % (1000 * 60)) / 1000);
// Update targets if they exist
if (this.hasDaysTarget) {
this.daysTarget.textContent = String(days).padStart(2, "0");
}
if (this.hasHoursTarget) {
this.hoursTarget.textContent = String(hours).padStart(2, "0");
}
if (this.hasMinutesTarget) {
this.minutesTarget.textContent = String(minutes).padStart(2, "0");
}
if (this.hasSecondsTarget) {
this.secondsTarget.textContent = String(seconds).padStart(2, "0");
}
}
handleCountdownExpired() {
// Clear the interval
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
// Display zeros
if (this.hasDaysTarget) this.daysTarget.textContent = "00";
if (this.hasHoursTarget) this.hoursTarget.textContent = "00";
if (this.hasMinutesTarget) this.minutesTarget.textContent = "00";
if (this.hasSecondsTarget) this.secondsTarget.textContent = "00";
// Auto hide if configured
if (this.autoHideValue) {
setTimeout(() => {
this.hide();
}, 2000); // Wait 2 seconds before hiding
}
}
hide(event) {
if (event) event.preventDefault();
// Add exit animation using Tailwind classes
this.element.classList.remove("opacity-100", "translate-y-0");
this.element.classList.add("opacity-0");
// Add the appropriate slide out based on position
if (this.element.classList.contains("top-0")) {
this.element.classList.add("-translate-y-full");
} else if (this.element.classList.contains("bottom-0")) {
this.element.classList.add("translate-y-full");
}
// Wait for animation to complete before removing
setTimeout(() => {
// Set cookie to remember dismissal (only if cookieDays is not -1)
if (this.cookieDaysValue !== -1) {
this.setCookie(this.cookieNameValue, "true", this.cookieDaysValue);
// Remove element from DOM when using cookies
this.element.remove();
} else {
// When cookieDays is -1, keep element in DOM but hide it for refresh functionality
this.element.classList.add("hidden");
}
}, 300); // Match the transition duration
}
// Cookie management methods
setCookie(name, value, days) {
if (days === 0) {
// Session cookie (no expiration - lasts until browser closes)
document.cookie = name + "=" + value + ";path=/;SameSite=Lax";
} else {
// Persistent cookie with expiration
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
const expires = "expires=" + date.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/;SameSite=Lax";
}
}
getCookie(name) {
const nameEQ = name + "=";
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i];
while (cookie.charAt(0) === " ") {
cookie = cookie.substring(1, cookie.length);
}
if (cookie.indexOf(nameEQ) === 0) {
return cookie.substring(nameEQ.length, cookie.length);
}
}
return null;
}
isBannerDismissed() {
return this.getCookie(this.cookieNameValue) === "true";
}
}
Examples
Top announcement banner
A banner fixed to the top of the page with an icon, title, description, and action button. Perfect for product announcements and feature launches. Re-shows on every page refresh.
<div data-controller="banner" data-banner-cookie-name-value="top_announcement_dismissed" data-banner-cookie-days-value="-1" class="hidden fixed top-0 left-0 right-0 z-50 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white border-b border-black/10 dark:border-white/10 shadow-xs transition-all duration-300 ease-in-out opacity-0 -translate-y-full">
<div class="container mx-auto px-4">
<div class="flex items-center justify-between gap-2 sm:gap-4 py-3">
<div class="flex items-center gap-3 flex-1">
<div class="hidden sm:flex size-9 shrink-0 items-center justify-center rounded-full sm:bg-neutral-100 sm:dark:bg-neutral-800" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-90" aria-hidden="true">
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
<path d="m2 17 10 5 10-5"></path>
<path d="m2 12 10 5 10-5"></path>
</svg>
</div>
<div class="space-y-1">
<p class="text-sm font-semibold">New Feature Launch! 🚀</p>
<p class="hidden sm:block text-xs text-neutral-600 dark:text-neutral-400">Check out our latest Stimulus components for Rails.</p>
</div>
</div>
<div class="flex items-center gap-2">
<a href="javascript:void(0)" class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3 py-1.5 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">
Learn more
</a>
<button data-action="click->banner#hide" class="inline-flex items-center justify-center p-1.5 right-2 top-2 rounded-full opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden hover:bg-neutral-500/15 active:bg-neutral-500/25 disabled:pointer-events-none" aria-label="Close banner">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4"><line x1="18" x2="6" y1="6" y2="18"></line><line x1="6" x2="18" y1="6" y2="18"></line></svg>
</button>
</div>
</div>
</div>
</div>
Bottom cookie consent banner
A cookie consent banner positioned at the bottom of the page with multiple action buttons. Re-shows on every page refresh.
<div data-controller="banner" data-banner-cookie-name-value="cookie_consent_dismissed" data-banner-cookie-days-value="-1" class="hidden fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-neutral-900 border-t border-black/10 dark:border-white/10 shadow-xs transition-all duration-300 ease-in-out opacity-0 translate-y-full">
<div class="container mx-auto px-4">
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 py-4">
<div class="flex items-start gap-3 flex-1">
<div class="flex size-9 shrink-0 items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-800 mt-0.5" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" class="text-neutral-700 dark:text-neutral-50" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.75,8c-1.91,0-3.469-1.433-3.703-3.28-.099,.01-.195,.03-.297,.03-1.618,0-2.928-1.283-2.989-2.887-3.413,.589-6.011,3.556-6.011,7.137,0,4.004,3.246,7.25,7.25,7.25s7.25-3.246,7.25-7.25c0-.434-.045-.857-.118-1.271-.428,.17-.893,.271-1.382,.271Z"></path>
<circle cx="12.25" cy="1.75" r=".75" fill="currentColor" data-stroke="none" stroke="none"></circle>
<circle cx="14.75" cy="4.25" r=".75" fill="currentColor" data-stroke="none" stroke="none"></circle>
<circle cx="11.25" cy="11.75" r=".75" fill="currentColor" data-stroke="none" stroke="none"></circle>
<circle cx="7" cy="7" r="1" fill="currentColor" data-stroke="none" stroke="none"></circle>
<circle cx="7.25" cy="11.25" r="1.25" fill="currentColor" data-stroke="none" stroke="none"></circle>
</svg>
</div>
<div class="space-y-1">
<h3 class="text-sm font-semibold text-neutral-900 dark:text-white">We use cookies</h3>
<p class="text-xs text-neutral-600 dark:text-neutral-400 max-w-2xl">By continuing to use this site, you agree to our use of cookies.</p>
</div>
</div>
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 w-full sm:w-auto">
<button data-action="click->banner#hide" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">
Accept All Cookies
</button>
<a href="javascript:void(0)" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200">
Manage Preferences
</a>
</div>
</div>
</div>
</div>
Black Friday countdown banner
A promotional banner with a real-time countdown timer to a specific deadline. Only real, fixed dates are supported to maintain ethical marketing practices. Auto-hides when countdown expires and re-shows on every page refresh.
<%
# Calculate the last Friday of November (Black Friday)
current_year = Date.today.year
# Find the last day of November
black_friday = Date.new(current_year, 11, -1)
# Move backwards until we find a Friday
black_friday -= 1 until black_friday.friday?
# If Black Friday has already passed this year, calculate for next year
if Date.today > black_friday
black_friday = Date.new(current_year + 1, 11, -1)
black_friday -= 1 until black_friday.friday?
end
# Set the countdown to end at 11:59:59 PM on Black Friday
countdown_end_time = Time.new(
black_friday.year,
black_friday.month,
black_friday.day,
23, 59, 59
).iso8601
%>
<div data-controller="banner" data-banner-cookie-name-value="black_friday_dismissed" data-banner-cookie-days-value="-1" data-banner-countdown-end-time-value="<%= countdown_end_time %>" data-banner-auto-hide-value="true" class="hidden fixed top-0 left-0 right-0 border-b border-black/10 dark:border-white/10 shadow-xs z-50 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white transition-all duration-300 ease-in-out opacity-0 -translate-y-full">
<div class="container mx-auto px-4">
<div class="flex flex-col md:flex-row gap-2 md:items-center py-3">
<div class="flex grow gap-3 md:items-center">
<div class="flex grow flex-col justify-between gap-3 md:flex-row md:items-center">
<div class="space-y-0.5 flex items-center gap-3">
<div class="flex size-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-red-500/20 to-orange-500/20 max-md:mt-0.5" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" class="text-red-500 dark:text-red-400 opacity-90" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="7" cy="7" r="1" fill="currentColor" data-stroke="none" stroke="none"></circle><circle cx="11" cy="11" r="1" fill="currentColor" data-stroke="none" stroke="none"></circle><line x1="6.75" y1="11.25" x2="11.25" y2="6.75"></line><path d="M14.5,9c0-.967,.784-1.75,1.75-1.75v-1.5c0-1.104-.895-2-2-2H3.75c-1.105,0-2,.896-2,2v1.5c.966,0,1.75,.783,1.75,1.75s-.784,1.75-1.75,1.75v1.5c0,1.104,.895,2,2,2H14.25c1.105,0,2-.896,2-2v-1.5c-.966,0-1.75-.783-1.75-1.75Z"></path></g></svg>
</div>
<p class="text-sm font-semibold">Black Friday Sale! 🔥</p>
</div>
<div class="flex gap-3 max-md:flex-wrap items-center">
<div class="w-full sm:w-auto flex justify-center items-center divide-x divide-neutral-200 dark:divide-neutral-700/75 rounded-lg bg-neutral-100 dark:bg-neutral-950/50 text-sm border border-neutral-200 dark:border-neutral-700/75">
<span class="flex h-9 items-center justify-center px-3 py-2">
<span data-banner-target="days" class="text-xs sm:text-sm font-mono font-semibold">00</span>
<span class="ml-0.5 text-xs text-neutral-600 dark:text-neutral-400">d</span>
</span>
<span class="flex h-9 items-center justify-center px-3 py-2">
<span data-banner-target="hours" class="text-xs sm:text-sm font-mono font-semibold">00</span>
<span class="ml-0.5 text-xs text-neutral-600 dark:text-neutral-400">h</span>
</span>
<span class="flex h-9 items-center justify-center px-3 py-2">
<span data-banner-target="minutes" class="text-xs sm:text-sm font-mono font-semibold">00</span>
<span class="ml-0.5 text-xs text-neutral-600 dark:text-neutral-400">m</span>
</span>
<span class="flex h-9 items-center justify-center px-3 py-2">
<span data-banner-target="seconds" class="text-xs sm:text-sm font-mono font-semibold">00</span>
<span class="ml-0.5 text-xs text-neutral-600 dark:text-neutral-400">s</span>
</span>
</div>
<a href="javascript:void(0)" class="w-full sm:w-auto flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">
Shop Now
</a>
</div>
</div>
</div>
<button data-action="click->banner#hide" class="inline-flex items-center justify-center p-1.5 max-md:absolute max-md:top-2 max-md:right-2 right-2 top-2 rounded-full opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden hover:bg-neutral-500/15 active:bg-neutral-500/25 disabled:pointer-events-none" aria-label="Close banner">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4"><line x1="18" x2="6" y1="6" y2="18"></line><line x1="6" x2="18" y1="6" y2="18"></line></svg>
</button>
</div>
</div>
</div>
Promotional Sticky Banner
A centered sticky banner positioned at the bottom with a margin. Features a gradient background with pattern overlay and promo code display. Perfect for limited-time offers. Re-shows on every page refresh.
<div data-controller="banner" data-banner-cookie-name-value="promo_sticky_dismissed" data-banner-cookie-days-value="-1" class="hidden fixed bottom-4 left-1/2 z-50 w-full max-w-2xl mx-auto px-4 transition-all duration-300 ease-in-out opacity-0 -translate-x-1/2 translate-y-full">
<div class="relative bg-gradient-to-br from-white via-neutral-50 to-neutral-100 dark:from-neutral-900 dark:via-neutral-950 dark:to-black text-neutral-900 dark:text-white rounded-2xl shadow-lg overflow-hidden border border-neutral-300 dark:border-neutral-800">
<%# Background pattern %>
<div class="absolute inset-0 opacity-5">
<svg class="w-full h-full" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="10" cy="10" r="1" fill="currentColor"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)"/>
</svg>
</div>
<%# Content %>
<div class="relative px-4 sm:px-6 py-4">
<div class="flex items-start gap-4">
<div class="hidden sm:flex size-9 shrink-0 items-center justify-center rounded-full bg-neutral-900/10 dark:bg-white/10 backdrop-blur-sm ring-1 ring-neutral-900/10 dark:ring-white/10" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" class="opacity-90" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><circle cx="7" cy="7" r="1" fill="currentColor" data-stroke="none" stroke="none"></circle><circle cx="11" cy="11" r="1" fill="currentColor" data-stroke="none" stroke="none"></circle><line x1="6.75" y1="11.25" x2="11.25" y2="6.75"></line><path d="M14.5,9c0-.967,.784-1.75,1.75-1.75v-1.5c0-1.104-.895-2-2-2H3.75c-1.105,0-2,.896-2,2v1.5c.966,0,1.75,.783,1.75,1.75s-.784,1.75-1.75,1.75v1.5c0,1.104,.895,2,2,2H14.25c1.105,0,2-.896,2-2v-1.5c-.966,0-1.75-.783-1.75-1.75Z"></path></g></svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-2 sm:gap-4">
<div>
<h3 class="text-sm sm:text-base font-bold mb-1">Get 30% Off Your First Purchase!</h3>
<p class="text-xs sm:text-sm text-neutral-600 dark:text-neutral-400 mb-3">Join thousands of developers using our Rails components. Use code <code class="px-2 py-0.5 rounded bg-black/10 dark:bg-white/10 text-neutral-900 dark:text-white font-mono text-xs">HOTWIRE30</code> at checkout.</p>
<div class="flex flex-wrap gap-2">
<a href="javascript:void(0)" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-400/30 bg-neutral-800 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-white shadow-sm transition-all duration-100 ease-in-out select-none hover:bg-neutral-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-neutral-800 dark:hover:bg-neutral-100 dark:focus-visible:outline-neutral-200">
Claim Offer
</a>
<button data-action="click->banner#hide" class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200">
Maybe Later
</button>
</div>
</div>
<button data-action="click->banner#hide" class="inline-flex items-center justify-center p-1.5 absolute right-2 top-2 rounded-full opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden hover:bg-neutral-500/15 active:bg-neutral-500/25 disabled:pointer-events-none" aria-label="Close banner">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4"><line x1="18" x2="6" y1="6" y2="18"></line><line x1="6" x2="18" y1="6" y2="18"></line></svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
Configuration
The banner component is powered by a Stimulus controller that provides cookie management, countdown timers, and flexible configuration options.
Configuration Values
Targets
Actions
Key Features
- Cookie Management: Flexible cookie persistence options with configurable duration
- Countdown Timer: Real-time countdown with automatic updates every second
- Auto-Hide on Expiry: Optional automatic hiding when countdown reaches zero
- Flash Prevention: Smart visibility checks prevent unwanted content flashing on page load
- Smooth Animations: Built-in CSS transitions for elegant show/hide animations
Positioning
Banners use Tailwind's fixed positioning classes. Here are the recommended configurations:
- Top Banner: Use
fixed top-0 left-0 right-0with-translate-y-fullfor the initial hidden state - Bottom Banner: Use
fixed bottom-0 left-0 right-0withtranslate-y-fullfor the initial hidden state - Centered Bottom Banner: Use
fixed bottom-4 left-1/2 -translate-x-1/2withtranslate-y-fullfor a floating centered bottom banner
All banners should start with hidden opacity-0 classes and include transition-all duration-300 ease-in-out for smooth animations.
Flash Prevention
The banner controller is designed to prevent any flash of unwanted content. All banners have the hidden class by default, and the controller performs the following checks before removing it:
- Cookie check: If the banner was previously dismissed, it won't show at all
- Countdown expiry check: If a countdown is configured and already expired, the banner won't show
- Only then: The
hiddenclass is removed and the banner animates in smoothly
This ensures users never see a banner briefly flash on screen before being hidden.