YdsThemeProvider
YdsThemeProvider applies YDS color and shadow tokens as CSS custom properties to a subtree. It injects a <style> tag next to children and renders a wrapper div (display: contents) that carries the tokens. It never changes the document theme unless you ask it to.
Theming the whole document is CSS's job: import the token files onto :root. Reach for the provider when a section needs a different theme, or when a theme has to change in React.
Code example
import { YdsThemeProvider, Button } from '@yleisradio/yds-components-react';
<YdsThemeProvider theme="dark">
<Button text="Dark-themed button" />
</YdsThemeProvider>
Breaking changes: version 3.0
Upgrading YdsThemeProvider from 2.x. Package-wide list: Breaking changes in React package 3.0.
| 2.x | 3.0 |
|---|---|
Tokens via styled-components context (theme.yds.*) | CSS custom properties (var(--yds-color-*)) injected as a <style> tag |
theme defaulted to 'light' | theme defaults to 'default' (follows prefers-color-scheme). Pass theme="light" to force light theme |
unit and baseUnit converted font sizes at runtime | Props removed. Component CSS uses rem with a 1rem = 16px baseline. Convert in the app with PostCSS (postcss-rem-to-pixel, postcss-pxtorem) if needed |
| Tokens reached every descendant through React context, including portals | Tokens are inherited through the DOM. The provider themes its own subtree, not the document. Use render to choose the element, or scope to target a selector |
| Required around the app for YDS components to have a theme | Optional, and no longer the recommended way to theme a document. Import token CSS onto :root and keep the provider for sections and React-driven switching |
YDS theme tokens, such as theme.yds.BACKGROUND can be used in styled-components inside YdsThemeProvider | You need to provide JS theme from yds-core on app side or migrate to css custom properties (var(--yds-color-background)) |
styled-components is no longer a peer dependency of @yleisradio/yds-components-react. If the app keeps it, see Using styled-components with YDS 3.0.
Why to use
YDS components read colors from CSS custom properties (var(--yds-color-*)). Those variables are inherited, so they must exist on a DOM ancestor of every component.
YdsThemeProvider writes those variables for one subtree. Use it when you need to:
- Apply a different theme to a section — a dark sidebar on a light page, a branded widget — without touching the host page's CSS.
- Switch a theme in React (
light,dark,default, or a customThemeobject) in response to state. - Pass a theme generated by
@yleisradio/yds-theme-custom.
You do not need it for the document theme.
Document theming is CSS
Import token CSS onto :root once — no provider, no extra DOM node, no runtime <style> tag. File lists and wrappers: Get started.
This provider is the alternative when a subtree needs a different theme, the theme must change in React, or you pass a custom Theme object. scope=":root" exists as a migration step — see below.
Theming a section
Wrap the section. The provider renders a div with display: contents, so it carries tokens without producing a box of its own.
import { YdsThemeProvider, Button } from '@yleisradio/yds-components-react';
<YdsThemeProvider theme="dark">
<Button text="Dark-themed button" />
</YdsThemeProvider>;
If you already render an element there, pass it as render and skip the extra node. The theme class is merged with the element's own className:
<YdsThemeProvider theme="dark" render={<aside className="sidebar" />}>
<Navigation />
</YdsThemeProvider>
theme defaults to 'default', which follows prefers-color-scheme. Use 'light' or 'dark' to force a palette.
Sibling providers are independent, so two themes can sit side by side on the same page without either winning.
When to use
- Use
YdsThemeProviderfor a section that needs a different theme (a dark sidebar, a custom-branded widget). - Use it when the theme has to change in React — a user toggle, a per-page override.
- Use it for custom themes from
@yleisradio/yds-theme-customfor a brand-specific palette. - Use
renderwhen you already have an element at that spot, so the theme class lands on it instead of on an extradiv.
- Don't use it as the document theme. Import the token CSS onto
:rootinstead — Get started. - Don't wrap every individual component in its own provider.
- Don't hardcode raw colors inside components instead of theme tokens.
Migrating a whole-app provider
In 2.x, tokens flowed through React context, so a provider around the app reached everything. Tokens are now inherited through the DOM, and the provider only themes its own subtree.
Preferred: drop the root provider and import the token CSS onto :root. Keep providers only where a section or a toggle needs one.
Transitional: if you cannot change the app's CSS setup yet, scope=":root" restores the old reach. It renders nothing and writes the tokens straight to the document.
<YdsThemeProvider theme="dark" scope=":root">
<App />
</YdsThemeProvider>
Treat this as a migration step, not a destination. It puts a runtime <style> in the body, it pins the document theme for anything else on the page, and only one provider on a page can sensibly own :root.
Key YdsThemeProvider Props
theme
Specifies the theme to apply.
| Value / Type | Description |
|---|---|
'light' | Standard light theme. |
'dark' | Standard dark theme. |
'default' | Light tokens, plus dark tokens inside prefers-color-scheme: dark. This is the default. |
Theme object | Custom palette. Obtain one from customTheme() in @yleisradio/yds-theme-custom. |
Code example
import { YdsThemeProvider } from '@yleisradio/yds-components-react';
import customTheme from '@yleisradio/yds-theme-custom';
import { color } from '@yleisradio/yds-core';
<YdsThemeProvider theme="light"> ... </YdsThemeProvider>
<YdsThemeProvider theme="dark"> ... </YdsThemeProvider>
<YdsThemeProvider theme="default"> ... </YdsThemeProvider>
const theme = customTheme(color.GRAY_95, color.NEWS_VIOLET_30);
<YdsThemeProvider theme={theme}> ... </YdsThemeProvider>
render
Element that receives the theme class instead of the default wrapper div. Its own className is kept, and children come from the provider.
Use it whenever the subtree already starts with an element of yours — one node instead of two, and the element keeps its own layout rather than the wrapper's display: contents.
Code example
<YdsThemeProvider theme="dark" render={<aside className="sidebar" />}>
<Navigation />
</YdsThemeProvider>
{/* Renders <aside class="sidebar yds-theme-dark"> */}
scope
CSS selector to apply tokens to. Nothing is rendered around children, so the selector has to match an element you already own.
Use it for elements outside the provider's reach: ':root' or 'body' for a document theme, or a class on markup React does not render.
scope and render are mutually exclusive — the selector decides where tokens land, so there is nothing left for render to do.
Code example
<YdsThemeProvider theme="dark" scope=".sidebar">
{/* Put className="sidebar" on your own element */}
</YdsThemeProvider>
<YdsThemeProvider theme="dark" scope=":root">
<App />
</YdsThemeProvider>
Nesting providers
Each provider targets its own class, so an inner one overrides the outer theme for its subtree. Nesting needs no extra props.
Code example
<YdsThemeProvider theme="light">
<MainContent />
<YdsThemeProvider theme="dark">
<DarkSidebar />
</YdsThemeProvider>
</YdsThemeProvider>
YDS overlays (Modal, Dialog, BottomSheet) use the native <dialog> element and stay where they are in the DOM, so they inherit the theme of the provider they sit inside. Content you portal elsewhere yourself does not. Read useThemeContext().className and put it on the portal container:
Code example
import { useThemeContext } from '@yleisradio/yds-components-react';
const { className } = useThemeContext() ?? {};
createPortal(<div className={className}>{content}</div>, document.body);
Custom themes with yds-theme-custom
Use @yleisradio/yds-theme-custom to generate a Theme object from any background and highlight color pair, then pass it to YdsThemeProvider. The palette stays on the widget, so it is safe on a host page that already has tokens.
import { YdsThemeProvider } from '@yleisradio/yds-components-react';
import customTheme from '@yleisradio/yds-theme-custom';
import { color } from '@yleisradio/yds-core';
function PlusDeskiWidget() {
const theme = customTheme(color.GRAY_95, color.NEWS_VIOLET_30);
return (
<YdsThemeProvider theme={theme}>
{/* All YDS components here use the custom theme */}
</YdsThemeProvider>
);
}
For guaranteed WCAG compliance regardless of input colors, use generateAccessibleCustomTheme instead:
import { generateAccessibleCustomTheme } from '@yleisradio/yds-theme-custom';
import { color } from '@yleisradio/yds-core';
const theme = generateAccessibleCustomTheme(color.GRAY_95, color.NEWS_VIOLET_30);
See the @yleisradio/yds-theme-custom README for the full API.
Light and dark custom themes
customTheme() accepts a themeType argument ('light', 'dark', or 'auto'). Generate two theme variants and switch between them — for example by responding to a user toggle, an OS preference, or a page-level configuration.
React approach
import { YdsThemeProvider } from '@yleisradio/yds-components-react';
import customTheme from '@yleisradio/yds-theme-custom';
import { color } from '@yleisradio/yds-core';
const lightTheme = customTheme(color.WHITE, color.NEWS_VIOLET_50, {}, 'light');
const darkTheme = customTheme(color.GRAY_95, color.NEWS_VIOLET_30, {}, 'dark');
function Widget({ prefersDark }: { prefersDark: boolean }) {
return (
<YdsThemeProvider theme={prefersDark ? darkTheme : lightTheme}>
{/* Components respond to the active theme */}
</YdsThemeProvider>
);
}
CSS approach (class-based or prefers-color-scheme)
When you are not inside a React tree, or when you need static CSS, generate two custom themes — one forced light, one forced dark — and emit both onto a selector you own.
Pass 'light' / 'dark' as themeType so each palette uses the matching contrast rules (light background + dark text vs dark background + light text), even if the raw colors would auto-detect the other way.
import { themeToCss } from '@yleisradio/yds-components-react';
import customTheme from '@yleisradio/yds-theme-custom';
import { color } from '@yleisradio/yds-core';
const lightCustom = customTheme(color.WHITE, color.NEWS_VIOLET_50, {}, 'light');
const darkCustom = customTheme(color.GRAY_95, color.NEWS_VIOLET_30, {}, 'dark');
Follow the OS with prefers-color-scheme. Light is the default; dark overrides it when the user prefers dark:
const css = `
${themeToCss('.my-widget', lightCustom)}
@media (prefers-color-scheme: dark) {
${themeToCss('.my-widget', darkCustom)}
}
`;
<div class="my-widget">…</div>
Follow a class when the app (or the user) toggles theme independently of the OS. Keep the light rule as the default, then override with a dark class on the same node or an ancestor:
const css = `
${themeToCss('.my-widget', lightCustom)}
${themeToCss('.my-widget.theme-dark', darkCustom)}
${themeToCss('.theme-dark .my-widget', darkCustom)}
`;
<div class="my-widget">Light custom palette</div>
<div class="my-widget theme-dark">Dark custom palette</div>
themeToCss with a Theme object emits color variables only. Shadow and spacing tokens still come from the document token CSS (or from 'light' / 'dark' presets, which include shadows).
A single custom theme is the same helper without a media query or dark class:
const css = themeToCss('.my-widget', darkCustom);
Or import the preset class-scoped files and put yds-theme-dark on a descendant of html (not on <html> itself — :root is html, so :root .yds-theme-dark would not match):
@import '@yleisradio/yds-core/tokens/build/css/_theme-dark.css';
@import '@yleisradio/yds-core/tokens/build/css/_shadow-dark.css';
<body class="yds-theme-dark">...</body>
Outputting only changed tokens
When embedding a custom theme inside a page that already sets most base tokens, emit only the variables that differ. Pass { onlyChanged: true } to themeToCssFragment:
import customTheme, { themeToCssFragment } from '@yleisradio/yds-theme-custom';
import { color } from '@yleisradio/yds-core';
const theme = customTheme(color.GRAY_95, color.NEWS_VIOLET_30, {}, 'dark');
const cssFragment = themeToCssFragment(theme, { onlyChanged: true, themeType: 'dark' });
This is useful for a lightweight embed (quiz, score widget) on a page that already loads the full YDS dark theme.
| Option | Type | Default | Description |
|---|---|---|---|
onlyChanged | boolean | false | When true, omit tokens whose value matches the base theme. |
themeType | 'light' | 'dark' | 'auto' | 'auto' | Which base theme to diff against. Ignored when baseTheme is provided. |
baseTheme | Theme | — | Explicit base theme object to diff against. |
Behavior
- Every provider injects a
<style>tag next tochildrenand applies the tokens to a class:yds-theme-light,yds-theme-dark,yds-theme-default, or a generatedyds-theme-custom-*for aThemeobject.scopereplaces that class with your selector. - The default wrapper uses
display: contents, so it produces no box. Flex and grid children inside it still lay out normally, but child selectors on the parent (.parent > .item,:nth-child) see the wrapper as the single child. Passrenderwhen that matters. - The innermost provider's variables win for DOM descendants of its element.
- Invalid custom theme objects from
yds-theme-customfall back to the dark base theme.
Accessibility
- Theme tokens are tested for WCAG AA contrast ratios by default. Custom themes generated by
customTheme()follow the accessibility rules described in CUSTOM_THEME_GUIDELINES.md. - For guaranteed WCAG compliance, use
generateAccessibleCustomTheme()which adjusts colors automatically if needed. - When writing only changed tokens via
onlyChanged: true, ensure the base page theme already provides accessible contrast for the tokens you are not overriding.
API Reference
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
theme | 'light' | 'dark' | 'default' | Theme | No | 'default' | Theme to apply to the subtree. |
render | ReactElement | No | — | Element that receives the theme class instead of the default wrapper div. Not allowed with scope. |
scope | string | No | — | CSS selector to apply tokens to, such as ':root'. Renders nothing. Not allowed with render. |
children | ReactNode | Yes | — | Content to render with the theme. |
Type Definitions
import type { Theme } from '@yleisradio/yds-core';
import type { ReactElement, ReactNode } from 'react';
type YdsThemeProviderProps = {
theme?: 'light' | 'dark' | 'default' | Theme;
children: ReactNode;
} & (
| { render?: ReactElement; scope?: never }
| { scope: string; render?: never }
);
useThemeContext
Returns the nearest provider's scope, or null outside one. Use className to theme an element the provider cannot reach itself.
interface ThemeScope {
selector: string;
theme: 'light' | 'dark' | 'default' | Theme;
className: string | undefined;
}
Related
- Get started with yds-components-react 3.0 — Token CSS on
:root. - Design Tokens — Theming — Overview of the token system.