Skip to content

Commit 1132d25

Browse files
authored
feat(react-icons): propagate headless/ API to stable (#1119)
1 parent 6ea5ed1 commit 1132d25

18 files changed

Lines changed: 117 additions & 50 deletions

packages/docsite/stories/Icons/UserGuidance.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { Meta } from '@storybook/addon-docs/blocks';
2323
| **Loading** | Bundled with JS — no flash | FOIT / FOUT risk during font load | One request per sprite group; webpack plugin can merge into a single request |
2424
| **Setup** | None (default) | Webpack subsetting plugin, static assets hosting (font) | Webpack subsetting plugin, static assets hosting (sprite), same-origin proxy for CDN |
2525

26-
> **Note:** A **headless** rendering mode (vanilla CSS, no Griffel runtime) is available as an alpha opt-in via `/headless/*` imports (e.g. `@fluentui/react-icons/headless/svg/access-time`). With CSS extraction it reduces JS bundle sizes significantly — up to −70% for font icons, −56% for SVG sprites, −14% for inline SVG. This table will be updated once the headless API ships in stable.
26+
> **Note:** A **headless** rendering mode (vanilla CSS, no Griffel runtime) is available via `/headless/*` imports (e.g. `@fluentui/react-icons/headless/svg/access-time`). With CSS extraction it reduces JS bundle sizes significantly — up to −70% for font icons, −56% for SVG sprites, −14% for inline SVG. See the [Headless API documentation](https://github.com/microsoft/fluentui-system-icons/blob/main/packages/react-icons/docs/headless.md) for details.
2727
2828
### When to use inline SVG (default)
2929

packages/react-icons/README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,13 +348,11 @@ SVG sprites offer smaller bundles, faster renders, and zero runtime overhead.
348348

349349
👉 **[Full documentation →](./docs/preview-features/svg-sprites.md)**
350350

351-
## Headless API — ⚠️ Alpha
352-
353-
> **This feature is available as an alpha prerelease only.** Install via `npm i @fluentui/react-icons@alpha`
351+
## Headless API
354352

355353
A drop-in replacement for the standard API that removes the CSS-in-JS runtime — provides data-attribute selectors for styling behaviour with opt-in pre-defined vanilla CSS.
356354

357-
👉 **[Full documentation →](./docs/preview-features/headless.md)**
355+
👉 **[Full documentation →](./docs/headless.md)**
358356

359357
## Viewing Icons
360358

packages/react-icons/build-verify.test.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2473,28 +2473,30 @@ describe('Build Verification', () => {
24732473
'useIconState.d.ts',
24742474
'bundleIcon.js',
24752475
'bundleIcon.d.ts',
2476-
'headless.css',
2476+
'utils.js',
2477+
'utils.d.ts',
2478+
'styles.css',
24772479
];
24782480
for (const file of expectedFiles) {
24792481
expect(fs.existsSync(path.join(headlessDir, file))).toBe(true);
24802482
}
24812483

2482-
// headless.css must use data-attribute selectors (the key differentiator from CSS-in-JS)
2483-
const css = await readFile(path.join(headlessDir, 'headless.css'), 'utf-8');
2484+
// styles.css must use data-attribute selectors (the key differentiator from CSS-in-JS)
2485+
const css = await readFile(path.join(headlessDir, 'styles.css'), 'utf-8');
24842486
expect(css).toContain('[data-fui-icon]');
24852487
expect(css).toContain('[data-fui-icon-rtl]');
24862488
expect(css).toContain('[data-fui-icon-hidden]');
24872489

24882490
// Font sub-infrastructure
24892491
expect(fs.existsSync(path.join(headlessDir, 'fonts', 'createFluentFontIcon.js'))).toBe(true);
2490-
expect(fs.existsSync(path.join(headlessDir, 'fonts', 'headless-fonts.css'))).toBe(true);
2492+
expect(fs.existsSync(path.join(headlessDir, 'fonts', 'styles.css'))).toBe(true);
24912493
});
24922494

24932495
it('package.json#sideEffects include headless entries css entries', () => {
24942496
const packageJsonPath = path.join(__dirname, 'package.json');
24952497
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
24962498

2497-
expect(packageJson.sideEffects).toEqual(['**/headless/fonts/headless-fonts.css', '**/headless/headless.css']);
2499+
expect(packageJson.sideEffects).toEqual(['**/headless/fonts/styles.css', '**/headless/styles.css']);
24982500
});
24992501
});
25002502

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { AirplaneFilled, AirplaneRegular } from '@fluentui/react-icons/headless/svg/airplane';
2+
import { bundleIcon } from '@fluentui/react-icons/headless/utils';
3+
4+
const Airplane = bundleIcon(AirplaneFilled, AirplaneRegular);
5+
6+
console.log(Airplane);
7+
8+
export default { name: 'Headless - Bundle Icon' };
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { AirplaneRegular } from '@fluentui/react-icons/headless/fonts/airplane';
2+
3+
console.log(AirplaneRegular);
4+
5+
export default { name: 'Headless - Single Fonts' };
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { AirplaneRegular } from '@fluentui/react-icons/headless/svg/airplane';
2+
3+
console.log(AirplaneRegular);
4+
5+
export default { name: 'Headless - Single SVG' };

