Lunia DS

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.

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-pill

Usage

The demo above, as code.

demo.tsx
'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.

PropTypeDefaultDescription
badgestring"New"Short word in the badge at the start of the pill.
morestringundefinedA second phrase that folds open after the line on hover and on keyboard focus, for example "See what changed".
hrefstringundefinedWhere the pill links to when there is no card content.
cardWidthnumber360Width of the open card in px; it never grows wider than the view minus 32px.
EventDescription
openChangeFires with true when the card starts to open and false when it starts to close; the source has no such event.
SlotDescription
defaultThe line of news inside the pill; plain text is also used as the card's accessible name.
cardWhat 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.

IDTriggerElementPropertyFrom → toTimingReduced motion
AP1hover the pillpillinset border colorborder-secondary → border-primaryduration-lg (320)ease-outSwaps at once
AP2hover the pillarrowtransform translateX + color0, fg-quaternary → 2px, fg-primaryduration-lg (320)ease-outSwaps at once
AP3hover or keyboard focus on the pillmore phrasemax-width + opacity0, 0 → measured width, 1duration-4xl (520)ease-outShows at once
AP4hover or focus leaves the pillmore phrasemax-width + opacitymeasured width, 1 → 0, 0duration-4xl (520)ease-outHides at once
AP5press the pill (open)card surfacewidth + height + border-radiuspill size, half the pill height → card size, radius-3xlduration-2xl (420)ease-outResizes at once
AP6press the pill (open)pill line copyopacity1 → 0duration-2xs (120)ease-exitHides at once
AP7press the pill (open)card contentopacity0 → 1duration-md (260)ease-exitdelay 140msShows at once
AP8press the pill (open)card contenttransform translateY-distance-nudge → 0duration-2xl (420)ease-outMoves at once
AP9close button, Escape, press outside or press the pill againcard surfacewidth + height + border-radiuscard size, radius-3xl → pill size, half the pill heightduration-2xl (420)ease-outResizes at once
AP10closecard contentopacity1 → 0duration-2xs (120)ease-exitHides at once
AP11closepill line copyopacity0 → 1duration-sm (200)ease-exitdelay 180msShows at once
AP12hover the close buttonclose buttonbackground-color + colortransparent, fg-quaternary → bg-primary_hover, fg-primaryduration-xs (160)ease-outSwaps 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.