Icons
Icons give actions and objects a familiar shape, so people can scan a screen quickly. Nuton uses Remix Icon, coloured with the Icon tokens and always paired with a text name.
Beta. Icon colours are Figma variables (Mapped/Icon/*), light mode only until dark values are designed (TD-11). There are no icon-size tokens: components size icons with interim spacing tokens (pending a Figma home, D9) or Figma type line heights, and the 56px key icon uses a raw 28px glyph. Stable needs icon-size tokens in Figma and the design review of the Lucide-to-Remix migration (D1).
Overview
Remix Icon (@remixicon/react 4.9.0) is Nuton's only icon set (D1). Icons are drawn on a 24px grid, take their colour from currentColor and come in two styles: Line and Fill.
16px · Field hint20px · Buttons, menus, small inputs24px · Large input32px · Key icon 64<RiBookOpenLine style={{ inlineSize: 'var(--nu-space-4)', blockSize: 'var(--nu-space-4)' }} aria-hidden="true" />
<RiBookOpenLine style={{ inlineSize: 'var(--nu-space-5)', blockSize: 'var(--nu-space-5)' }} aria-hidden="true" />
<RiBookOpenLine style={{ inlineSize: 'var(--nu-space-6)', blockSize: 'var(--nu-space-6)' }} aria-hidden="true" />
<RiBookOpenLine style={{ inlineSize: 'var(--nu-space-8)', blockSize: 'var(--nu-space-8)' }} aria-hidden="true" />RiBookOpenLine · LineRiBookOpenFill · FillRiSettings3Line · LineRiSettings3Fill · Fill<RiBookOpenLine aria-hidden="true" /> // Line: the default
<RiBookOpenFill aria-hidden="true" /> // Fill: small status marks- Line is the default. Buttons, inputs and menus in
@nuton/uiuse Line icons. - Fill is for small status marks, where a solid shape reads better at a small size: the Field hint uses
RiInformationFilland Avatar status badges useRiPushpinFill,RiStarFillandRiVerifiedBadgeFill. Guidance on Fill for selected or active states is still to be agreed with design. - Icons are never bundled in
@nuton/ui. Apps import the icons they need and pass them in through props such asleadingIconoricon.
Common icons in Nuton
The icons @nuton/ui uses plus the verified Remix candidates from the Lucide inventory. Every name below is an export of @remixicon/react 4.9.0. For anything else, search the full set and check the export exists before you use it.
50 of 50 icons
RiSparkling2LineAI actions: generate, summariseRiMagicLineAI rewrite, improveRiAddLineCreate a lesson or spaceRiBookOpenLineLessons, readingRiGraduationCapLineCourses, learnersRiFileTextLineDocuments, notesRiPresentationLineSlidesRiFolderLineSpaces, collectionsRiFolderOpenLineOpen spaceRiGroupLineClass, membersRiUser6LineProfile, accountRiHomeLineHomeRiSearchLineSearch, ComboboxRiSettings3LineSettingsRiEditLineEditRiPencilLineRename, writeRiDeleteBinLineDeleteRiShareLineShareRiDownload2LineDownloadRiUpload2LineUpload materialRiFileCopyLineCopyRiSendPlaneLineSend a messageRiMailLineEmail, inviteRiMegaphoneLineAnnouncementsRiCalendarLineDue datesRiTimerLineTimed quizRiBarChartBoxLineProgress, analyticsRiLayoutGridLineGrid viewRiImageLineImagesRiVideoLineVideo lessonsRiMicLineVoice inputRiPlayLineStart, playRiLightbulbLineTips, ideasRiPushpinLinePin; Avatar statusRiLockLinePrivate, lockedRiEyeLineShow passwordRiEyeOffLineHide passwordRiInformationLineField hint (fill)RiErrorWarningLineErrorRiAlertLineWarningRiCheckboxCircleLineDone, successRiCheckLineSelected item in a menuRiCloseLineClose, removeRiMore2LineMore actionsRiRefreshLineRegenerate, retryRiLoader4LineLoadingRiArrowLeftLineBackRiArrowRightLineContinueRiArrowDownSLineSelect chevronRiArrowUpSLineCollapse
Licence
Remix Icon License v1.0: conditions
Remix Icon may be used inside Nuton's products and its design system. The licence forbids a standalone icon pack, so Nuton doesn't publish or re-export the icons as a set: there is no @nuton/icons package (D1, provenance review). Add @remixicon/react as a dependency of your app and import individual icons. Don't copy the SVGs into a shared library, and don't redistribute them on their own.
Migrating from Lucide
Migration in progress (D1)
The teacher app still uses Lucide: 68 distinct icons in 181 places. Each has a Remix candidate that exists in the package, and 23 have more than one plausible match (for example Sparkles → RiSparkling2Line or RiSparklingLine). Every row needs design review; nothing has been swapped yet. Migrate with a semantic and visual check, never by search and replace, and don't mix Lucide and Remix on one screen. The list is in docs/migration/icon-inventory-teacher.md.
Tokens
Generated from @nuton/tokens. Icon colours come from the Figma Mapped collection. Icon sizes have no tokens of their own; components use spacing tokens and type line heights.
Icon colours
| Preview | Token | Value | Source |
|---|---|---|---|
--nu-icon-strong-950 | → gray-950#171717 | FigmaMapped/Icon/Strong-950 | |
--nu-icon-sub-600 | → gray-600#5c5c5c | FigmaMapped/Icon/Sub-600 | |
--nu-icon-soft-400 | → gray-400#a4a4a4 | FigmaMapped/Icon/Soft-400 | |
--nu-icon-disabled-300 | → gray-300#d1d1d1 | FigmaMapped/Icon/Disabled-300 | |
--nu-icon-white | → gray-0#ffffff | FigmaMapped/Icon/White |
Sizes used for icons
| Preview | Token | Value | Source |
|---|---|---|---|
--nu-space-3 | 12px | Interim | |
--nu-space-4 | 16px | Interim | |
--nu-space-5 | 20px | Interim | |
--nu-space-6 | 24px | Interim | |
--nu-space-8 | 32px | Interim | |
--nu-type-label-small-line-height | 20px | FigmaTextStyle/Label/Small | |
--nu-type-paragraph-x-small-line-height | 16px | FigmaTextStyle/Paragraph/X Small |
Spacing tokens are interim: Figma doesn't bind spacing yet (D9). Button, Link button and Button group size their icons with the Label/Small or Paragraph/X Small line height, so the icon matches the text line.
Icon sizes in components
| Slot | Size token | Size | Colour |
|---|---|---|---|
| Field hint and error icon | --nu-space-4 | 16px | Icon/Soft-400 (error: State/Error/Base; disabled: Icon/Disabled-300) |
| Button, Link button (md), Button group (sm, xs) | --nu-type-label-small-line-height | 20px | Inherits the label colour (currentColor) |
| Link button (sm), Button group (2xs) | --nu-type-paragraph-x-small-line-height | 16px | Inherits the label colour (currentColor) |
| Input leading icon, large (48) | --nu-space-6 | 24px | Icon/Sub-600; Icon/Soft-400 while empty; Icon/Strong-950 when invalid |
| Input leading icon, small (36) and inline; trailing icon | --nu-space-5 | 20px | Same as above; disabled: Icon/Disabled-300 |
| Menu item leading and trailing icon, check mark | --nu-space-5 | 20px | Icon/Sub-600; check mark Icon/Strong-950 |
| Combobox and command menu search icon | --nu-space-5 | 20px | Icon/Sub-600 in the command menu |
| Checkbox indicator tick | --nu-space-3 | 12px | Text/White on State/Primary/Base |
| Key icon glyph at 32 and 40 | --nu-space-5 | 20px | State/<colour>/Base (gray: Icon/Sub-600) |
| Key icon glyph at 48 | --nu-space-6 | 24px | As above |
| Key icon glyph at 64 | --nu-space-8 | 32px | As above |
| Key icon glyph at 56 | None (raw value in KeyIcon.css) | 28px | As above |
Read from the component CSS in packages/ui. The 56px key icon is the only icon size without a token; it needs one when icon sizes get a Figma home.
Usage
import { RiAddLine } from '@remixicon/react'
import { Button } from '@nuton/ui'
// Pass the icon to the component. It sizes, colours and hides the icon for you.
<Button leadingIcon={<RiAddLine />}>New lesson</Button>CSS variables
.lesson-meta__icon {
inline-size: var(--nu-space-5);
block-size: var(--nu-space-5);
color: var(--nu-icon-sub-600); /* Remix icons fill with currentColor */
}Tailwind
Both Tailwind outputs expose the icon colours as colour utilities. Sizes aren't exposed as utilities; use the CSS variables.
/* app.css */
@import "tailwindcss";
@import "@nuton/tokens/tokens.css";
@import "@nuton/tokens/tailwind-v4-theme.css";
<RiTimerLine className="text-icon-sub-600" aria-hidden="true" />// tailwind.config.js
module.exports = { presets: [require('@nuton/tokens/tailwind-v3-preset')] }
<RiTimerLine className="text-icon-sub-600" aria-hidden="true" />Choosing a token
--nu-icon-strong-950: icons that carry meaning on their own, and icons next to strong text.--nu-icon-sub-600: the default for icons next to secondary text, in inputs and in menus.--nu-icon-soft-400: decorative icons only, such as an empty input's search icon or a hint icon whose text carries the message. Below 3:1 on white, so never the only cue.--nu-icon-disabled-300: icons inside disabled controls.--nu-icon-white: icons on dark or saturated fills, such as a filled button.- Inside a component, let the icon inherit
currentColor. Set an icon token only for standalone icons.
In use
Real @nuton/ui components. Each sizes and colours its icon from tokens, and hides it from assistive technology because the text names the control.
Buttons
<Button leadingIcon={<RiSparkling2Line />}>Generate quiz</Button>
<Button appearance="stroke" trailingIcon={<RiArrowRightLine />}>Next lesson</Button>
<Button iconOnly appearance="stroke" aria-label="Lesson settings"><RiSettings3Line /></Button>Fields and inputs
Search by title or topic.
Shown to everyone in the space.
<Field label="Find a lesson" hint="Search by title or topic.">
<Input leadingIcon={<RiSearchLine />} placeholder="Photosynthesis" />
</Field>
<Field label="Space name" hint="Shown to everyone in the space." hintIcon={<RiInformationFill />}>
<Input size="sm" defaultValue="Biology 101" />
</Field>Key icons
<KeyIcon icon={<RiBookOpenLine />} /> // decorative
<KeyIcon icon={<RiGraduationCapLine />} color="blue" appearance="lighter" />
<KeyIcon icon={<RiTimerLine />} size={48} label="Timed quiz" /> // meaningful: role="img"Icon-only actions
<Button iconOnly size="sm" appearance="ghost" aria-label="Edit lesson"><RiEditLine /></Button>
<Button iconOnly size="sm" appearance="ghost" aria-label="Share lesson"><RiShareLine /></Button>
<Button iconOnly size="sm" appearance="ghost" aria-label="Download lesson"><RiDownload2Line /></Button>
<Button iconOnly size="sm" appearance="ghost" aria-label="More actions"><RiMore2Line /></Button>Accessibility
Decorative or meaningful
An icon next to text is decorative: hide it with aria-hidden="true" so the name isn't read twice. @nuton/ui does this for every icon prop. An icon that stands alone and carries meaning needs a name: role="img" with an aria-label, or KeyIcon's label.
Icon-only buttons
An icon-only Button or ButtonGroupItem must have an aria-label (or aria-labelledby) that names the action, such as “Share lesson”. Both warn in development when it's missing. Use icon-only only for familiar actions; otherwise show the label.
Contrast (WCAG 1.4.11)
A meaningful icon needs 3:1 against its background. Icon/Strong-950 and Icon/Sub-600 resolve to the same greys as Text/Strong-950 (17.93:1 on white) and Text/Sub-600 (6.69:1 on white, 6.24:1 on Weak-50). Icon/Soft-400 shares Text/Soft-400's grey, 2.49:1 on white (C1), so it's for decorative icons only (C12). Icon/Disabled-300 shares Stroke/Sub-300's grey, 1.53:1 (C7); disabled controls are exempt.
Target size
The smallest icon-only button is 36 × 36, above the 24 × 24 minimum (WCAG 2.5.8). Two known gaps: icon-only Button group items at 2xs sit edge to edge (C26), and Link button has no padding or icon-only mode (C28).
Not by icon alone
Don't rely on an icon's shape or colour to carry a state. An error shows its message, and a selected menu item has a check mark as well as its fill (C18).
Forced colours
Remix icons fill with currentColor, so they follow the system colours in Windows high-contrast mode.
You must: hide decorative icons; name icon-only buttons and meaningful standalone icons; use Icon/Sub-600 or stronger for icons that carry meaning.
Guidelines and related
Use icons to
- Help people scan: a leading icon on a button, menu item or search field.
- Label a familiar action in a tight space, such as edit, share or more actions.
- Mark status next to text, such as a hint, an error or a pinned item.
Don't use icons to
- Replace a label for an uncommon or destructive action.
- Decorate every item in a list; when everything has an icon, none of them help.
- Mix icon families. Remix only (D1), no Lucide or custom SVGs alongside it.