Lunia DS

AI output

Citation

A small numbered source marker inside a sentence that opens a preview card with the site, title and snippet, where the card rises out of a light blur on hover or focus and sinks back when the pointer or focus leaves.

Loading preview

A short paragraph about city bike lanes with three markers: two open cards with a site, title and snippet, the third is unavailable. Replay opens the first card, then the second, then the unavailable one.

Installation

Run this in your app after the one-time setup on the Getting started page.

$ npx @ganisigit/lunia-ds add citation

Usage

The demo above, as code.

demo.tsx
'use client';

import * as React from 'react';
import { Citation, type CitationSource } from './citation';

const SOURCES: CitationSource[] = [
  {
    title: 'Protected bike lanes and street safety',
    url: 'https://www.citystreets.example/reports/bike-lanes',
    snippet:
      'Streets with a physical barrier saw far fewer crashes involving people on bikes than streets with paint only.',
  },
  {
    title: 'How many people ride to work, by city',
    url: 'https://www.commutedata.example/ride-share',
    snippet: 'Ride counts rose most in cities that finished a connected network, not single short lanes.',
  },
  {
    title: 'Planning notes from the transport office',
    url: 'https://www.transportoffice.example/notes/2026',
  },
];

// Replay opens the first card, then the second, then the one that is not available.
const SCRIPT: [at: number, open: number | null][] = [
  [400, 0],
  [2400, 1],
  [4400, 2],
  [6400, null],
];

export default function CitationDemo({ replay = 0 }: { replay?: number }) {
  const [open, setOpen] = React.useState<number | null>(null);

  React.useEffect(() => {
    if (replay === 0) return;
    const timers = SCRIPT.map(([at, index]) => window.setTimeout(() => setOpen(index), at));
    return () => {
      for (const t of timers) window.clearTimeout(t);
    };
  }, [replay]);

  const marker = (index: number, status: 'available' | 'unavailable' = 'available') => (
    <Citation
      label={index + 1}
      source={SOURCES[index]}
      status={status}
      open={open === index}
      onOpenChange={(next) => setOpen((now) => (next ? index : now === index ? null : now))}
    />
  );

  return (
    <p className="m-0 w-full max-w-[520px] text-md leading-7 text-secondary">
      Cities that build bike lanes as a connected network see more daily riders {marker(0)}. Lanes that keep a
      physical barrier between bikes and cars also record fewer serious crashes {marker(1)}. A few early plans
      have not been published yet {marker(2, 'unavailable')}.
    </p>
  );
}

API

Parts: root (inline wrapper), marker (link to the source, shows the number), preview card (preview card primitive): site icon, site name, title, snippet, unavailable line (in the card when the source could not be loaded).

PropTypeDefaultDescription
labelstring | number-Marker text, usually the source number, for example 2. Required.
source{ title: string; url: string; snippet?: string; icon?: slot }undefinedThe source the marker links to and the card describes. Without it the citation shows as unavailable.
status"available" | "unavailable""available"Use unavailable when the source could not be fetched.
openDelaynumber120Hover time in ms before the card opens; focus opens it at once.
closeDelaynumber150Time in ms after the pointer leaves before the card closes; blur closes it at once.
openbooleanundefinedControlled state of the card; leave it out to let hover and focus decide.
EventDescription
openChangeThe preview card opened or closed. Payload: the new open state.
SlotDescription
iconSite icon (favicon) in the card; a globe icon by default.

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
CIT1Card opens (openDelay after hover, at once on focus)preview cardopacity, translate y (from the side it opens on), blur0, distance-rise, 2px → 1, 0, 0duration-sm (200)ease-outShows at once
CIT2Card closes (closeDelay after pointer leave, at once on blur or Escape)preview cardopacity, translate y (back to the side it opened on)1, 0 → 0, distance-riseduration-xs (160)ease-exitHides at once
CIT3Hover, or the card opens or closesmarkerbackground color, text colorbg-tertiary, text-tertiary → bg-quaternary, text-primary (open: bg-brand-solid, text-white)duration-xs (160)ease-outSwaps at once

Accessibility

  • The marker is a real link to the source (new tab, rel noreferrer) with an accessible name such as "Source 2: Rainfall records by region"; when unavailable the name ends with ", not available".
  • The card is a preview card, not a tooltip: it adds a visual preview for pointer and keyboard users, holds no focusable content, and Tab moves on to the next link. Everything a screen reader needs is in the marker name, so the card itself is not announced.
  • The card opens on hover after openDelay and at once on keyboard focus, and closes on pointer leave after closeDelay, on blur and on Escape.
  • Unavailable markers use a strike-through plus the words in the name and in the card, never color alone.
  • The marker has a visible focus ring (Focus rings/focus-ring); the card flips above the marker near the bottom of the screen and shifts left near the right edge so it is never cut off.