Skip to content
Open
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
170 changes: 170 additions & 0 deletions packages/core/src/components/icon-next/IconNext.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/* !
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
*/

import type { Meta, StoryObj } from "@storybook/react-vite";
import { storybookLayoutDecorator, StoryLabel } from "@storybook-common";

import { CircleArrowDownIcon, HouseIcon, PlayFilledIcon, PlayIcon } from "@blueprintjs/icons/next";

import { Intent } from "../../common";
import { Button } from "../button/buttons";

import { IconNext } from "./iconNext";

const meta: Meta<typeof IconNext> = {
title: "Next Icons/IconNext",
component: IconNext,
decorators: [storybookLayoutDecorator],
parameters: {
layout: "centered",
},
} satisfies Meta<typeof IconNext>;

export default meta;
type Story = StoryObj<typeof meta>;

/**
* Dynamic string usage — the icon is loaded asynchronously by name.
*/
export const DynamicString: Story = {
name: "Dynamic String",
render: () => (
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<IconNext icon="house" size={32} />
<IconNext icon="circle-arrow-down" size={32} />
<IconNext icon="star" size={32} />
<IconNext icon="gear" size={32} />
<IconNext icon="magnifying-glass" size={32} />
</div>
),
};

/**
* Element passthrough — works the same as current Icon.
*/
export const ElementPassthrough: Story = {
name: "Element Passthrough",
render: () => (
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<IconNext icon={<HouseIcon size={32} />} />
<IconNext icon={<CircleArrowDownIcon size={32} />} />
<IconNext icon={<PlayIcon size={32} />} />
</div>
),
};

/**
* Outlined vs filled variant switching.
*/
export const VariantComparison: Story = {
name: "Variants",
render: () => (
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
<div>
<StoryLabel title="Outlined (default)" />
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<IconNext icon="play" size={32} />
<IconNext icon="pause" size={32} />
<IconNext icon="speaker-high" size={32} />
<IconNext icon="fast-forward" size={32} />
<IconNext icon="skip-backward" size={32} />
</div>
</div>
<div>
<StoryLabel title="Filled" />
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<IconNext icon="play" variant="filled" size={32} />
<IconNext icon="pause" variant="filled" size={32} />
<IconNext icon="speaker-high" variant="filled" size={32} />
<IconNext icon="fast-forward" variant="filled" size={32} />
<IconNext icon="skip-backward" variant="filled" size={32} />
</div>
</div>
</div>
),
};

/**
* Filled fallback — requesting "filled" on an icon without a filled variant
* silently falls back to outlined. A dev-mode console warning is emitted.
*/
export const FilledFallback: Story = {
name: "Filled Fallback",
render: () => (
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
<div>
<StoryLabel title="Has filled variant (play)" />
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<IconNext icon="play" size={32} />
<IconNext icon="play" variant="filled" size={32} />
</div>
</div>
<div>
<StoryLabel title="No filled variant (anchor) — falls back to outlined" />
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<IconNext icon="anchor" size={32} />
<IconNext icon="anchor" variant="filled" size={32} />
</div>
</div>
</div>
),
};

/**
* Side-by-side: dynamic IconNext vs static component.
*/
export const DynamicVsStatic: Story = {
name: "Dynamic vs Static",
render: () => (
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
<div>
<StoryLabel title="IconNext (dynamic)" />
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<IconNext icon="play" size={32} />
<IconNext icon="play" variant="filled" size={32} />
</div>
</div>
<div>
<StoryLabel title="Static import (same icon)" />
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<PlayIcon size={32} />
<PlayFilledIcon size={32} />
</div>
</div>
</div>
),
};

/**
* IconNext used inside Blueprint components via icon prop.
*/
export const InComponents: Story = {
name: "In Components",
render: () => (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<div style={{ display: "flex", gap: 8 }}>
{Object.values(Intent).map(intent => (
<Button
key={intent}
icon={<IconNext icon="bell" />}
endIcon={<IconNext icon="play" variant="filled" />}
intent={intent}
text={intent || "none"}
/>
))}
</div>
<div style={{ display: "flex", gap: 8 }}>
{Object.values(Intent).map(intent => (
<Button
key={intent}
icon={<IconNext icon="pause" variant="filled" />}
intent={intent}
text={undefined}
aria-label="pause"
/>
))}
</div>
</div>
),
};
194 changes: 194 additions & 0 deletions packages/core/src/components/icon-next/icon-next.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
---
title: IconNext
tag: new
---

# IconNext

<div class="@ns-callout @ns-intent-warning @ns-icon-warning-sign @ns-callout-has-body-content">

**Experimental**

`IconNext` renders the next-generation icon set, which is experimental: names are stable, but the
icon set and component API may change in upcoming 6.x releases.

</div>

<div class="@ns-callout @ns-intent-primary @ns-icon-info-sign">