packages/react-icons/docs/bundle-size-rendering-approaches-comparison.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151

5252
When webpack uses `style-loader` (the default — no `MiniCssExtractPlugin`), the **headless fonts** bundle is **+1,344 B minified (+13%)** larger than the Griffel baseline. This is counter-intuitive — the headless approach was designed to remove the CSS-in-JS runtime, but in this mode it _replaces_ it with an even heavier runtime.
5353

54-
**Root cause:** The two plain CSS imports (`headless-fonts.css`, `headless.css`) require webpack's `css-loader` + `style-loader` runtime to inject styles into the DOM at runtime. This runtime comprises ~10 extra webpack modules (style injection/removal API, `<style>` element creation, CSP nonce support, URL escaping, CSS text stringification, etc.) that collectively weigh more than Griffel's built-in runtime (hash function, `mergeClasses`, `makeStyles`, style insertion).
54+
**Root cause:** The two plain CSS imports (`fonts/styles.css`, `styles.css`) require webpack's `css-loader` + `style-loader` runtime to inject styles into the DOM at runtime. This runtime comprises ~10 extra webpack modules (style injection/removal API, `<style>` element creation, CSP nonce support, URL escaping, CSS text stringification, etc.) that collectively weigh more than Griffel's built-in runtime (hash function, `mergeClasses`, `makeStyles`, style insertion).
5555

5656
**When headless _does_ win:** With CSS extraction (`MiniCssExtractPlugin`), the CSS moves into a separate `.css` file and the loader runtime is not needed — headless fonts then drop to **3.098 kB** (−69.5%). The style-loader overhead also gets amortized when an app already uses `css-loader`/`style-loader` for other CSS imports (the runtime is shared).
5757

packages/react-icons/docs/preview-features/headless.md renamed to packages/react-icons/docs/headless.md

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
# Headless API
22

3-
> **⚠️ Alpha** — this feature is available as an alpha prerelease only.
4-
>
5-
> Install via `npm i @fluentui/react-icons@prerelease`
6-
73
The Headless API is a drop-in replacement for the standard icon API that removes the CSS-in-JS runtime dependency. It provides `data-*` attribute selectors for styling behaviour with opt-in pre-defined vanilla CSS — making it suitable for any React setup, including those without a CSS-in-JS runtime.
84

95
## Benefits
@@ -16,7 +12,7 @@ The Headless API is a drop-in replacement for the standard icon API that removes
1612

1713
## How it works
1814

19-
The standard API uses Griffel's `makeStyles` / `mergeClasses` to inject CSS rules at runtime. The headless API replaces this with HTML `data-*` attributes and a shipped CSS file (`headless.css`) that targets them:
15+
The standard API uses Griffel's `makeStyles` / `mergeClasses` to inject CSS rules at runtime. The headless API replaces this with HTML `data-*` attributes and a shipped CSS file (`styles.css`) that targets them:
2016

2117
| Concern | Standard (Griffel) | Headless (CSS) |
2218
| --------------------- | ----------------------------------- | -------------------------------------------------------------------------- |
@@ -33,17 +29,17 @@ You **must** import the headless CSS file — this is the key difference from th
3329
**For SVG icons and SVG sprites:**
3430

3531
```ts
36-
import '@fluentui/react-icons/headless/headless.css';
32+
import '@fluentui/react-icons/headless/styles.css';
3733
```
3834

3935
**For font icons** (additionally):
4036

4137
```ts
42-
import '@fluentui/react-icons/headless/headless-fonts.css';
43-
import '@fluentui/react-icons/headless/headless.css';
38+
import '@fluentui/react-icons/headless/fonts/styles.css';
39+
import '@fluentui/react-icons/headless/styles.css';
4440
```
4541

