Marketing
Announcement pill
A small pill above a page headline that names one piece of news, opens a second phrase on hover, and grows into a card with the details when pressed.
Live preview
Loading preview
A pill that reads "New" + "Version 2.4 is out" with the more phrase "See what changed", centered above a page headline. Pressing it grows the pill into a 360px card with three short release notes and a "Read the release notes" link; Replay closes the card and opens it again after a short pause.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add announcement-pillUsage
The demo above, as code.
'use client';
import * as React from 'react';
import { AnnouncementPill } from './announcement-pill';
const NOTES = [
'Search now finds words inside your files.',
'Shared folders sync in the background.',
'Dark mode follows your system setting.',
];
export default function AnnouncementPillDemo({ replay = 0 }: { replay?: number }) {
const root = React.useRef<HTMLDivElement>(null);
// Replay closes the card if it is open, then opens it again after a short pause so the grow can be seen.
React.useEffect(() => {
if (replay === 0) return;
const pill = () => root.current?.querySelector<HTMLElement>('button[data-slot="pill"]');
if (pill()?.getAttribute('aria-expanded') === 'true') pill()?.click();
const timer = window.setTimeout(() => pill()?.click(), 1100);
return () => window.clearTimeout(timer);
}, [replay]);
return (
<div ref={root} className="flex min-h-[300px] w-full max-w-[640px] flex-col items-center gap-6 pt-6">
<AnnouncementPill
more="See what changed"
card={
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2 pe-8">
<span className="inline-flex items-center rounded-full border border-utility-brand-200 bg-utility-brand-50 px-2 py-0.5 text-xs font-medium text-utility-brand-700">
New
</span>
<span className="text-xs text-tertiary">Released Oct 12</span>
</div>
<h3 className="m-0 text-md font-semibold text-primary">Version 2.4 is out</h3>
<ul className="m-0 flex list-none flex-col gap-2 p-0">
{NOTES.map((note) => (
<li key={note} className="flex items-start gap-2 text-sm text-secondary">
<svg
aria-hidden="true"
viewBox="0 0 16 16"
fill="none"
stroke="var(--lunia-fg-success-primary)"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="mt-0.5 size-4 shrink-0"
>
<path d="m3.5 8.5 3 3 6-7" />
</svg>
{note}
</li>
))}
</ul>
<a
href="#release-notes"
className="inline-flex w-fit items-center gap-1 text-sm font-semibold text-brand-secondary"
>
Read the release notes
<svg
aria-hidden="true"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="size-4"
>
<path d="M3.5 8h9M8.75 4.25 12.5 8l-3.75 3.75" />
</svg>
</a>
</div>
}
>
Version 2.4 is out
</AnnouncementPill>
<h2 className="m-0 text-display-xs font-semibold text-primary">Build things that last</h2>
</div>
);
}
API
Parts: root, pill (link without a card, button with a card), badge, line, more phrase (folds open on hover and focus), arrow, card surface (grows out of the pill), pill line copy (fades out as the card grows), card content, close button.
| Prop | Type | Default | Description |
|---|---|---|---|
| badge | string | "New" | Short word in the badge at the start of the pill. |
| more | string | undefined | A second phrase that folds open after the line on hover and on keyboard focus, for example "See what changed". |
| href | string | undefined | Where the pill links to when there is no card content. |
| cardWidth | number | 360 | Width of the open card in px; it never grows wider than the view minus 32px. |
| Event | Description |
|---|---|
| openChange | Fires with true when the card starts to open and false when it starts to close; the source has no such event. |
| Slot | Description |
|---|---|
| default | The line of news inside the pill; plain text is also used as the card's accessible name. |
| card | What the pill opens into; without it the pill is a plain link. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| AP1 | hover the pill | pill | inset border color | border-secondary → border-primary | duration-lg (320)ease-out | Swaps at once |
| AP2 | hover the pill | arrow | transform translateX + color | 0, fg-quaternary → 2px, fg-primary | duration-lg (320)ease-out | Swaps at once |
| AP3 | hover or keyboard focus on the pill | more phrase | max-width + opacity | 0, 0 → measured width, 1 | duration-4xl (520)ease-out | Shows at once |
| AP4 | hover or focus leaves the pill | more phrase | max-width + opacity | measured width, 1 → 0, 0 | duration-4xl (520)ease-out | Hides at once |
| AP5 | press the pill (open) | card surface | width + height + border-radius | pill size, half the pill height → card size, radius-3xl | duration-2xl (420)ease-out | Resizes at once |
| AP6 | press the pill (open) | pill line copy | opacity | 1 → 0 | duration-2xs (120)ease-exit | Hides at once |
| AP7 | press the pill (open) | card content | opacity | 0 → 1 | duration-md (260)ease-exitdelay 140ms | Shows at once |
| AP8 | press the pill (open) | card content | transform translateY | -distance-nudge → 0 | duration-2xl (420)ease-out | Moves at once |
| AP9 | close button, Escape, press outside or press the pill again | card surface | width + height + border-radius | card size, radius-3xl → pill size, half the pill height | duration-2xl (420)ease-out | Resizes at once |
| AP10 | close | card content | opacity | 1 → 0 | duration-2xs (120)ease-exit | Hides at once |
| AP11 | close | pill line copy | opacity | 0 → 1 | duration-sm (200)ease-exitdelay 180ms | Shows at once |
| AP12 | hover the close button | close button | background-color + color | transparent, fg-quaternary → bg-primary_hover, fg-primary | duration-xs (160)ease-out | Swaps at once |
Accessibility
- Without card content the pill is a link; with card content it is a button with aria-expanded and aria-controls pointing at the card.
- The card has role dialog, named by the pill's line when it is plain text, otherwise "Announcement"; it is not modal and the page stays usable.
- Opening moves focus to the first link or button in the card; Escape and the close button return focus to the pill; a press outside closes the card without moving focus.
- The card content is inert while the card is closed or closing, and the copy of the pill line inside the card is aria-hidden.
- The more phrase opens on keyboard focus as well as on hover, so keyboard users see the same text.
- The close button is named "Close" and has a visible focus ring, like the pill.
- Reduced motion: the card opens and closes at once, with no resize, fade or slide; the more phrase shows and hides at once.