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-barCopy src/lib/ui/AnnouncementBar.svelte, with Icon.svelte, RollingNumber.svelte, motion.ts, stored.ts and tokens.css. No dependencies.
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.
<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.
<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.
<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
| Prop | Type | Default |
|---|---|---|
messages | { text, link?, href?, onclick?, countdown?: { to, label? } }[] (required) | |
tone | 'inverted' | 'accent' | 'neutral' | 'warning' | 'inverted' |
interval | number: milliseconds per message | 6000 |
persist | string: remember a dismissal under this key | |
dismissible | boolean | true |
label | string: the bar's name for screen readers | 'Announcements' |
ondismiss | () => void |
Keyboard
| Key | Does |
|---|---|
| Tab | Moves to the link, the pause button and Dismiss; focus holds the rotation |
| Enter Space | Pause 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.