See the [**next icons**](#icons/next-icons) page for a searchable list of all available next-generation icons.

</div>

## Usage

```ts copy
import { IconNext } from "@blueprintjs/core";
```

```tsx
<IconNext icon="star" size={30} />
```

`IconNext` renders an icon from the **@blueprintjs/icons/next** set as an inline `<svg>`. Give it an
icon name and it loads the shared next-icon paths module on demand via a dynamic import. The first dynamic
icon loads one lazy chunk containing all next-icon paths; this keeps those paths out of the initial bundle,
but does not create a separate chunk for each icon. You can also pass a JSX element directly.

Many Blueprint components provide an `icon` prop which accepts an icon name or a JSX element to use as
the icon. Pass an `<IconNext>` element to use a next-generation icon in those slots.

@reactExample IconNextExample

## Examples

### Basic

Use the `<IconNext>` component to render a next icon in React. The `icon` prop is typed against
`BlueprintIconsNext`, so editors offer autocomplete for known icon names and reject unknown ones. The
companion `IconNextNames` object maps PascalCase identifiers to those names (for example
`IconNextNames.Star` is `"star"`).

The optional `size` prop sets the exact width and height of the icon; the element can also be sized
with CSS. If `title` is _not_ provided, `aria-hidden` is set to `true`, since an unlabeled icon is
assumed to be decorative.

```tsx
import { IconNext } from "@blueprintjs/core";
import { IconNextNames, IconSize } from "@blueprintjs/icons/next";

// icon name string literals are type checked
<IconNext icon="circle-plus" />
<IconNext icon={IconNextNames.Star} size={20} />

// constants are provided for standard sizes
<IconNext icon="trash" size={IconSize.LARGE} intent="danger" />

// you can also pass all valid HTML props
<IconNext icon="plus" onClick={handleAdd} onKeyDown={handleAddKeys} />
```

### Migrating dynamic icon names

`IconNext` itself accepts next-generation icon names and does not implicitly reinterpret legacy names. For
dynamic values that may already use either naming scheme, normalize the value with `iconNameToIconNextName`.
This helper uses **next-first** precedence, which makes it safe to apply broadly to mixed or already-migrated
values:

```tsx
import { IconNext } from "@blueprintjs/core";
import { iconNameToIconNextName } from "@blueprintjs/icons/next";

const nextIconName = iconNameToIconNextName(savedIconName);

return nextIconName === undefined ? null : <IconNext icon={nextIconName} />;
```

```ts
iconNameToIconNextName("house"); // "house" (already a next name)
iconNameToIconNextName("home"); // "house" (renamed legacy icon)
iconNameToIconNextName("user"); // "user" (ambiguous name; next meaning wins)
iconNameToIconNextName("unknown"); // undefined
```

When the source is known to contain legacy names, use `legacyIconNameToIconNextName` instead. It always
interprets its input using the legacy mapping, including names that also exist in the next set:

```ts
import { legacyIconNameToIconNextName } from "@blueprintjs/icons/next";

legacyIconNameToIconNextName("home"); // "house"
legacyIconNameToIconNextName("user"); // "user-circle" (legacy meaning wins)
legacyIconNameToIconNextName("house"); // undefined (not a legacy name)
```

Custom sizes are supported. The following React element:

```tsx
<IconNext icon="star" size={30} />
```

...renders this HTML markup:

```xml
<span class="@ns-icon @ns-icon-star" aria-hidden="true">
<svg data-icon="star" width="30" height="30" viewBox="0 0 16 16" role="img">
<path d="..."></path>
</svg>
</span>
```

Unlike the legacy `Icon` component, next icons are drawn on a single 16px grid (note the
`viewBox="0 0 16 16"` above) and scaled to the requested `size`, so there is no separate 20px grid to
switch between.

### Outlined and filled variants

Next icons come in two styles. Every icon has an **outlined** version (the default); a subset also have
a **filled** version. Select the style with the `variant` prop:

```tsx
<IconNext icon="play" />
<IconNext icon="play" variant="filled" />
```

If `"filled"` is requested for an icon that has no filled version (for example `"anchor"`), the
component falls back to the outlined style and logs a development-mode warning.

### Usage with other components

Many Blueprint components accept an `icon` prop which can be specified as either a string icon name or a
JSX element. To use a next-generation icon, pass an `<IconNext>` element:

```tsx
import { Button, IconNext } from "@blueprintjs/core";

<Button icon={<IconNext icon="magnifying-glass" />} text="Search" />
<Button icon={<IconNext icon="bell" variant="filled" />} text="Notifications" />
```

## Props interface

@interface DefaultIconNextProps

## DOM attributes

The `<IconNext>` component forwards extra HTML attributes to its root DOM element. By default, the root
element is a `<span>` wrapper around the icon `<svg>`. The tag name of this element may be customized via
the `tagName` prop as either:

- a custom HTML tag name (for example `<div>` instead of the default `<span>` wrapper), or
- `null`, which makes the component omit the wrapper element and only render the `<svg>` as its root element

By default, `<IconNext>` supports a limited set of DOM attributes which are assignable to _all_ HTML and
SVG elements. In some cases, you may want to use more specific attributes which are only available on HTML
elements or SVG elements. The `<IconNext>` component has a generic type which allows for this more advanced
usage. You can specify a type parameter on the component opening tag to (for example) set an HTML-only
attribute:

```tsx
import { IconNext } from "@blueprintjs/core";
import * as React from "react";

function Example() {
const [isDraggable, setIsDraggable] = React.useState();
// explicitly declare type of the root element so that we can set the "draggable" DOM attribute
return <IconNext<HTMLSpanElement> icon="house" draggable={isDraggable} />;
}
```

Another use case for this type parameter API may be to get the correct type definition for an event
handler on the root element when _omitting_ the icon wrapper element:

```tsx
import { IconNext } from "@blueprintjs/core";
import * as React from "react";

function Example() {
const handleClick: React.MouseEventHandler<SVGSVGElement> = () => {
/* ... */
};
// explicitly declare type of the root element so that we can narrow the type of the event handler
return <IconNext<SVGSVGElement> icon="plus" onClick={handleClick} tagName={null} />;
}
```
Loading