Skip to content

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

  1. Find and add a PostCSS extension or loader for your build tool.
  2. 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) => string
sharedScopeHash: undefined, // Provide a function that returns an array of shared scope hashes
verbose: 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, but body remains global. e.g. body .my-classbody .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-classhtml 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, or input 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 is true, 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 with sharedScopeHash: () => ['shared-1', 'shared-2']

  • scopeHash: 'auto': Reads from scope-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'