Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9f3342a
chore: boilerplate package
weronikaolejniczak Sep 7, 2026
819fd8f
chore(eui): extract button constants for eui-vanilla
weronikaolejniczak Sep 7, 2026
96904f0
chore(eui-vanilla): setup Storybook
weronikaolejniczak Sep 7, 2026
d07c6df
feat(eui-vanilla): generate CSS from EUI Emotion
weronikaolejniczak Sep 7, 2026
73f5621
feat(eui-vanilla): add mountButton
weronikaolejniczak Sep 7, 2026
6bd869f
fat(eui-vanilla): log iframe JSON-RPC in Actions
weronikaolejniczak Sep 7, 2026
9472332
fix(eui-vanilla): fix mismatches with EUI
weronikaolejniczak Sep 7, 2026
c62f096
chore(eui-vanilla): implement a bench
weronikaolejniczak Sep 7, 2026
95c7069
chore(eui-vanilla): add contributing guidelines
weronikaolejniczak Sep 7, 2026
f340731
chore(eui-vanilla): make package.json generic
weronikaolejniczak Sep 7, 2026
d71043b
chore(eui-vanilla): remove leaky non-public API
weronikaolejniczak Sep 7, 2026
c33ebd7
feat(eui-vanilla): glob components
weronikaolejniczak Sep 7, 2026
06d5349
chore(eui-vanilla): drop the barrel bench (same bytes)
weronikaolejniczak Sep 8, 2026
ad91199
refactor(eui-vanilla): use singular component id source
weronikaolejniczak Sep 8, 2026
bee4c94
refactor(eui-vanilla): drop generated CSS entry string
weronikaolejniczak Sep 8, 2026
942969d
refactor(eui-vanilla): remove HTML preview output
weronikaolejniczak Sep 8, 2026
ed796e3
chore(eui-vanilla): rewrite documentation
weronikaolejniczak Sep 8, 2026
2ee361f
refactor(eui-vanilla): make the theming standard with MCP SDK approach
weronikaolejniczak Sep 8, 2026
b007acc
refactor(eui-vanilla): make the JSON-RPC Storybook only
weronikaolejniczak Sep 8, 2026
605a9d3
refactor(eui): make link security helpers dependency-free
weronikaolejniczak Sep 8, 2026
65fba73
refactor(eui-vanilla): reuse EUI link security helpers
weronikaolejniczak Sep 8, 2026
70ca215
refactor(eui-vanilla): use EUI as workspace dependency
weronikaolejniczak Sep 8, 2026
1e19e3d
fix(eui-vanilla): extract linked Emotion keyframes
weronikaolejniczak Sep 8, 2026
b60bd56
refactor(eui-vanilla): clean generated folder before regeneration
weronikaolejniczak Sep 8, 2026
d64ce2d
chore(eui-vanilla): update the README.md with spike conclusions
weronikaolejniczak Sep 8, 2026
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
5 changes: 5 additions & 0 deletions packages/eui-vanilla/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/dist
/generated
/tmp
/node_modules
/storybook-static
25 changes: 25 additions & 0 deletions packages/eui-vanilla/.storybook/fonts.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* Inter variable files from EUI's Storybook. `format('woff2')` so Chrome maps wght 450 instead of rounding to 500. */
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: block;
font-weight: 300 700;
src: url('../../eui/node_modules/@fontsource-variable/inter/files/inter-latin-ext-wght-normal.woff2')
format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}