46-
> **Note:** `headless-fonts.css` contains `@font-face` declarations with relative paths to the font files (`.woff2`, `.woff`, `.ttf`). Your bundler (webpack, Vite, esbuild) will resolve these into the dependency graph automatically, enabling font subsetting plugins to process them.
42+
> **Note:** `fonts/styles.css` contains `@font-face` declarations with relative paths to the font files (`.woff2`, `.woff`, `.ttf`). Your bundler (webpack, Vite, esbuild) will resolve these into the dependency graph automatically, enabling font subsetting plugins to process them.
4743
4844
> **Tip 💡:** It's highly recommended to enable our [`react-icons-font-subsetting-webpack-plugin`](https://www.npmjs.com/package/@fluentui/react-icons-font-subsetting-webpack-plugin) to get same fonts "tree-shaking" perf boost
4945
@@ -54,7 +50,7 @@ import '@fluentui/react-icons/headless/headless.css';
5450
Headless SVG icons are grouped by icon kind and exposed via `@fluentui/react-icons/headless/svg/{icon-group}`:
5551

5652
```tsx
57-
import '@fluentui/react-icons/headless/headless.css';
53+
import '@fluentui/react-icons/headless/styles.css';
5854

5955
import {
6056
AccessTime20Filled,
@@ -78,7 +74,7 @@ function MyComponent() {
7874
Headless SVG sprites work the same way as standard SVG sprites but without Griffel:
7975

8076
```tsx
81-
import '@fluentui/react-icons/headless/headless.css';
77+
import '@fluentui/react-icons/headless/styles.css';
8278

8379
import { AccessTime20Filled, AccessTime24Filled } from '@fluentui/react-icons/headless/svg-sprite/access-time';
8480
import { Add16Filled } from '@fluentui/react-icons/headless/svg-sprite/add';
@@ -93,15 +89,15 @@ function MyComponent() {
9389
}
9490
```
9591

96-
> **Same-origin requirement:** Sprite files must be served from the same origin as your application. See the [SVG Sprites same-origin section](./svg-sprites.md#same-origin-requirement) for details.
92+
> **Same-origin requirement:** Sprite files must be served from the same origin as your application. See the [SVG Sprites same-origin section](./preview-features/svg-sprites.md#same-origin-requirement) for details.
9793
9894
### Font Icons
9995

100-
Headless font icons require both CSS files — `headless.css` for base styles and `headless-fonts.css` for `@font-face` declarations:
96+
Headless font icons require both CSS files — `styles.css` for base styles and `fonts/styles.css` for `@font-face` declarations:
10197

10298
```tsx
103-
import '@fluentui/react-icons/headless/headless-fonts.css';
104-
import '@fluentui/react-icons/headless/headless.css';
99+
import '@fluentui/react-icons/headless/fonts/styles.css';
100+
import '@fluentui/react-icons/headless/styles.css';
105101

106102
import { Airplane20Filled, Airplane24Regular } from '@fluentui/react-icons/headless/fonts/airplane';
107103

