shadimage
uniquel
componentannouncement-barsource: ok · hit

Announcement Bar

Restrained product update strip with gradient sheen and a clean collapse dismiss.

npx shadcn add "https://uniquel.io/r/announcement-bar.json"
free

Where it goes

banner

Visual style

gradientminimal

Interaction

accordion

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationframer-motion

Files1 · source cached 10/14/2026, 3:19:38 PM

components/navigation/AnnouncementBar.tsx
'use client';

import { AnimatePresence, motion } from 'framer-motion';
import { useState, type ReactNode } from 'react';

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
//
// message       Bar copy
// action        Optional CTA — { label, href?, onClick? }
// icon          Optional leading mark
// dismissible   Show collapse dismiss control
// tone          neutral | accent | inverse
// sticky        Stick to the top of the viewport
// onDismiss     Fired after collapse
// className     Styles for the outer shell
//
type AnnouncementAction = {
  label: string;
  href?: string;
  onClick?: () => void;
};

type AnnouncementBarProps = {
  message?: string;
  action?: AnnouncementAction;
  icon?: ReactNode;
  dismissible?: boolean;
  tone?: 'neutral' | 'accent' | 'inverse';
  sticky?: boolean;
  onDismiss?: () => void;
  className?: string;
};

const TONE = {
  neutral:
    'border-[color:var(--color-border,#e5e7eb)] bg-[var(--color-background,#fff)] text-[color:var(--color-foreground,#1f2937)]',
  accent:
    'border-rose-200/60 bg-linear-to-r from-rose-50 via-white to-amber-50 text-rose-950',
  inverse:
    'border-gray-800 bg-linear-to-r from-gray-950 via-gray-900 to-gray-950 text-white',
};

export default function AnnouncementBar({
  message = 'New: Coverflow Gallery is live — explore the catalog.',
  action = { label: "See what's new", href: '#' },
  icon,
  dismissible = true,
  tone = 'neutral',
  sticky = false,
  onDismiss,
  className = '',
}: AnnouncementBarProps) {
  const [open, setOpen] = useState(true);
  const inverse = tone === 'inverse';

  return (
    <AnimatePresence initial={false}>
      {open ? (
        <motion.div
          className={[
            'w-full overflow-hidden border-b',
            sticky ? 'sticky top-0 z-50' : '',
            TONE[tone],
            className,
          ]
            .filter(Boolean)
            .join(' ')}
          initial={{ height: 0, opacity: 0 }}
          animate={{ height: 'auto', opacity: 1 }}
          exit={{ height: 0, opacity: 0 }}
          transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
        >
          <div className='relative mx-auto flex max-w-5xl items-center justify-center gap-3 px-4 py-2.5 sm:px-6'>
            <span
              className='pointer-events-none absolute inset-0 bg-[linear-gradient(105deg,transparent_40%,rgba(255,255,255,0.35)_50%,transparent_60%)] bg-size-[200%_100%] opacity-40'
              aria-hidden
            />

            {icon ? (
              <span className='relative shrink-0 text-sm' aria-hidden>
                {icon}
              </span>
            ) : (
              <span
                className={[
                  'relative h-1.5 w-1.5 shrink-0 rounded-full',
                  inverse ? 'bg-white/70' : 'bg-rose-400',
                ].join(' ')}
                aria-hidden
              />
            )}

            <p className='relative text-center text-xs font-medium tracking-wide sm:text-[13px]'>
              {message}
              {action ? (
                <>
                  {' '}
                  <a
                    href={action.href ?? '#'}
                    onClick={action.onClick}
                    className={[
                      'inline-flex items-center gap-1 underline-offset-4 transition-[opacity,transform] hover:underline',
                      inverse ? 'text-white/90' : 'text-gray-900',
                    ].join(' ')}
                  >
                    {action.label}
                    <motion.span
                      aria-hidden
                      className='inline-block'
                      whileHover={{ x: 2 }}
                    >
                      →
                    </motion.span>
                  </a>
                </>
              ) : null}
            </p>

            {dismissible ? (
              <button
                type='button'
                aria-label='Dismiss announcement'
                onClick={() => {
                  setOpen(false);
                  window.setTimeout(() => onDismiss?.(), 320);
                }}
                className={[
                  'absolute right-3 inline-flex h-7 w-7 items-center justify-center rounded-full transition-colors sm:right-4',
                  inverse
                    ? 'text-white/50 hover:bg-white/10 hover:text-white'
                    : 'text-[color:var(--color-muted-foreground,#9ca3af)] hover:bg-[var(--color-muted,#f3f4f6)] hover:text-[color:var(--color-foreground,#374151)]',
                ].join(' ')}
              >
                <svg viewBox='0 0 12 12' className='h-3 w-3' aria-hidden>
                  <path
                    d='M3 3l6 6M9 3L3 9'
                    stroke='currentColor'
                    strokeWidth='1.4'
                    strokeLinecap='round'
                  />
                </svg>
              </button>
            ) : null}
          </div>
        </motion.div>
      ) : null}
    </AnimatePresence>
  );
}

More from uniquel

Browse all components in this library →