Citation
Link an answer to its sources with numbered markers and source previews.
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use citations whenever an answer draws on documents, search results or a knowledge base, so people can check a claim without leaving the conversation. Most of the time you don't render them yourself: pass sources to a Chat Message and [1] in the text becomes ChatCitation for sources[0], with ChatSources under the finished answer. Hover or focus a number in the preview to see its source.
import { ChatMessage, type ChatSource } from '@youj/react'
const sources: ChatSource[] = [
{ title: 'Regions and pricing', url: 'https://youj.io/docs/regions', snippet: 'Per-region pricing for functions…' },
]
<ChatMessage role="assistant" content="fra1 is the cheapest at 10M requests a month [1]." sources={sources} />A ChatSource has a title and a url, with an optional snippet for the preview and an icon (a favicon, say) to replace the domain's monogram. Chat Markdown takes the same sources prop.
Description
An assistant answer whose [1] and [2] become citation chips, with its sources listed underneath.
Citations in plain text with a row of sources below; a source that isn't http(s) is shown but not linked.
ChatCitation and ChatSources also work outside a message, anywhere text rests on sources. ChatSources takes a label (default "Sources"). A source whose URL isn't http(s) or mailto: is shown, but not linked and not previewed.
Code
citation-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsindex, source, defaultOpen… (9)
Props
| Prop | Type | |
|---|---|---|
indexRequired | numberThe number in the text, 1-based. | – |
sourceRequired | ChatSource | – |
defaultOpen | booleanOpen the preview from the start (a story, a tour). | – |
ChatSources
| Prop | Type | |
|---|---|---|
sourcesRequired | ChatSource[] | – |
label | stringdefault "Sources" | "Sources" |
Other props pass through: extends ComponentProps<'div'>. | ||
ChatSource
Each entry in sources:
| Prop | Type | |
|---|---|---|
titleRequired | stringNames the source in the preview, the chip and the marker's label. | – |
urlRequired | stringOpens in a new tab. Its domain shows in the preview and the monogram. | – |
snippet | stringA line or two from the source, for the preview. | – |
icon | ReactNodeReplaces the domain's monogram, e.g. a favicon. | – |
MotionPreview opens, preview closes, open and close delays… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Preview opensscale .96 → 1 and fade, out of the chip (yj-pop) | duration.fastease.enter140ms · cubic-bezier(0.16, 1, 0.3, 1) | 140mscubic-bezier(0.16, 1, 0.3, 1) | |
| Preview closes | duration.instantease.exit80ms · cubic-bezier(0.4, 0, 1, 1) | 80mscubic-bezier(0.4, 0, 1, 1) | |
| Open and close delayshover intent, not motion; it stays while the pointer travels onto it | no token 500ms, 250ms | 500ms, 250ms | |
| Chip hoverbackground and text take the accent | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
The preview appears and disappears at once.
Reduced motion
The preview opens and closes without scaling or fading. The hover delays are unchanged, since they're about intent, not motion.
AccessibilityKeyboard, focus
Accessibility
- Each marker is a link that opens its source in a new tab, named by its number and the source's title ("1, Regions and pricing"). The preview is a sighted convenience: the link carries everything.
ChatSourcesis a list named by itslabel. A chip that already shows its title isn't named twice.- Keyboard focus opens the preview like hover does, and Escape closes it.