Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 30 additions & 70 deletions docs/contributor-docs/adding-icons.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,92 +4,52 @@ category: Contributor Guides
order: 9
---

## Adding and Modifying Icons
## Lucide Icons

- Use dashes in the name of the .svg files (e.g `calendar-month`).
- Use the same name for the "Line" and "Solid" variants, and save them in the respective folder, e.g. `instructure-ui/packages/ui-icons/svg/Line/calendar-month.svg` and `instructure-ui/packages/ui-icons/svg/Solid/calendar-month.svg`.
- Double-check that the SVG size is 1920x1920.
The bulk of the icon set comes from [Lucide](https://lucide.dev). These are not manually maintained
— they are automatically picked up from the `lucide-react` npm package at build time.

```js
---
type: code
---
<svg
width="1920"
height="1920"
viewBox="0 0 1920 1920"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{...}
</svg>
```

- The files cannot contain [clipping paths](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath)! Sadly, when the Designers export icons from Figma, most of the time they have a clipping path around the whole canvas. When an SVG includes clipping paths, the `Icon Font` variant may not render correctly. Specifically, the use of `<g clip-path="...">` and `<clipPath id="...">` elements can cause rendering issues. If the source code has them, manually refactor the code, e.g:

```js
---
type: code
---
**To get a new or updated Lucide icon**, bump the `lucide-react` version in
`packages/ui-icons/package.json`. The index is regenerated automatically as part of
`pnpm run bootstrap` (via `build-icons`), so no manual step is needed.

Every icon exported by `lucide-react` becomes `<LucideIconName>InstUIIcon` in `@instructure/ui-icons`,
**except** for icons that are shadowed by a custom icon of the same name (see below).

If a Lucide icon is missing or looks wrong, check whether it exists in the installed version of
`lucide-react` first — if not, the only path is to add it as a custom icon.

// Before:
<svg
width="1920"
height="1920"
viewBox="0 0 1920 1920"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_1007_24)">
<path d="..." fill="#2D3B45" />
</g>
<defs>
<clipPath id="clip0_1007_24">
<rect width="1920" height="1920" fill="white" />
</clipPath>
</defs>
</svg>
## Adding Custom Icons

// After:
<svg
width="1920"
height="1920"
viewBox="0 0 1920 1920"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="..." fill="#2D3B45" />
</svg>
```
Custom icons live in `packages/ui-icons/svg/Custom/` and are consumed directly by the build script (`ui-scripts/lib/generate-custom-index.ts`).

- If the icon has to be bidirectional (being mirrored in RTL mode, typically arrow icons), add the icon name to the bidirectional list in `packages/ui-icons/icons.config.js`. Deprecated icons are handled here as well.
- Use kebab-case filenames ending in `.svg`. The filename becomes the React export name: `ai-info.svg` → `AiInfoInstUIIcon`, `canvas-logo.svg` → `CanvasLogoInstUIIcon`.

- Run `pnpm run bootstrap`.
- For solid/filled icons, the filename must end in `-solid.svg` (e.g. `bell-solid.svg`).

- Finally, run `pnpm run dev` and verify that the icons are displayed correctly under [Icons](/#icons). Check all 3 versions (React, SVG and icon font).
- If a custom icon has the same name as a Lucide icon (e.g. `message-square-check.svg`), the custom version takes precedence and the Lucide one is hidden from the package.

(Note: The fonts are sometimes not rendered correctly, but we decided not to fix them, because they are not really used anywhere, and we might stop supporting icon fonts in the future in general.)
- After dropping the file into `svg/Custom/`, the index is regenerated automatically as part of
`pnpm run bootstrap` (via `build-icons`).

### Guidelines for Drawing Icons
- Run `pnpm run dev` and verify the icon looks correct in the Icons gallery.

- Draw your icons on the 1920 x 1920 art-boards.
### Drawing Guidelines

- Before you flatten shapes or vectorize strokes as described below, make a hidden copy of the original paths off
to the side so that you can more easily come back and make changes later.
- Uncheck "Clip content" on the frame before exporting. Otherwise Figma wraps every layer in `<g clip-path="url(#…)">` and adds a `<defs><clipPath>…</clipPath></defs>` block, which can cause rendering issues

- Flatten your shapes.
- Use `currentColor` for all path fills and strokes. The build script reads the SVG as-is — no color replacement happens. If you exported with a hardcoded hex value, replace it manually before regenerating the index

- Export strokes to vector.
- Stroke icons: set `fill="none"` on every path, not just on the root `<svg>`. Select all shape layers and set Fill to None in the Design panel

- Don’t use borders on vectors, especially not inside/outside borders which aren’t supported in SVG. Do not use clipping paths.
- Remove `width` and `height` from the `<svg>` root — keep only `viewBox` and `xmlns`. Export at 1× in Figma

- Make sure none of the paths go outside of the art-board. If so, the glyph in the icon font will be misaligned.
Draw inside the lines.
- Flatten all transforms before exporting (Object → Flatten Selection)

- Fill the space edge-to-edge as much as possible. The build process will add margins as needed.
- Standard icons use `viewBox="0 0 24 24"`. Brand/logo icons can use any square viewBox (e.g. `0 0 1920 1920`)

- Don't use inline styles
- Mixed stroke + fill icons are supported. Paths with `fill="currentColor"` render filled; paths with `fill="none"` and a `stroke` render as outlines

- Don't use `class` or `for` attributes
- Do not use per-element `stroke-width`. The wrapper applies a uniform stroke width derived from the icon size

- Always have `<svg>` as the root tag
- Do not use `<mask>` or `<use>` elements. These are not supported — flatten or redesign the layer
9 changes: 4 additions & 5 deletions docs/guides/upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,24 @@ TODO add details

## New icons

InstUI has switched to a new icon set, [Lucide](https://lucide.dev/icons/). We are still keeping some Instructure-specific icons, like product logos. We have a codemod that will help you migrate your code to the new icon set (see below).
InstUI has switched to a new icon set based on [Lucide](https://lucide.dev/icons/). We are still keeping some Instructure-specific icons, like product logos, also new custom icons are added. We have a codemod that will help you migrate your code to the new icon set.

### Lucide Icons Package

InstUI v12 introduces a new icon package **`@instructure/ui-icons-lucide`** based on the [Lucide](https://lucide.dev/icons/) icon library, providing 1,700+ icons with improved theming and RTL support. The new Lucide icons are wrapped with `wrapLucideIcon` to integrate with InstUI's theming system while maintaining access to all native Lucide props.
InstUI introduces new icons based on the [Lucide](https://lucide.dev/icons/) icon library, providing 1,900+ stroke-based icons with improved theming and RTL support. The icons are wrapped with `wrapLucideIcon` to integrate with InstUI's theming system while maintaining access to all native icon props.

**Key differences from `SVGIcon`/`InlineSVG`:**

| Property | Old API (SVGIcon) | New API (Lucide) |
| Property | Old API (SVGIcon) | New API |
| :-------------- | :---------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- |
| **size** | `'x-small'` \| `'small'` \| `'medium'` \| `'large'` \| `'x-large'` | `'xs'` \| `'sm'` \| `'md'` \| `'lg'` \| `'xl'` \| `'2xl'` \| `number` (pixels) |
| **color** | Limited tokens: `'primary'` \| `'secondary'` \| `'success'` \| `'error'` \| `'warning'` \| etc. | 60+ theme tokens (`'baseColor'`, `'successColor'`, `'actionPrimaryBaseColor'`, etc.) or any CSS color |
| **strokeWidth** | ❌ Not available | `'xs'` \| `'sm'` \| `'md'` \| `'lg'` \| `'xl'` \| `'2xl'` \| `number` \| `string` |
| **children** | `React.ReactNode` | ❌ Removed |
| **focusable** | `boolean` | ❌ Removed |
| **description** | `string` (combined with title) | ❌ Removed (use `title` only) |
| **src** | `string` | ❌ Removed |

The new icons automatically sync with theme changes, support all InstUI color tokens, and provide better TypeScript integration. All standard HTML and SVG attributes can be passed directly to Lucide icons and will be spread onto the nested SVG element. Existing `@instructure/ui-icons` package remains available for legacy Instructure-specific icons.
The new icons automatically sync with theme changes, support all InstUI color tokens, and provide better TypeScript integration. All standard HTML and SVG attributes can be passed directly to Lucide icons and will be spread onto the nested SVG element.

## Focus rings

Expand Down
110 changes: 110 additions & 0 deletions docs/patterns/UsingIcons.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,116 @@ relevantForAI: true

## Using Icons

Icons from `@instructure/ui-icons` are available as `<Name>InstUIIcon` components — browse them
in the [Icons gallery](#icons). Legacy class-component icons (`IconHeartLine`, `IconSearchLine`,
etc.) are still available for backwards compatibility but are deprecated — see
[Legacy Icons](#legacy-icons) below. Components with the new theming system only accept new icons.

### Accessibility

Without a `title` prop the icon is decorative: `aria-hidden="true"` and `role="presentation"` are
set automatically. When a `title` is provided, the icon becomes meaningful: `aria-label` is set to
the title value and `role="img"` is added.

```js
---
type: example
---
<View as="div" margin="small" padding="medium" background="primary">
<Text as="div" size="large">I <HeartInstUIIcon size="md" color="errorColor" title="Love" /> New York</Text>
</View>
```

### Size

Use the `size` prop with a semantic size token. Stroke width scales automatically with the size.

```js
---
type: example
---
<View as="div" margin="small" padding="medium" background="primary">
<Flex wrap="wrap" alignItems="center" gap="small">
<Flex.Item><Text>xs</Text> <SearchInstUIIcon size="xs" /></Flex.Item>
<Flex.Item><Text>sm</Text> <SearchInstUIIcon size="sm" /></Flex.Item>
<Flex.Item><Text>md</Text> <SearchInstUIIcon size="md" /></Flex.Item>
<Flex.Item><Text>lg</Text> <SearchInstUIIcon size="lg" /></Flex.Item>
<Flex.Item><Text>xl</Text> <SearchInstUIIcon size="xl" /></Flex.Item>
<Flex.Item><Text>2xl</Text> <SearchInstUIIcon size="2xl" /></Flex.Item>
</Flex>
</View>
```

Illustration sizes (`illu-sm`, `illu-md`, `illu-lg`) are intended for larger decorative contexts.

### Color

Use the `color` prop with a semantic color token — a theme-aware named value (e.g. `errorColor`,
`successColor`) that automatically adapts to the active theme. `inherit` passes `currentColor` through from the parent element.
`ai` renders the icon with an AI gradient.

```js
---
type: example
---
<View as="div" margin="small" padding="medium" background="primary">
<Flex wrap="wrap" alignItems="center" gap="small">
<Flex.Item><HeartInstUIIcon size="lg" color="baseColor" /></Flex.Item>
<Flex.Item><HeartInstUIIcon size="lg" color="mutedColor" /></Flex.Item>
<Flex.Item><HeartInstUIIcon size="lg" color="successColor" /></Flex.Item>
<Flex.Item><HeartInstUIIcon size="lg" color="errorColor" /></Flex.Item>
<Flex.Item><HeartInstUIIcon size="lg" color="warningColor" /></Flex.Item>
<Flex.Item><HeartInstUIIcon size="lg" color="infoColor" /></Flex.Item>
<Flex.Item><AiInfoInstUIIcon size="lg" color="ai" /></Flex.Item>
</Flex>
</View>
```

### Rotate

```js
---
type: example
---
<View as="div" margin="small" padding="medium" background="primary">
<Flex wrap="wrap" alignItems="center" gap="small">
<Flex.Item><ChevronUpInstUIIcon size="lg" rotate="0" /></Flex.Item>
<Flex.Item><ChevronUpInstUIIcon size="lg" rotate="90" /></Flex.Item>
<Flex.Item><ChevronUpInstUIIcon size="lg" rotate="180" /></Flex.Item>
<Flex.Item><ChevronUpInstUIIcon size="lg" rotate="270" /></Flex.Item>
</Flex>
</View>
```

### Stroke vs Filled

Lucide icons are stroke-based. Several custom icons come in a filled variant, identified by the
`Solid` suffix in their export name (e.g. `BellInstUIIcon` vs `BellSolidInstUIIcon`).

```js
---
type: example
---
<View as="div" margin="small">
<Flex gap="small" alignItems="center">
<Flex.Item><Text>Stroke</Text> <HeartInstUIIcon size="lg" color="errorColor" /></Flex.Item>
<Flex.Item><Text>Filled</Text> <HeartSolidInstUIIcon size="lg" color="errorColor" /></Flex.Item>
</Flex>
</View>
```

---

## Legacy Icons

> **Deprecated.** The legacy icon set (`IconHeartLine`, `IconSearchLine`, etc.) is kept for
> backwards compatibility. Use the `InstUIIcon` components above for new code.
> To migrate existing usage run:
>
> ```
> npx @instructure/ui-codemods migrateToNewIcons <path>
> ```

### Accessibility

By default, the icon's `role` is set to `presentation`. However, when the `title` prop is set, the role attribute is set to `img`. Include the `description` prop to further describe the icon.
Expand Down
6 changes: 2 additions & 4 deletions packages/__docs__/buildScripts/DataTypes.mts
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,7 @@ type Glyph = {
glyphName: string
}

type MainIconsData = {
glyphs: Glyph[]
}
type LegacyIconsData = Glyph[]

type MainDocsData = {
themes: Record<string, { resource: Theme }>
Expand Down Expand Up @@ -170,7 +168,7 @@ export type {
LibraryOptions,
Glyph,
MainDocsData,
MainIconsData,
LegacyIconsData,
JsDocResult,
MinorVersionData,
Section,
Expand Down
6 changes: 3 additions & 3 deletions packages/__docs__/buildScripts/build-docs.mts
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,10 @@ async function buildDocs() {
)

// eslint-disable-next-line no-console
console.log('Copying icons data...')
console.log('Copying legacy icons data...')
fs.copyFileSync(
projectRoot + '/packages/ui-icons/src/__build__/icons-data.json',
buildDir + 'icons-data.json'
projectRoot + '/packages/ui-icons/src/generated/legacy/legacy-icons-data.json',
buildDir + 'legacy-icons-data.json'
)

// eslint-disable-next-line no-console
Expand Down
6 changes: 4 additions & 2 deletions packages/__docs__/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ import { getComponentsForVersion } from './versioned-components'
import { rebrandDark, rebrandLight } from '@instructure/ui-themes'
import { debounce } from '@instructure/debounce'

import '@instructure/ui-icons/src/__build__/icon-font/Solid/InstructureIcons-Solid.css'
import '@instructure/ui-icons/src/__build__/icon-font/Line/InstructureIcons-Line.css'
// eslint-disable-next-line @instructure/no-relative-imports
import '../ui-icons/src/generated/icon-font/Solid/InstructureIcons-Solid.css'
// eslint-disable-next-line @instructure/no-relative-imports
import '../ui-icons/src/generated/icon-font/Line/InstructureIcons-Line.css'

import { DateTime } from '@instructure/ui-i18n'
// @ts-ignore webpack import
Expand Down
1 change: 0 additions & 1 deletion packages/__docs__/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@
"moment": "^2.30.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-window": "^2.2.3",
"semver": "^7.7.2",
"uuid": "^11.1.0",
"webpack-merge": "^6.0.1"
Expand Down
Loading
Loading