Style Isolation
Eufemia provides you with a way to isolate its styles, so you can use different Eufemia versions along side each other on the same page.
How it works
The isolated styles are in files with the suffix --isolated.min.css
. All selectors inside these files are scoped with a prefix e.g. eufemia-scope--1_2_3
, which in practice means that the original selector .dnb-button
will be .eufemia-scope--1_2_3 .dnb-button
.
<div class="eufemia-scope--1_2_3"><!-- App 1 using Eufemia v1.2.3 --><button class="dnb-button">Eufemia 1.2.3</button></div><div class="eufemia-scope--2_1_0"><!-- App 2 using Eufemia v2.1.0 --><button class="dnb-button">Eufemia 2.1.0</button></div>
In order to apply the styles, you need to wrap your app with the IsolatedStyleScope
component.
import { IsolatedStyleScope } from '@dnb/eufemia/shared'function MyApp() {return <IsolatedStyleScope>Your app content</IsolatedStyleScope>}
Optionally (but recommended), add the PostCSS plugin to your build tool. This ensures your custom styles have the correct CSS specificity when overriding or extending Eufemia styles.
How to use it
1. Import the isolated CSS files
In the root of your project, import the isolated CSS files:
import '@dnb/eufemia/style/isolated'
You can also import the isolated CSS files directly:
import '@dnb/eufemia/style/dnb-ui-basis--isolated.min.css'import '@dnb/eufemia/style/themes/theme-ui/ui-theme-components--isolated.min.css'import '@dnb/eufemia/style/themes/theme-ui/ui-theme-basis--isolated.min.css'
2. Define the scope
import { IsolatedStyleScope } from '@dnb/eufemia/shared'function MyApp() {return <IsolatedStyleScope>Your app content</IsolatedStyleScope>}
Notes:
- Remove existing
.dnb-core-style
classes. - Font files are loaded from the CDN (Read more about hosted fonts), so they are shared between Eufemia versions.
3. Add the PostCSS plugin
- Find and add a PostCSS extension or loader for your build tool.
- Then create a
postcss.config.js
file in your project root:
module.exports = {plugins: [require('@dnb/eufemia/cjs/plugins/postcss-isolated-style-scope')(/* options */),],}
In some cases your bundler may not support CJS, so you can use the ESM version of the plugin:
import styleScopePlugin from '@dnb/eufemia/plugins/postcss-isolated-style-scope.js'export default {plugins: [styleScopePlugin(/* options */)],}
The plugin accepts an options object. The default options are:
{skipClassNames: [],replaceClassNames: undefined,// { 'old-class': 'new-class' }scopeHash: 'auto',// Can be a function: (file) => stringsharedScopeHash: undefined, // Provide a function that returns an array of shared scope hashesverbose: false,}
CSS Modules are supported including SASS (SCSS) files.
CSS Specificity
When extending or overriding styles from Eufemia, it's essential to match the CSS specificity of the original selectors to ensure your styles are applied correctly.
To help with this, you can use the PostCSS plugin (style-scope) that automatically adds the required scope class to your CSS or SCSS (SASS). This ensures your styles have the necessary specificity to take effect.
.myButtonStyle:global(.dnb-button) {padding: 2rem;}
Without the PostCSS plugin, the example above will not work as expected.
The scope element
If you want to use the scope element in your app, you can use a React Hook to get the root element:
import IsolatedStyleScope, {useIsolatedStyleScope,} from '@dnb/eufemia/shared/IsolatedStyleScope'function Component() {const { getScopeElement } = useIsolatedStyleScope()React.useEffect(() => {const element = getScopeElement()}, [getScopeElement])return null}render(<IsolatedStyleScope><Component /></IsolatedStyleScope>,)
Optionally, you can provide a different scope hash to the hook if you need to retrieve an element from a nested scope:
const { getScopeElement } = useIsolatedStyleScope('my-scope')
Additional information
Isolated CSS files
Every component has its own isolated CSS file. You can import them directly:
import '@dnb/eufemia/style/components/button/button--isolated.min.css'
Omit selectors from the scope
You can prepend your selectors with :not(#isolated)
which will omit the scope class from the selector. Also, the selector :not(#isolated)
will be removed.
:not(#isolated) .my-global-class {}
Overwrite the given scope hash
By using the PostCSS plugin, you can overwrite the given scope hash by providing a string or function that returns a string.
{scopeHash: (file) => 'my-hash',}
Additionally, you then also need to provide the same scope hash to the Eufemia component.
import { IsolatedStyleScope } from '@dnb/eufemia/shared/IsolatedStyleScope'function MyApp() {return (<IsolatedStyleScope scopeHash="my-hash">Your app content</IsolatedStyleScope>)}
Shared scopes
The PostCSS plugin supports shared scopes. This means that you can use the same selector in one file and have it be duplicated for multiple scopes.
In order to do that, you can provide a function that returns an array of shared scope hashes.
{sharedScopeHash: () => ['shared-1', 'shared-2'],}
This will create duplicate selectors for each shared scope.
.main-scope .my-class,.shared-1 .my-class,.shared-2 .my-class {/* Styles for Eufemia v1.2.3 */}
Get the current scope hash
You can use getStyleScopeHash
to get the scope hash for the current Eufemia version via the IsolatedStyleScope
component:
import { getStyleScopeHash } from '@dnb/eufemia/shared/IsolatedStyleScope'getStyleScopeHash() // 'eufemia-scope--1_2_3'
Selector transformation and scoping behavior
General Rules
- CSS
:root
selectors are rewritten to target the scoped container. e.g.:root {}
→.eufemia-scope--1_2_3 {}
- A
html
selector is left untouched and continues to target the global<html>
element. e.g.html {}
→html {}
Additional Cases
-
A
body
selector remains unchanged. e.g.body {}
→body {}
-
A selector like
body .my-class
is scoped so that the class is prefixed, butbody
remains global. e.g.body .my-class
→body .eufemia-scope--1_2_3 .my-class
-
Combined
html body
selectors remain untouched. e.g.html body {}
→html body {}
-
A combined selector like
html body .my-class
results in only.my-class
being scoped. e.g.html body .my-class
→html body .eufemia-scope--1_2_3 .my-class
-
Class selectors are prefixed with the scope class. e.g.
.my-class
→.eufemia-scope--1_2_3 .my-class
-
ID selectors are scoped similarly. e.g.
#header
→.eufemia-scope--1_2_3 #header
-
Tag selectors like
strong
,em
, orinput
are scoped. e.g.strong
→.eufemia-scope--1_2_3 strong
-
Attribute selectors are scoped. e.g.
[data-test]
→.eufemia-scope--1_2_3 [data-test]
-
Pseudo-classes are preserved after scoping. e.g.
.button:hover
→.eufemia-scope--1_2_3 .button:hover
-
Pseudo-elements are scoped. e.g.
.icon::before
→.eufemia-scope--1_2_3 .icon::before
-
The universal selector
*
, and pseudo-elements like::before
and::after
, are scoped when grouped. e.g.*, ::before, ::after
→.eufemia-scope--1_2_3 *, .eufemia-scope--1_2_3 ::before, .eufemia-scope--1_2_3 ::after
-
Selectors starting with
:not(#isolated)
are excluded from scoping. e.g.:not(#isolated) .x
→.x
-
Selectors already containing the correct scope (e.g.
.eufemia-scope--something
) are not scoped again. -
@keyframes
are omitted from scoping for now.
CSS Modules Specific
-
When
runAsCssModule
istrue
, scope classes are injected using:global(...)
. e.g..my-class
→:global(.eufemia-scope--1_2_3) .my-class
-
A top-level
:global
block is replaced with a scoped global. e.g.:global {}
→:global(.eufemia-scope--1_2_3) {}
-
A leading
:global
selector chain is wrapped accordingly. e.g.:global .x
→:global(.eufemia-scope--1_2_3) :global .x
Special Configurations
-
replaceClassNames
: Specific class names are renamed before scoping. e.g..old-name
→.eufemia-scope--1_2_3 .new-name
with{ 'old-name': 'new-name' }
-
skipClassNames
: These classes are never scoped. e.g..skip-me
→.skip-me
with['skip-me']
-
sharedScopeHash
: Selectors are duplicated for each shared scope. e.g..my-class
→.main-scope .my-class, .shared-1 .my-class, .shared-2 .my-class
withsharedScopeHash: () => ['shared-1', 'shared-2']
-
scopeHash: 'auto'
: Reads fromscope-hash.txt
if available, or falls back to default. e.g. (scopeHash: 'my-hash'
)my-hash
→.my-hash .my-class
with a string:scopeHash: 'my-hash'
with a function:scopeHash: (file) => 'my-hash'