@font-face {
font-family: 'Inter';
font-style: normal;
font-display: block;
font-weight: 300 700;
src: url('../../eui/node_modules/@fontsource-variable/inter/files/inter-latin-wght-normal.woff2')
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
35 changes: 35 additions & 0 deletions packages/eui-vanilla/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { searchForWorkspaceRoot } from 'vite';

import type { StorybookConfig } from '@storybook/html-vite';

const vanillaRoot = join(dirname(fileURLToPath(import.meta.url)), '..');

const config: StorybookConfig = {
stories: ['../src/**/*.stories.ts'],
framework: {
name: '@storybook/html-vite',
options: {},
},
async viteFinal(viteConfig) {
viteConfig.server ??= {};
viteConfig.server.fs ??= {};
viteConfig.server.fs.allow = [
...(viteConfig.server.fs.allow ?? []),
searchForWorkspaceRoot(vanillaRoot),
vanillaRoot,
];
return viteConfig;
},
};

export default config;
78 changes: 78 additions & 0 deletions packages/eui-vanilla/.storybook/mcp-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import type { Decorator } from '@storybook/html-vite';
import { action } from 'storybook/actions';

import { isStoryNotification } from './mcp';

let patched = false;
let lastTheme: string | undefined;

const install = () => {
if (patched) return;
patched = true;

const { parent } = window;
const original = parent.postMessage.bind(parent);

parent.postMessage = (
message: unknown,
targetOriginOrOptions?: unknown,
transfer?: Transferable[]
) => {
if (isStoryNotification(message)) action('mcp →')(message);

if (
typeof targetOriginOrOptions === 'string' ||
targetOriginOrOptions === undefined
)
return original(message, targetOriginOrOptions as string, transfer);

return original(message, targetOriginOrOptions as WindowPostMessageOptions);
};

window.addEventListener('message', (event: MessageEvent) => {
if (event.source === window) return;
if (isStoryNotification(event.data)) action('mcp ←')(event.data);
});
};

/**
* Decorator for Storybook stories.
*
* Preview iframe is the MCP app. Logs JSON-RPC `postMessage` in "Actions" tab.
* Theme toolbar is sent as `host-context-changed`.
*
* @param storyFn - Story function
* @param context - Story context
* @returns Story result
*/
export const mcpDecorator: Decorator = (storyFn, context) => {
install();

const theme = context.globals.theme === 'dark' ? 'dark' : 'light';
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;

if (lastTheme !== theme) {
lastTheme = theme;

const message = {
jsonrpc: '2.0' as const,
method: 'ui/notifications/host-context-changed',
params: { theme },
};

action('mcp ←')(message);

window.dispatchEvent(new MessageEvent('message', { data: message }));
}

return storyFn();
};
38 changes: 38 additions & 0 deletions packages/eui-vanilla/.storybook/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export type StoryNotification = {
jsonrpc: '2.0';
method: string;
params?: Record<string, unknown>;
};

/**
* Check if the data is a story notification.
*
* @param data - data to check
* @returns true if the data is a story notification
*/
export const isStoryNotification = (data: unknown): data is StoryNotification =>
typeof data === 'object' &&
data !== null &&
(data as StoryNotification).jsonrpc === '2.0' &&
typeof (data as StoryNotification).method === 'string';

/**
* Post a story notification.
*
* @param method - method to post
* @param params - parameters to post
*/
export const postStoryNotification = (
method: string,
params?: Record<string, unknown>
): void => {
window.parent.postMessage({ jsonrpc: '2.0', method, params }, '*');
};
43 changes: 43 additions & 0 deletions packages/eui-vanilla/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

/// <reference types="vite/client" />

import type { Preview } from '@storybook/html-vite';

import { mcpDecorator } from './mcp-decorator';
import './fonts.css';

import.meta.glob('../generated/*.css', { eager: true });

const preview: Preview = {
globalTypes: {
theme: {
description: 'Theme',
toolbar: {
title: 'Theme',
items: [
{ value: 'light', title: 'Light' },
{ value: 'dark', title: 'Dark' },
],
},
},
},
initialGlobals: {
theme: 'light',
},
decorators: [mcpDecorator],
parameters: {
options: {
showPanel: true,
},
layout: 'padded',
},
};

export default preview;
1 change: 1 addition & 0 deletions packages/eui-vanilla/.storybook/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
84 changes: 84 additions & 0 deletions packages/eui-vanilla/BENCHMARK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Benchmark

**Measured 7 Sep 2026.**

This document serves the purpose of comparing vanilla EUI vs full `@elastic/eui` on the button example.

tl;dr; **Vanilla is 82× smaller gzip** (4.0 kB vs 329 kB).

## Results

| | Vanilla | React only | EUI |
| --- | --- | --- | --- |
| JS raw | 2.0 kB | 139.2 kB | 1,079 kB |
| JS gzip | 1.0 kB | 44.6 kB | 329 kB |
| JS brotli | 0.9 kB | 39.1 kB | 242 kB |
| CSS raw | 32.2 kB | - | in JS |
| CSS gzip | 3.0 kB | - | in JS |
| Payload gzip | 4.0 kB | 44.6 kB | 329 kB |
| Payload brotli | 3.3 kB | 39.1 kB | 242 kB |
| Modules | 4 | 8 | 1,452 |
| JS parse (median) | 0.03 ms | 1.6 ms | 14.1 ms |

Vanilla payload = JS + generated CSS.

React-only and EUI have no separate CSS file. Emotion emits CSS from JS at runtime.

Vanilla gzip is mostly CSS (3.0 kB of 4.0 kB). The mount script is 1.0 kB gzip.

React alone, with a native `<button>` and no EUI, is already 45 kB gzip.

## What is in the EUI bundle

Deep-importing `EuiButton` + `EuiProvider` produced the same 1.08 MB as `import { EuiButton, EuiProvider } from '@elastic/eui'`.

Tree-shaking dropped unused files (1,452 vs 2,604 modules). It did not shrink the minified output.

Source graph before minify (4.2 MB):

| Share | Package |
| --- | --- |
| 55% | EUI source |
| 24% | `@elastic/eui-theme-common` + `@elastic/eui-theme-borealis` |
| 4% | React + ReactDOM |
| 4% | moment |
| 3% | lodash |
| 3% | chroma-js |
| 2% | Emotion |
| 6% | other (focus-lock, uuid, numeral...) |

## Methodology

Three bundles. Same tool. Same target.

1. **Vanilla** - `mountButton` with `{ label: 'Button', display: 'fill' }`, plus `generated/base.css` and `generated/button.css`.
2. **EUI** - `createRoot` + `EuiProvider` + `<EuiButton fill>Button</EuiButton>`. Source is `packages/eui/src`, not the published `es/` build.
3. **React only** - `createRoot` rendering a native `<button>`. Floor for any React iframe.

esbuild: bundle, minify, IIFE, `es2020`. `process.env.NODE_ENV` is `"production"`.

Sizes: raw bytes, gzip `-9`, Node brotli.

Parse: `new Function` in Node (V8). Compile only. Do not execute. Median of 25 runs.

Fonts are not in any payload. The host is expected to load Inter.

Vanilla CSS includes the base reset and every button size, color and display. It is not fill-only.

## Not measured

- First paint, layout or runtime CPU.
- Download over a real network.
- A production Kibana or Cloud UI webpack split.
- The published `@elastic/eui` `es/` build.

## Reproduce

```bash
yarn workspace @elastic/eui-vanilla generate
yarn workspace @elastic/eui-vanilla bench
```

Defaults to `button`. Pass another id after `--` (`yarn workspace @elastic/eui-vanilla bench -- badge`). Add `src/<id>/bench.mjs`.

Writes JSON to stdout. `--out path.json` also writes a file.
Loading
Loading