[EuiIllustration] Add adaptive light-dark illustration variant - #9797
[EuiIllustration] Add adaptive light-dark illustration variant#9797clintandrewhall wants to merge 3 commits into
Conversation
Generate a single `adaptive` SVG per asset by merging the `light`/`dark` pair, rewriting differing colors into CSS `light-dark()` so a single string recolors via the ancestor `color-scheme` with no runtime SVG swap. Assets whose `light`/`dark` markup is not structurally identical keep `light`/`dark` only. The adaptive SVG is also emitted as `@elastic/eui-illustrations/svgs/<name>.adaptive.svg` for `<img>`/CSS consumers. `EuiIllustration` now renders the adaptive SVG when present, setting `color-scheme` from the active EUI theme, and falls back to the discrete `light`/`dark` markup otherwise.
There was a problem hiding this comment.
Pull request overview
Adds a CSS light-dark()-driven “adaptive” illustration variant to @elastic/eui-illustrations, and updates EuiIllustration + docs/stories so illustrations can recolor between light/dark without swapping SVG markup at runtime.
Changes:
- Extend illustration assets to optionally include a single
adaptiveSVG (generated by merginglight/darkcolor differences intolight-dark()). - Emit
.adaptive.svgfiles for<img>/CSS consumers and export them via@elastic/eui-illustrations/svgs/*. - Prefer
adaptiveinEuiIllustration(with feature detection +color-schemewiring), plus docs/storybook/test updates.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/website/docs/components/display/illustrations/index.mdx | Updates website docs and adds an “Adaptive color modes” demo snippet. |
| packages/illustrations/src/types.ts | Adds adaptive?: string to the illustrations package type contract. |
| packages/illustrations/scripts/generate.js | Generates merged adaptive SVG markup and writes src/generated/svgs/*.adaptive.svg. |
| packages/illustrations/README.md | Documents the new adaptive variant and consumption patterns. |
| packages/illustrations/package.json | Adds ./svgs/* export and a build:svgs copy step. |
| packages/illustrations/changelogs/upcoming/adaptive_illustrations.md | Changelog entry for adaptive variants + emitted .adaptive.svg files. |
| packages/eui/src/components/illustration/illustration.tsx | Prefers adaptive markup when supported and pins color-scheme from EUI theme. |
| packages/eui/src/components/illustration/illustration.test.tsx | Adds tests covering adaptive preference vs fallback behavior. |
| packages/eui/src/components/illustration/illustration.stories.tsx | Adds an Adaptive story demonstrating color-scheme behaviors. |
| packages/eui/changelogs/upcoming/adaptive_illustrations.md | Changelog entry for EuiIllustration adaptive rendering behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
💚 Build Succeeded
History
|
💚 Build Succeeded
History
|
| // Sentinels resolved to real CSS in `AdaptiveExample`: `EuiProvider` to the | ||
| // live theme color mode (a module-level const can't read `useEuiTheme()`), and | ||
| // `system` to `light dark` (the value that follows `prefers-color-scheme`). |
There was a problem hiding this comment.
nit:
I think this comment is redundant. It's normal we cannot use hooks at a module level, we see PROVIDER_SCHEME set to EuiProvider and SYSTEM_SCHEME to system. If SYSTEM_SCHEME needs more explanation, we can add a JSDoc comment there that explains the value can be system | light | dark (prefers-color-scheme).
| const svg = colorMode === 'DARK' ? type.dark : type.light; | ||
| const isDark = colorMode === 'DARK'; | ||
| const useAdaptive = type.adaptive != null && supportsLightDark(); | ||
| const svg = useAdaptive ? type.adaptive! : isDark ? type.dark : type.light; |
There was a problem hiding this comment.
nit:
| const svg = useAdaptive ? type.adaptive! : isDark ? type.dark : type.light; | |
| const svg = useAdaptive ? type.adaptive : isDark ? type.dark : type.light; |
| /** | ||
| * A single optimized SVG whose colors resolve at runtime via CSS | ||
| * `light-dark()`, driven by the active `color-scheme`. Absent when the | ||
| * `light`/`dark` files are not structurally identical and cannot be safely | ||
| * auto-merged; consumers must fall back to {@link light}/{@link dark}. | ||
| */ |
There was a problem hiding this comment.
nit:
Just an opinionated rewrite to follow the wording of other JSDoc comments:
| /** | |
| * A single optimized SVG whose colors resolve at runtime via CSS | |
| * `light-dark()`, driven by the active `color-scheme`. Absent when the | |
| * `light`/`dark` files are not structurally identical and cannot be safely | |
| * auto-merged; consumers must fall back to {@link light}/{@link dark}. | |
| */ | |
| /** | |
| * Optimized SVG markup for the color-adaptive mode. | |
| * | |
| * Colors resolve at runtime via CSS `light-dark()`, driven by the active | |
| * `color-scheme`. Absent when the `light`/`dark` files are not structurally | |
| * identical and cannot be safely auto-merged; | |
| * consumers must fall back to {@link light}/{@link dark}. | |
| */ |
There was a problem hiding this comment.
non-blocking thought:
We should probably add tests for this script. Not necessarily on this PR.
| padding: 8px; | ||
| border-radius: 4px; |
There was a problem hiding this comment.
suggestion:
| padding: 8px; | |
| border-radius: 4px; | |
| padding: ${euiTheme.size.s}; | |
| border-radius: ${euiTheme.border.radius.medium}; |
| * @remarks | ||
| * The `light`/`dark` markup is inlined via `dangerouslySetInnerHTML`. Only pass | ||
| * trusted, package-authored SVGs — never untrusted or user-supplied markup, as | ||
| * it would be rendered as-is and could introduce an XSS vulnerability. | ||
| */ |
There was a problem hiding this comment.
suggestion:
We should mention adaptive here as well.
| * @remarks | |
| * The `light`/`dark`/`adaptive` markup is inlined via `dangerouslySetInnerHTML`. Only pass | |
| * trusted, package-authored SVGs — never untrusted or user-supplied markup, as | |
| * it would be rendered as-is and could introduce an XSS vulnerability. | |
| */ |
| // Pins `color-scheme` so the adaptive SVG's `light-dark()` colors follow the | ||
| // EUI color mode rather than the OS preference. | ||
| const inlineStyle = useAdaptive | ||
| ? { colorScheme: isDark ? 'dark' : 'light', ...style } |
There was a problem hiding this comment.
non-blocking suggestion:
Do we want to reverse to avoid consumers overriding it? Unlikely scenario.
| ? { colorScheme: isDark ? 'dark' : 'light', ...style } | |
| ? { ...style, colorScheme: isDark ? 'dark' : 'light' } |
There was a problem hiding this comment.
nit:
We should disable controls for Adaptive story. We don't need them there and they don't work anyway.
There was a problem hiding this comment.
non-blocking question:
The point of stories for the components is to showcase the component usage. But here we are showcasing @elastic/eui-illustration usage. So maybe we can rely on the documentation website entry instead?
| /** | ||
| * Most assets ship a single \`adaptive\` SVG whose colors resolve via CSS | ||
| * \`light-dark()\`. \`EuiIllustration\` sets \`color-scheme\` from the EUI theme; | ||
| * this story sets it manually so the same string renders pinned \`light\`, | ||
| * pinned \`dark\`, following \`EuiProvider\`, and following the OS | ||
| * (\`light dark\`, via \`prefers-color-scheme\`) at once. | ||
| * \`aerospace\` has no \`adaptive\` variant and cannot adapt. | ||
| */ |
There was a problem hiding this comment.
nit:
Do we even need this comment? It's verbose, the escaping makes it hard to read and the story is self-explanatory?
| * falls back to {@link light}/{@link dark}. Inlined verbatim — see the | ||
| * interface's security note. | ||
| */ | ||
| readonly adaptive?: string; |
There was a problem hiding this comment.
nit:
Little too verbose for my taste as well:
| readonly adaptive?: string; | |
| /** Trusted SVG markup for both color modes via CSS `light-dark()`. Inlined verbatim — see the interface's security note. */ | |
| readonly adaptive?: string; |

