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>

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

Value Description Type Default
cookieName
Unique cookie name to track if the banner has been dismissed String "banner_dismissed"
cookieDays
Number of days to remember the banner dismissal (-1 = no cookie, re-shows on every page refresh; 0 = session only; >0 = persistent cookie for specified days) Number 0
countdownEndTime
ISO 8601 date string for countdown deadline (e.g., "2024-12-31T23:59:59"). String ""
autoHide
Automatically hide the banner when countdown expires (only works if countdownEndTime is set) Boolean false

Targets

Target Description Required
hours
Element that displays the hours in the countdown Optional
minutes
Element that displays the minutes in the countdown Optional
seconds
Element that displays the seconds in the countdown Optional

Actions

Action Description Usage
hide
Hides the banner with animation and sets a cookie to remember the dismissal click->banner#hide

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-0 with -translate-y-full for the initial hidden state
  • Bottom Banner: Use fixed bottom-0 left-0 right-0 with translate-y-full for the initial hidden state
  • Centered Bottom Banner: Use fixed bottom-4 left-1/2 -translate-x-1/2 with translate-y-full for 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 hidden class is removed and the banner animates in smoothly

This ensures users never see a banner briefly flash on screen before being hidden.

Table of contents

Get notified when new components come out