Feedback

Announcement bar

A slim bar for news at the top of a page: one message or several taking turns. Dismissed, it folds shut and can stay gone on the next visit.

Installation

npx sinaui add announcement-bar

Examples

Taking turns

Several messages rise in one after another; the ring round the pause button shows how long until the next. Pointing at the bar or focusing a link holds it. Dismiss it and the page closes up.

Al-Biruni’s Chronology of Ancient Nations is back on the shelves. Borrow it

<script lang="ts">
	import { RefreshIcon } from '@hugeicons/core-free-icons';
	import AnnouncementBar from '#lib/ui/AnnouncementBar.svelte';
	import Button from '#lib/ui/Button.svelte';
	import Icon from '#lib/ui/Icon.svelte';

	let key = $state(0);
	let shown = $state(true);
	// Two days and a few hours from when the page opens, so the countdown always has time left.
	const saleEnds = Date.now() + (2 * 24 + 5) * 3600_000;
</script>

<div class="stage">
	{#key key}
		<AnnouncementBar
			label="Library news"
			interval={5000}
			ondismiss={() => setTimeout(() => (shown = false), 360)}
			messages={[
				{
					text: 'Al-Biruni’s Chronology of Ancient Nations is back on the shelves.',
					link: 'Borrow it',
					href: '#rotating'
				},
				{
					text: 'Copying desks are half price this week.',
					countdown: { to: saleEnds, label: 'Ends in' },
					link: 'Book a desk',
					href: '#rotating'
				},
				{
					text: 'Lectures on optics resume after the harvest.',
					link: 'Timetable',
					href: '#rotating'
				}
			]}
		/>
	{/key}
	<div class="page" aria-hidden="true">
		<span class="line"></span><span class="line short"></span>
	</div>
	{#if !shown}
		<span class="again">
			<Button
				variant="secondary"
				size="sm"
				onclick={() => {
					key++;
					shown = true;
				}}><Icon icon={RefreshIcon} size={14} /> Show again</Button
			>
		</span>
	{/if}
</div>

<style>
	/* A stand-in page: the bar sits across its top, the page closes up when it goes. */
	.stage {
		position: relative;
		display: flex;
		flex-direction: column;
		inline-size: min(40rem, 100%);
		block-size: 9.5rem;
		overflow: hidden;
		border-radius: calc(var(--ui-radius) * 1.5);
		background: var(--ui-surface);
		box-shadow: var(--ui-shadow-card);
	}
	.page {
		display: grid;
		align-content: start;
		gap: 0.625rem;
		padding: 1.25rem;
	}
	.line {
		block-size: 0.5rem;
		inline-size: 42%;
		border-radius: 999px;
		background: var(--ui-subtle);
	}
	.short {
		inline-size: 26%;
	}
	.again {
		position: absolute;
		inset-block-start: 0.875rem;
		inset-inline-end: 0.875rem;
		animation: in var(--ui-dur) var(--ui-ease-out) both;
	}
	@keyframes in {
		from {
			opacity: 0;
			scale: 0.95;
		}
	}
</style>

With a countdown

A live countdown to a moment you give it, its digits rolling as they change. It disappears once the time has passed.

The reading room stays open late tonight. Closes in Oct 1, 2026, 9:43 PM See the hours

<script lang="ts">
	import { RefreshIcon } from '@hugeicons/core-free-icons';
	import AnnouncementBar from '#lib/ui/AnnouncementBar.svelte';
	import Button from '#lib/ui/Button.svelte';
	import Icon from '#lib/ui/Icon.svelte';

	let key = $state(0);
	let shown = $state(true);
	const closes = Date.now() + 5 * 3600_000 + 42 * 60_000;
</script>

<div class="stage">
	{#key key}
		<AnnouncementBar
			label="Reading room hours"
			tone="accent"
			ondismiss={() => setTimeout(() => (shown = false), 360)}
			messages={[
				{
					text: 'The reading room stays open late tonight.',
					countdown: { to: closes, label: 'Closes in' },
					link: 'See the hours',
					href: '#countdown'
				}
			]}
		/>
	{/key}
	<div class="page" aria-hidden="true">
		<span class="line"></span><span class="line short"></span>
	</div>
	{#if !shown}
		<span class="again">
			<Button
				variant="secondary"
				size="sm"
				onclick={() => {
					key++;
					shown = true;
				}}><Icon icon={RefreshIcon} size={14} /> Show again</Button
			>
		</span>
	{/if}
</div>

<style>
	/* A stand-in page: the bar sits across its top, the page closes up when it goes. */
	.stage {
		position: relative;
		display: flex;
		flex-direction: column;
		inline-size: min(40rem, 100%);
		block-size: 9.5rem;
		overflow: hidden;
		border-radius: calc(var(--ui-radius) * 1.5);
		background: var(--ui-surface);
		box-shadow: var(--ui-shadow-card);
	}
	.page {
		display: grid;
		align-content: start;
		gap: 0.625rem;
		padding: 1.25rem;
	}
	.line {
		block-size: 0.5rem;
		inline-size: 42%;
		border-radius: 999px;
		background: var(--ui-subtle);
	}
	.short {
		inline-size: 26%;
	}
	.again {
		position: absolute;
		inset-block-start: 0.875rem;
		inset-inline-end: 0.875rem;
		animation: in var(--ui-dur) var(--ui-ease-out) both;
	}
	@keyframes in {
		from {
			opacity: 0;
			scale: 0.95;
		}
	}
</style>

Remembered

With persist, a dismissal is kept in this browser: reload and it stays gone. Change the key to show a new notice.

Books on the lower shelves are being moved before the river rises.

<script lang="ts">
	import AnnouncementBar from '#lib/ui/AnnouncementBar.svelte';
</script>

<div class="stage">
	<AnnouncementBar
		label="Closure notice"
		tone="warning"
		persist="sinaui-demo-flood-notice"
		messages={[{ text: 'Books on the lower shelves are being moved before the river rises.' }]}
	/>
	<div class="page" aria-hidden="true">
		<span class="line"></span><span class="line short"></span>
	</div>
</div>

<style>
	/* A stand-in page: the bar sits across its top, the page closes up when it goes. */
	.stage {
		position: relative;
		display: flex;
		flex-direction: column;
		inline-size: min(40rem, 100%);
		block-size: 9.5rem;
		overflow: hidden;
		border-radius: calc(var(--ui-radius) * 1.5);
		background: var(--ui-surface);
		box-shadow: var(--ui-shadow-card);
	}
	.page {
		display: grid;
		align-content: start;
		gap: 0.625rem;
		padding: 1.25rem;
	}
	.line {
		block-size: 0.5rem;
		inline-size: 42%;
		border-radius: 999px;
		background: var(--ui-subtle);
	}
	.short {
		inline-size: 26%;
	}
</style>

Props

PropTypeDefault
messages{ text, link?, href?, onclick?, countdown?: { to, label? } }[] (required)
tone'inverted' | 'accent' | 'neutral' | 'warning''inverted'
intervalnumber: milliseconds per message6000
persiststring: remember a dismissal under this key
dismissiblebooleantrue
labelstring: the bar's name for screen readers'Announcements'
ondismiss() => void

Keyboard

KeyDoes
TabMoves to the link, the pause button and Dismiss; focus holds the rotation
Enter SpacePause or play, dismiss

Accessibility

  • A labeled region, so screen reader users can find it or skip it.
  • Changing messages aren't announced; only the one showing can be reached.
  • Rotation stops while pointed at or focused, and the pause button stops it for good.
  • A countdown is read as the moment it ends ("Ends Oct 3, 5:00 PM"), not ticking seconds.
  • With reduced motion, messages swap and the bar closes without sliding.
Introduction
Theming
Changelog
Button
Action button
Confirm button
Copy button
Hold to confirm
Split button
Calendar
Checkbox
Color picker
Combobox
Date picker
Date range picker
File drop
Form
Input
Mention input
Multi select
Number field
OTP input
Password input
Phone input
Radio cards
Radio group
Rich text editor
Search field
Segmented control
Select
Signature pad
Slider
Switch
Tag input
Upload
Wheel picker
Textarea
Time field
Toggle group
Toolbar
Alert dialog
Command palette
Context menu
Dialog
Drawer
Dropdown
Hover card
Popover
Tooltip
Announcement bar
Alert
Empty state
Meter
Progress
Rolling number
Skeleton
Spinner
Toast
Avatar
Avatar group
Badge
Card
Carousel
Chart
Chat
Code block
Data table
Folder
Kbd
Mind map
Resizable panels
Scroll area
Separator
Sortable
Table
Tag group
Virtual list
Audio player
Image
Image compare
Video
Visualizer
Breadcrumb
File tree
Menubar
Navigation menu
Pagination
Sidebar layout
Stepper
Tabs
Alarm clock
Clock
Dynamic island
Fluid orb
Hourglass
Stopwatch
Timer
AI chat
AI media
Auth
Board
Booking
Dashboard
File explorer
Messaging
Notifications
Settings
Team
Overview
Pricing
Agent chat
Calendar
Voice agent
Mail inbox
CMS
Blog
Marketing site
E-commerce
AI agents
SaaS landing page
SaaS app
Accordion
Dark theme
Light theme