@@ -149,7 +145,7 @@ import {
149145
The `bundleIcon` function works identically to the standard API:
150146

151147
```tsx
152-
import '@fluentui/react-icons/headless/headless.css';
148+
import '@fluentui/react-icons/headless/styles.css';
153149
import { bundleIcon } from '@fluentui/react-icons/headless';
154150
import { AccessTimeFilled } from '@fluentui/react-icons/headless/svg/access-time';
155151
import { AccessTimeRegular } from '@fluentui/react-icons/headless/svg/access-time';
@@ -179,6 +175,6 @@ You can keep root-level barrel imports and leverage build transforms to adopt th
179175

180176
Use `headless/svg` as the target path (or `headless/svg-sprite` for sprites, `headless/fonts` for font icons).
181177

182-
> **Note:** You still need to manually add the CSS import (`import '@fluentui/react-icons/headless/headless.css'`) to your application entry point — build transforms only rewrite component imports.
178+
> **Note:** You still need to manually add the CSS import (`import '@fluentui/react-icons/headless/styles.css'`) to your application entry point — build transforms only rewrite component imports.
183179
184-
👉 **[Build-Time Transform setup (Babel & SWC) →](../build-transforms.md)**
180+
👉 **[Build-Time Transform setup (Babel & SWC) →](./build-transforms.md)**

packages/react-icons/docs/preview-features/README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,3 @@
77
## [Atomic API (SVG Sprites)](./svg-sprites.md)
88

99
SVG sprites offer smaller bundles, faster renders, and zero runtime overhead.
10-
11-
## [Headless API](./headless.md)
12-
13-
A drop-in replacement for the standard API that removes the CSS-in-JS runtime — provides data-attribute selectors for styling behaviour with opt-in pre-defined vanilla CSS.

packages/react-icons/package.json

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
{
22
"name": "@fluentui/react-icons",
33
"version": "2.0.331",
4-
"sideEffects": false,
4+
"sideEffects": [
5+
"**/headless/fonts/styles.css",
6+
"**/headless/styles.css"
7+
],
58
"main": "lib-cjs/index.js",
69
"module": "lib/index.js",
710
"typings": "lib/index.d.ts",
@@ -14,8 +17,8 @@
1417
"scripts": {
1518
"clean": "git clean -fXd lib/ lib-cjs/ intermediate/ src/ tmp/",
1619
"generate:base-svg": "node scripts/copy-base-svg.js --source=../svg-icons/icons --dest=./intermediate",
17-
"convert:svg": "node scripts/convert.js --source=./intermediate --dest=./src --perIconDest=./src/atoms/svg --spriteDest=./src/atoms/svg-sprite --headlessPerIconDest=./src/atoms/headless-svg --headlessSpriteDest=./src/atoms/headless-svg-sprite --rtl=./intermediate/rtl.json --metadata=./tmp/metadata-svg.json",
18-
"convert:fonts": "node scripts/convert-font.js --source=./src/utils/fonts --dest=./src/fonts --perIconDest=./src/atoms/fonts --headlessPerIconDest=./src/atoms/headless-fonts --codepointDest=./src/utils/fonts --rtl=./intermediate/rtl.json --metadata=./tmp/metadata-font.json",
20+
"convert:svg": "node scripts/convert.js --source=./intermediate --dest=./src --perIconDest=./src/atoms/svg --spriteDest=./src/atoms/svg-sprite --headless --headlessPerIconDest=./src/atoms/headless-svg --rtl=./intermediate/rtl.json --metadata=./tmp/metadata-svg.json",
21+
"convert:fonts": "node scripts/convert-font.js --source=./src/utils/fonts --dest=./src/fonts --perIconDest=./src/atoms/fonts --headless --headlessPerIconDest=./src/atoms/headless-fonts --codepointDest=./src/utils/fonts --rtl=./intermediate/rtl.json --metadata=./tmp/metadata-font.json",
1922
"convert:merge-metadata": "node scripts/merge-metadata.js --svgMetadata=./tmp/metadata-svg.json --fontMetadata=./tmp/metadata-font.json --output=./metadata.json",
2023
"generate:font-regular": "node ../../importer/generateFont.js --source=intermediate --dest=src/utils/fonts --iconType=Regular --codepoints=../../fonts/FluentSystemIcons-Regular.json",
2124
"generate:font-filled": "node ../../importer/generateFont.js --source=intermediate --dest=src/utils/fonts --iconType=Filled --codepoints=../../fonts/FluentSystemIcons-Filled.json",
@@ -61,6 +64,7 @@
6164
"files": [
6265
"docs/preview-features/*.md",
6366
"docs/build-transforms.md",
67+
"docs/headless.md",
6468
"lib/",
6569
"lib-cjs/"
6670
],
@@ -126,6 +130,33 @@
126130
"types": "./lib/providers.d.ts",
127131
"import": "./lib/providers.js",
128132
"require": "./lib-cjs/providers.js"
133+
},
134+
"./headless": {
135+
"types": "./lib/headless/index.d.ts",
136+
"import": "./lib/headless/index.js",
137+
"require": "./lib-cjs/headless/index.js"
138+
},
139+
"./headless/fonts": {
140+
"types": "./lib/headless/fonts/index.d.ts",
141+
"import": "./lib/headless/fonts/index.js",
142+
"require": "./lib-cjs/headless/fonts/index.js"
143+
},
144+
"./headless/styles.css": "./lib/headless/styles.css",
145+
"./headless/fonts/styles.css": "./lib/headless/fonts/styles.css",
146+
"./headless/svg/*": {
147+
"types": "./lib/atoms/headless-svg/*.d.ts",
148+
"import": "./lib/atoms/headless-svg/*.js",
149+
"require": "./lib-cjs/atoms/headless-svg/*.js"
150+
},
151+
"./headless/fonts/*": {
152+
"types": "./lib/atoms/headless-fonts/*.d.ts",
153+
"import": "./lib/atoms/headless-fonts/*.js",
154+
"require": "./lib-cjs/atoms/headless-fonts/*.js"
155+
},
156+
"./headless/utils": {
157+
"types": "./lib/headless/utils.d.ts",
158+
"import": "./lib/headless/utils.js",
159+
"require": "./lib-cjs/headless/utils.js"
129160
}
130161
}
131162
}

0 commit comments

Comments
 (0)