How to build an animated gradient announcement banner in React
An animated gradient banner in React cycles the background-position of a 200%-wide linear gradient using a CSS keyframe animation, while a useState flag controls dismissal. The entrance slide is a separate keyframe that translates the banner from -100% to 0 on mount.
- Stack: React 18 + Tailwind v4 + lucide-react, ~100 lines, no Framer Motion required.
- Two injected CSS keyframes handle the full animation: gradient-shift (6s loop) and banner-in (0.4s entrance).
- Accessible: close button has an aria-label; the banner uses semantic color tokens, not hardcoded values.
- Fully responsive, text scales from xs to sm, the close button is positioned absolutely so it never wraps.
- Works in both light and dark themes via CSS custom property tokens (--color-accent, --color-background).
Banner Gradient is a dismissible announcement bar for the top of a page. A looping CSS gradient animates the background, a brief slide-in keyframe announces its presence on mount, and a close button removes it from the DOM without a page reload. Simple enough to drop into any layout, polished enough to feel intentional.
Anatomy
The root div is position:relative with overflow:hidden. Two absolutely positioned divs layer inside it: first, the animated gradient background (linear-gradient at 200% width, shifted by keyframe); second, a subtle radial overlay that adds highlight depth. On top of both sits the content row: a Sparkles icon, a short paragraph with an optional inline link, and a close button pinned to the right edge with position:absolute.
How it works
The gradient effect needs only two CSS keyframes injected via a `<style>` tag inside the component. `banner-gradient-shift` moves background-position from `0% 50%` to `100% 50%` and back over 6 seconds with ease-in-out, creating a smooth color sweep. `banner-gradient-in` translates the whole banner from `translateY(-100%)` to `translateY(0)` with 0.4s ease-out on mount. Dismissal is a plain `useState(true)`, when set to false the component returns null and React unmounts it cleanly.
How to build it in React
Set up the layered structure
Create a relative div with overflow:hidden as the root. Inside, add two absolutely positioned divs: one for the gradient background and one for the radial highlight overlay. Both should cover the entire root with `inset-0`. The content row goes last, positioned relative so it sits above the background layers.
Inject the CSS keyframes
Drop a `<style>` tag at the end of the JSX to define both keyframes. The gradient animation needs backgroundSize set to `200% 200%` on the target element for the position shift to produce visible motion. Keep the entrance animation on the root element via the `animation` style prop.
@keyframes banner-gradient-shift { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } } @keyframes banner-gradient-in { from { opacity: 0; transform: translateY(-100%); } to { opacity: 1; transform: translateY(0); } }Wire up dismissal with useState
Keep a single boolean flag. When it is false, return null early so React removes the element from the DOM entirely. The close button calls setVisible(false) via onClick. No exit animation is needed here, but you can wrap the root in a Framer Motion AnimatePresence if you want a fade-out on dismiss.
const [visible, setVisible] = useState(true); if (!visible) return null;Use CSS tokens for theming
Avoid hardcoded hex values. Plug `var(--color-accent)` into the gradient and `var(--color-background)` into the text and icon colors. This way the banner automatically respects whatever theme preset is active without any extra JavaScript logic.
When to use it
Use it for time-limited announcements: a launch discount, a planned maintenance window, a new feature rollout. It works best as a true top-of-page opener placed before the navbar or hero. Skip it on pages where the headline CTA needs to be the first thing a visitor reads; the banner will compete for attention. Also avoid stacking multiple banners simultaneously, one message, one banner.
Used by
FAQ
How do I persist the dismissed state across page reloads?
Replace useState with a value read from localStorage. On mount, check if a key like `banner-dismissed` is set; if so, initialize visible to false. Write the key in the close handler. Wrap the localStorage read in a try/catch for SSR safety.
Can I add a fade-out animation when the banner is dismissed?
Yes. Wrap the root div in Framer Motion's AnimatePresence and add an exit prop with opacity:0 and y:-20. Change the inner div to motion.div and pass the exit alongside initial and animate. The banner will fade and slide up before React removes it.
Why inject keyframes in a <style> tag instead of a Tailwind class?
Tailwind v4 does support arbitrary keyframes via the config, but for a self-contained component that ships as a copy-paste snippet the inline <style> tag keeps all the animation logic in one file. No build config change required, and the keyframes are scoped by their unique names.
Does the banner work without JavaScript?
No. The 'use client' directive makes it a client component; without JS the banner does not render. For a no-JS fallback, move the banner into a Server Component and remove the dismiss button, or render it in a <noscript> tag as a static HTML bar.