Note
EUI implementation of elastic/kibana#221938
Ideally, these would not be hard-coded values, but rather CSS variables that are shared throughout, defined by
EuiProvider. But as EUI doesn't currently define them, we'll work with hardcoded for now.One of the purposes of this approach was to prevent having to maintain a
.light.svgand.dark.svg, but since we're maintaining two anyway, we might not need this as much as before. Open to discussion.Summary
Adds a color-mode adaptive variant to
@elastic/eui-illustrationsand wiresEuiIllustrationto use it, so a single SVG string recolors between light and dark via CSSlight-dark()— no runtime SVG swap.Light System, Light Theme
Light System, Dark Theme
Dark System, Light Theme
Dark System, Dark Theme
packages/illustrations/scripts/generate.js): for each asset, merge thelight/darkpair by pairing elements and rewriting differing color properties (fill,stroke,stop-color, etc.) intolight-dark(<light>, <dark>)inline styles, producing a singleadaptiveSVG. The merge runs as a dedicated single pass (with an idempotency guard) solight-dark()values are never nested, andminifyStylesis disabled so the functions survive optimization. Assets whoselight/darkmarkup is not structurally identical are skipped and keeplight/darkonly.EuiIllustrationSource.adaptive, optional) and also emitted as@elastic/eui-illustrations/svgs/<name>.adaptive.svg(withcolor-scheme: light darkon the root) for<img>/CSS consumers. Abuild:svgsstep copies these intolib/svgs.EuiIllustration: renders the adaptive SVG when present and supported (feature-detected viaCSS.supports('color', 'light-dark(...)'), defaulting on for SSR), settingcolor-schemefrom the active EUI theme. Falls back to the discretelight/darkmarkup otherwise.Adaptivestory showing one string undercolor-scheme: light,dark, followingEuiProvider, and following the OS (light dark), plus a matching website demo and README updates.Test plan
yarn workspace @elastic/eui-illustrations buildregenerates adaptive SVGs without nestedlight-dark().illustration.test.tsxpass (adaptive preferred when supported; discrete fallback otherwise; a11y labelling).Display/EuiIllustration/Adaptive:light/darkpin correctly,EuiProviderfollows the theme toggle, andsystemfollows the OS/browserprefers-color-scheme.