# Banner 1

> A dismissible announcement bar that remembers the choice

A shadcn/ui block from the [@shadcnstore](https://www.shadcnstore.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcnstore/banner-1).

- Page, with a live preview: https://shadcn.exchange/r/shadcnstore/banner-1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcnstore/banner-1-light-882888bc3eb45c93.png
- Registry item: https://shadcnstore.com/r/banner-1.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcnstore.com/r/banner-1.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `motion`
- Registry items: `@shadcnstore/motion-avatar-group`, `avatar`, `badge`, `button`

## Files

### `storage/app/registry-sources/marketing/banners/banner-1/page.tsx`

```tsx
import { Banner1 } from "@/components/blocks/marketing/banners/banner-1/components/banner-1"

export default function Page() {
  return (
    <div className="min-h-svh w-full">
      <Banner1 />
    </div>
  )
}
```

### `storage/app/registry-sources/marketing/banners/banner-1/banner-1.tsx`

```tsx
'use client'

import { useEffect, useState } from 'react'
import { useReducedMotion } from 'motion/react'
import { ArrowRight, X } from 'lucide-react'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { AvatarGroup } from '@/components/motion/avatar-group'
import { announcement, announcementAvatars } from "@/components/data/banner-1-data"

/**
 * 'bar'  — full-width strip above the header (the default).
 * 'pill' — floating rounded card pinned to the bottom of the viewport, with an avatar stack.
 * Both shapes share the same data file.
 */
const shape: 'bar' | 'pill' = 'bar'

type Visibility = 'visible' | 'closing' | 'closed'

export function Banner1() {
  /**
   * Dismissal lives in component state only, so a reload brings the banner back. To keep a
   * visitor's dismissal between visits, persist it (localStorage, a cookie, or the account)
   * and read that back here before the first paint.
   */
  const [visibility, setVisibility] = useState<Visibility>('visible')
  const reduceMotion = useReducedMotion()

  useEffect(() => {
    if (visibility !== 'closing') {
      return
    }

    const timer = window.setTimeout(() => setVisibility('closed'), 200)
    return () => window.clearTimeout(timer)
  }, [visibility])

  const dismiss = () => {
    setVisibility(reduceMotion ? 'closed' : 'closing')
  }

  if (visibility === 'closed') {
    return null
  }

  const closing = visibility === 'closing'

  if (shape === 'pill') {
    return (
      <div
        className={`fixed inset-x-4 bottom-4 z-50 transition-all duration-200 ease-out motion-reduce:transition-none sm:inset-x-auto sm:left-1/2 sm:-translate-x-1/2 ${
          closing ? 'translate-y-2 opacity-0' : 'opacity-100'
        }`}
      >
        <div className='bg-background/95 border-border flex items-center gap-3 rounded-full border py-2 pr-2 pl-3 shadow-lg backdrop-blur sm:pl-4'>
          <AvatarGroup aria-hidden='true'>
            {announcementAvatars.map(src => (
              <Avatar key={src} className='ring-background size-6 ring-2'>
                <AvatarImage src={src} alt='' />
                <AvatarFallback />
              </Avatar>
            ))}
          </AvatarGroup>
          <p className='truncate text-sm'>{announcement.message}</p>
          <a
            href={announcement.href}
            className='hover:text-foreground text-muted-foreground focus-visible:ring-ring hidden shrink-0 items-center gap-1 rounded-sm text-sm font-medium transition-colors duration-150 focus-visible:ring-2 focus-visible:outline-none sm:inline-flex'
          >
            {announcement.linkLabel}
            <ArrowRight className='size-3.5' />
          </a>
          <Button
            variant='ghost'
            size='icon'
            className='size-8 shrink-0 rounded-full'
            aria-label='Dismiss announcement'
            onClick={dismiss}
          >
            <X className='size-4' />
          </Button>
        </div>
      </div>
    )
  }

  return (
    <div
      className={`grid transition-all duration-200 ease-out motion-reduce:transition-none ${
        closing ? 'grid-rows-[0fr] opacity-0' : 'grid-rows-[1fr] opacity-100'
      }`}
    >
      <div className='overflow-hidden'>
        {/* Neutral alternative: swap for `bg-muted text-foreground border-b border-border`. */}
        <div className='bg-primary text-primary-foreground'>
          <div className='mx-auto flex w-full max-w-7xl items-center gap-3 px-4 py-2.5 sm:px-6 lg:px-8'>
            {announcement.badge ? (
              <Badge
                variant='secondary'
                className='hidden shrink-0 rounded-full px-2 py-0 text-xs font-semibold sm:inline-flex'
              >
                {announcement.badge}
              </Badge>
            ) : null}

            <p className='min-w-0 flex-1 truncate text-sm'>{announcement.message}</p>

            <a
              href={announcement.href}
              className='focus-visible:ring-ring inline-flex shrink-0 items-center gap-1 rounded-sm text-sm font-medium underline-offset-4 transition-opacity duration-150 hover:opacity-80 hover:underline focus-visible:ring-2 focus-visible:outline-none'
            >
              <span className='hidden sm:inline'>{announcement.linkLabel}</span>
              <span className='sr-only sm:hidden'>{announcement.linkLabel}</span>
              <ArrowRight className='size-3.5' aria-hidden='true' />
            </a>

            <Button
              variant='ghost'
              size='icon'
              className='hover:bg-primary-foreground/10 hover:text-primary-foreground size-8 shrink-0'
              aria-label='Dismiss announcement'
              onClick={dismiss}
            >
              <X className='size-4' />
            </Button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Banner1
```

### `storage/app/registry-sources/marketing/banners/banner-1/banner-1-data.ts`

```ts
export interface Announcement {
  /**
   * Dismissal is persisted under this id.
   * Change it and everyone who dismissed the previous announcement sees the new one.
   */
  id: string
  badge?: string
  message: string
  href: string
  linkLabel: string
}

export const announcement: Announcement = {
  id: 'workflows-2-launch',
  badge: 'New',
  // Keep this to one line: the bar must never wrap on any breakpoint.
  message: 'Workflows 2.0 is live — branching, retries and scheduled runs.',
  href: '#announcement',
  linkLabel: 'Read the post',
}

/** Used by the pill shape only; decorative, so every alt is empty. */
export const announcementAvatars = [
  'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/sarah-johnson.400w.01a6ba.avif',
  'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/michael-rodriguez.400w.d8234b.avif',
  'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/emily-chen.400w.8fb3cf.avif',
]
```
