Sources
Sources show where an answer or lesson comes from: citation cards, a stack of site icons, and file rows. Use them next to AI answers and lesson content.
Beta until design confirms the code-only focus, pressed and disabled styles, the favicon fallback and the selected-row behaviour, and the components are checked in a browser at 375px.
Preview
Pick a component and change its options. The favicons here are neutral placeholders made by the docs; apps pass real favicon URLs.
import { SourceCard } from '@nuton/ui'
<SourceCard
href="https://openbiology.example/respiration"
title="Cellular respiration: how cells release energy"
site="openbiology.example"
favicon={faviconUrl}
excerpt="Cellular respiration turns glucose and oxygen into usable energy (ATP), water and carbon dioxide. Most of it happens in the mitochondria."
/>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages. They aren't published to a registry yet (D4: registry deferred; D12: distribution waits on the provenance review). Inside the Nuton monorepo, add them as workspace dependencies. The commands below apply once the packages are published.
1. Add the packages
pnpm add @nuton/ui @nuton/tokensnpm install @nuton/ui @nuton/tokensyarn add @nuton/ui @nuton/tokensPeer dependencies: react and react-dom 18 or later. No brand images ship with the package: your app supplies favicons (D12).
2. Load tokens and component styles once
// app/layout.tsx (or your app's entry file)
import '@nuton/tokens/tokens.css' // --nu-* design tokens
import '@nuton/ui/styles.css' // component styles (uses the tokens)SourceItem uses the FileFormatIcon styles, which ship in the same stylesheet.
Usage
import { SourceCard, SourceItem, SourceStack } from '@nuton/ui'
export function AnswerSources({ sources, onOpen }) {
return (
<>
<SourceStack sources={sources} onClick={onOpen} />
{sources.map((s) => (
<SourceCard key={s.url} href={s.url} title={s.title} site={s.domain} favicon={s.faviconUrl} excerpt={s.quote} />
))}
<SourceItem name="Form and Function in Design.pdf" onClick={() => openFile('form-and-function')} />
</>
)
}- A
SourceCardlinks from its title, which covers the whole card. It opens in a new tab by default and says so to screen readers; setnewTab={false}for sources inside Nuton. SourceStacksummarises many sources in one small control. Give itonClickto open the full list, and wirearia-expandedwhen it shows and hides them.SourceItemis a file row. Passselectedwhen the row picks which file is shown, so it reads as a toggle.- Pass favicons as URLs from your source data. When one is missing or fails, a neutral globe shows instead.
- Put cards and items in a list (
<ul>) so people hear how many there are.
Variants and sizes
Three components, each with one size.
Source card
<SourceCard
href="https://openbiology.example/respiration"
title="Cellular respiration: how cells release energy"
site="openbiology.example"
favicon={faviconUrl}
excerpt="Cellular respiration turns glucose and oxygen into usable energy (ATP)…"
/>Source stack
<SourceStack sources={sources} total={13} /> // 4 icons + "+9"
<SourceStack sources={sources.slice(0, 3)} /> // 3 icons, no count
<SourceStack sources={sources} total={13} onClick={openSources} /> // buttonSource item
<SourceItem name="Form and Function in Design.pdf" />
<SourceItem name="Lecture 3 – Colour theory.pptx" />
<SourceItem name="Reading list.docx" />Sizes
| Part | Size | Token or source |
|---|---|---|
| Source card | Padding 16, radius 16, gap 4, 1px border | --nu-source-card-pad, --nu-radius-16 |
| Card favicon and site | 16 circle with a 2px white ring; site Label/X Small 12/20, gap 6 | --nu-source-favicon-size, --nu-type-label-x-small-* |
| Card title and excerpt | Label/Small 14/20; Paragraph/Small 14/24, 2 lines | --nu-type-label-small-*, --nu-type-paragraph-small-* |
| Source stack | Padding 4, 20 icons overlapping by 4, “+N” chip 20 wide (grows for two digits). 28 tall. | --nu-source-stack-size, --nu-source-stack-overlap |
| Source item | Padding 4, gap 8, radius 8; 24 icon tile with a 20 badge; name Label/Small. 32 tall. | --nu-source-item-*, FileFormatIcon size={24} |
Values from the component CSS, which follows Figma's geometry (References 3852:44840, Links 3850:35433, source file item 3842:32371).
States and interactions
onClick it is a labelled image, not a control.aria-pressed="true".State tokens
| Token | Value | Used for |
|---|---|---|
--nu-source-card-bg / -bg-hover | Bg/White / Bg/Weak-50 | Card, hover and pressed |
--nu-source-item-bg-hover | Bg/Weak-50 | Item hover and pressed |
--nu-source-item-bg-selected | Bg/Soft-100 | Item active (selected) |
| Stack hover | Bg/Soft-200 | Clickable stack, hover and pressed |
Composition examples
AI answer with sources
The stack sits under the answer. Pressing it shows the source cards and sets aria-expanded, so screen readers know the list opened.
Lesson source files
A list of the files a lesson was built from. Choosing one shows it; the chosen row is pressed.
API reference
SourceCard props
| Prop | Type | Default | Description |
|---|---|---|---|
hrefrequired | string | — | URL of the source. |
titlerequired | ReactNode | — | Source title. Names the link. |
site | ReactNode | — | Site name or domain above the title. Part of the description. |
favicon | string | — | Favicon URL. Decorative. A globe shows when it is missing or fails. |
excerpt | ReactNode | — | Quote or summary. Part of the description. |
excerptLines | number | 2 | Lines before the ellipsis (1–4). 0 shows all. |
newTab | boolean | true | Opens in a new tab with rel="noopener noreferrer" and a hidden hint. |
labels | Partial<SourceCardLabels> | { newTab: '(opens in a new tab)' } | Overrides for the hint text. |
ref | Ref<HTMLAnchorElement> | — | Forwarded to the link. |
…rest | Omit<AnchorHTMLAttributes, 'title' | 'children'> | — | Any other link attribute. target and rel apply only with newTab={false}. |
SourceStack props
| Prop | Type | Default | Description |
|---|---|---|---|
sourcesrequired | readonly { favicon?: string; name?: string; id?: string }[] | — | The sources. The first max favicons show. |
max | number | 4 | Favicons shown before the "+N" count. |
total | number | sources.length | Total count when it is more than the sources passed. |
onClick | MouseEventHandler<HTMLButtonElement> | — | Makes the stack a button. Without it, it is role="img". |
labels | Partial<SourceStackLabels> | { count: n => 'n sources', more: 'more' } | The accessible name: the total, then 'more' after the visible +N on a button. |
ref | Ref<HTMLElement> | — | Forwarded to the button or span. |
…rest | Omit<ButtonHTMLAttributes, 'children'> | — | Spread on the root, e.g. aria-expanded and aria-controls. |
SourceItem props
| Prop | Type | Default | Description |
|---|---|---|---|
namerequired | string | — | File name. Names the button, sets the tooltip and the format icon. |
format | FileFormatType | from name | Format icon type (pdf, doc, link…). |
selected | boolean | — | Figma Active. Sets aria-pressed. Leave undefined for a plain action row. |
trailing | ReactNode | — | Extra text after the name, e.g. "12 pages". Part of the name. |
disabled | boolean | false | Native disabled (code-only style). |
ref | Ref<HTMLButtonElement> | — | Forwarded to the button. |
…rest | Omit<ButtonHTMLAttributes, 'children'> | — | Any other button attribute. |
Styling hooks
.nu-source-card (__site, __title, __excerpt[data-lines]), .nu-source-stack ([data-interactive], __more), .nu-source-item([data-selected], __name) and the shared .nu-source-favicon. Override with the --nu-source-* tokens.
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves to each card, clickable stack and item. |
| Enter | Opens a card link; activates a stack or item. |
| Space | Activates a stack or item (buttons). |
Screen readers
A card's title is the link, stretched over the whole card, so its name is exactly the visible title plus “(opens in a new tab)”; the site and excerpt describe it. A stack image is “13 sources”. A stack button is “13 sources, +9 more”, so the visible “+9” is part of its name for voice control (WCAG 2.5.3). An item is a button named by the file name; with selected it is announced as pressed or not pressed. Favicons and format icons are decorative.
Focus
A 2px primary outline. Cards and stacks put it 2px outside; item rows draw it inside, so rows in a tight list don't overlap.
Contrast and colour
Titles are 17.9:1, site names and excerpts 6.7:1 on white (6.2:1 on the hover fill). The “+N” text is 6.2:1 on Bg/Weak-50. Selection is also exposed as aria-pressed, not only by the fill.
Touch and pointer
Cards are large targets. The stack is 28 tall and items 32 tall, above the 24px minimum. Hover styles apply only on hover-capable pointers.
You must: pass a real title for every card (not the URL); keep aria-expanded in sync when a stack shows and hides sources; translate labels, including the new-tab hint and the source count.
Figma mapping
| Figma component | Figma values | Code |
|---|---|---|
| References · Property 1 | Default · Variant2 | SourceCard; Variant2 is the hover fill |
| Links · Property 1 | Default · Hover | SourceStack; hover only with onClick |
| Source file item · Property 1 | Default · Hover · Active | SourceItem; Active is selected |
| File Format (PDF) | 24 tile, 20 badge | FileFormatIcon size={24} with a 20 badge |
Guidelines and related components
Use sources for
- Citations behind an AI answer or a generated lesson.
- The files and links a lesson or space was built from.
Don't use them for
- Uploading files. Use File upload.
- General navigation links. Use a text link or Link button.
Do and don't
Common mistakes
- Putting buttons or other links inside a
SourceCard. The title link covers the whole card. - Giving a stack an
onClickbut no visible result, or not updatingaria-expanded. - Using
selectedon rows that open a file in a new view instead of choosing what is shown. - Shipping brand logos as favicons in the design system. Apps fetch them from the source.