# What's new in V4?
We have moved away from Styled Components (CSS-in-JS) to Linaria (build-time CSS extraction). This improves compatibility with projects that:
- rely on SSR
- use strict CSP
- prefer to use another styling framework
# AI agent migration prompt
If you are an AI coding agent, refer to the following instructions:
Migrate this project from @lifesg/react-design-system V3 to V4.
Read the full migration guide in this document and scan the codebase to identify all usages that need to change.
Based on the guide and your findings, create an implementation plan broken into phases. Each phase should be a logical, self-contained unit of work. In the implementation plan, explicitly conclude each phase by committing the changes in git with a descriptive message summarising what was migrated.
Execute the plan one phase at a time.
After all phases are complete:
- Verify the app compiles
- Run the full test suite and fix any failures
# Migrating from V3
If your project uses V3, follow these steps to install V4.
**Prerequisites:**
- Upgrade to the latest V3 release and ensure your build and unit tests are passing first. This will help us with troubleshooting V4-specific issues.
- Upgrade to Styled Components v6
- For NextJS projects, upgrade to v16 with Turbopack enabled
```
npm i @lifesg/react-design-system@3
```
**1. Upgrade to the V4 Design System**
```
npm i @lifesg/react-design-system@4
```
**2. Update the CSS reset**
```diff
- @import url("https://assets.life.gov.sg/react-design-system/v3/css/main.css");
+ @import url("https://assets.life.gov.sg/react-design-system/v4/css/main.css");
```
(no changes to fonts)
**3. Import the theme stylesheet**
In your application's JavaScript entrypoint (e.g. `pages/_app.tsx` or `app/layout.tsx` in a NextJS project):
```ts
import "@lifesg/react-design-system/theme/styles/lifesg.css";
```
Or in an existing global `.css` file:
```css
@import "@lifesg/react-design-system/theme/styles/lifesg.css";
```
**4. Set up the new ThemeProvider**
Replace the Styled Components `ThemeProvider` or `DSThemeProvider` with the new `ThemeProvider` from `@lifesg/react-design-system/theme`:
```diff
- import { DSThemeProvider, LifeSGTheme } from "@lifesg/react-design-system/theme";
- import { ThemeProvider } from "styled-components";
+ import { ThemeProvider } from "@lifesg/react-design-system/theme";
- {children}
- {children}
+ {children}
```
> [!IMPORTANT]
> If `DSThemeProvider` was not previously used, add `mode="light"` to disable the default dark mode in V4.
>
> ```tsx
>
> {children}
>
> ```
**5. Run codemod to migrate imports**
```console
$ npx lifesg-react-design-system
Select codemods to run:
❯◉ migrate-popover
```
**6. Migrate design token usages**
Refer to the [Design tokens in styling](#design-tokens-in-styling) and [Design tokens at runtime](#design-tokens-at-runtime) sections.
> [!TIP]
> If you encounter errors or issues, refer to the [troubleshooting](#troubleshooting) section
# Summary of changes
**General**
- [Design tokens in styling](#design-tokens-in-styling)
- [Design tokens at runtime](#design-tokens-at-runtime)
- [Breakpoint tokens in media queries](#breakpoint-tokens-in-media-queries)
- [Breakpoint tokens at runtime](#breakpoint-tokens-at-runtime)
- [Dark mode](#dark-mode)
- [ThemeProvider](#themeprovider)
- [ThemeSpec and customisation](#themespec-and-customisation)
- [Subpath imports](#subpath-imports)
- [V2 modules removed](#v2-modules-removed)
- [TypeScript theme declaration](#typescript-theme-declaration)
- [Cascade layers](#cascade-layers)
- [CSS imports](#css-imports)
**Components**
- [Border](#border)
- [BoxContainer](#boxcontainer)
- [Breakpoint](#breakpoint)
- [Button](#button)
- [ButtonWithIcon](#buttonwithicon)
- [Divider](#divider)
- [DSThemeProvider](#dsthemeprovider)
- [ErrorDisplay](#errordisplay)
- [Form.CustomField](#formcustomfield)
- [Form.Label](#formlabel)
- [IconButton](#iconbutton)
- [Markup](#markup)
- [Masonry](#masonry)
- [Modal](#modal)
- [Popover](#popover-deprecated)
- [PopoverV2](#popoverv2)
- [ProgressIndicator](#progressindicator)
- [TextList](#textlist)
- [TimeSlotBar](#timeslotbar)
- [TimeSlotBarWeek](#timeslotbarweek)
- [TimeSlotWeekView](#timeslotweekview)
- [Timetable](#timetable)
- [Tooltip](#tooltip)
- [useDSTheme](#usedstheme)
- [useTheme](#usetheme)
# Changes
## Design tokens in styling
**V3 usage**
In V3, design tokens were functions that accepted props. They could be used directly in Styled Components.
```tsx
import { Colour } from "@lifesg/react-design-system/theme";
const ExampleComponent = styled.div`
background: ${Colour["bg"]};
background: ${(props) => {
return props.disabled
? Colour["bg-disabled"](props)
: Colour["bg"](props);
}};
`;
```
**V4 usage**
In V4, design tokens are CSS variable strings. Update usages depending on the context:
1. In Styled Components (or other CSS-in-JS libraries):
```tsx
import { Colour } from "@lifesg/react-design-system/theme";
const ExampleComponent = styled.div`
background: ${Colour["bg"]};
`;
```
Note that for length values, you cannot negate variables directly. Apply `calc(${...} * -1)` instead:
```tsx
import { Spacing } from "@lifesg/react-design-system/theme";
const ExampleComponent = styled.div`
margin: calc(${Spacing["spacing-8"] * -1};
`;
```
2. In inline styles:
```tsx
```
3. In CSS modules:
```css
.example {
background: var(--fds-colour-bg-disabled);
}
```
## Design tokens at runtime
If you need the resolved CSS value at runtime (e.g. to pass to a third-party library or perform calculations), use the `useDesignToken` hook:
```tsx
import { Colour, useDesignToken } from "@lifesg/react-design-system/theme";
const ExampleComponent = () => {
const color = useDesignToken(Colour["text"]);
// color is a resolved CSS value e.g. "#1d2939"
return ;
};
```
For tokens that resolve to a numeric length unit (e.g. radius, spacing or breakpoints), use `parsePxOrRemValue` to get a number:
```tsx
import { Spacing, useDesignToken } from "@lifesg/react-design-system/theme";
import { parsePxOrRemValue } from "@lifesg/react-design-system/theme";
const ExampleComponent = () => {
const gap = parsePxOrRemValue(useDesignToken(Spacing["spacing-16"]));
// gap is a number e.g. 16
return ;
};
```
## Breakpoint tokens in media queries
In V3, breakpoint tokens were resolved to static CSS length values, so it was possible to embed them directly in media queries.
**V3 usage**
```tsx
const Container = styled.div`
@media (max-width: ${Breakpoint["sm-max"]}px) {
/* styles */
}
`;
```
**V4 usage**
In V4, breakpoint tokens are CSS variables and can no longer be used in media queries.
For responsive styling, you can use the provided `MediaQuery` directive:
```tsx
import { MediaQuery } from "@lifesg/react-design-system/theme";
const Container = styled.div`
${MediaQuery.MaxWidth.sm} {
/* styles */
}
`;
```
For advanced breakpoint detection, you can make use of the provided hooks in this way:
```tsx
import {
Breakpoint,
useResolvedBreakpointToken,
useMediaQuery,
} from "@lifesg/react-design-system/theme";
const ExampleComponent = () => {
const mobileBreakpoint = useResolvedBreakpointToken(Breakpoint["sm-max"]);
const isMobileLandscape = useMediaQuery({
clauses: [
{ feature: "orientation", value: "landscape" },
{ feature: "max-height", value: mobileBreakpoint },
],
});
return ;
};
const Container = styled.div`
&[data-mobile-landscape="true"] {
/* styles */
}
`;
```
## Breakpoint tokens at runtime
In V3, you had to extract the breakpoint value at runtime by invoking the design token.
**V3 usage**
```tsx
import { Breakpoint } from "@lifesg/react-design-system/theme";
import { useTheme } from "styled-components";
import { useMediaQuery } from "third-party-lib";
const ExampleComponent = () => {
const theme = useTheme();
const tablet = Breakpoint["lg-max"]({ theme });
const isTablet = useMediaQuery({ maxWidth: tablet }); // custom hook
};
```
**V4 usage**
In V4, media query hooks are provided out of the box:
```tsx
import {
Breakpoint,
useMaxWidthMediaQuery,
useMinWidthMediaQuery,
useMediaQuery,
} from "@lifesg/react-design-system/theme";
const ExampleComponent = () => {
const isTablet = useMaxWidthMediaQuery("lg");
const isTablet = useMinWidthMediaQuery("md");
const isTablet = useMediaQuery({ maxWidth: Breakpoint["lg-max"] });
const isTablet = useMediaQuery({ minWidth: Breakpoint["lg-min"] });
};
```
If the actual pixel value is required at runtime:
```tsx
import { Breakpoint, useResolvedBreakpointToken } from "@lifesg/react-design-system/theme";
import { parsePxOrRemValue } from "@lifesg/react-design-system/theme";
const ExampleComponent = () => {
const tablet = parsePxOrRemValue(useResolvedBreakpointToken(Breakpoint["lg-max"]));
const handleResize = () => {
if (window.innerWidth <= tablet) {
// do something
}
};
};
```
## Dark mode
In V4, dark mode is enabled by default. `ThemeProvider` follows the user's system preference unless a `mode` is explicitly set.
If your project does not support dark mode, set `mode="light"` to opt out:
```tsx
import { ThemeProvider } from "@lifesg/react-design-system/theme";
{children}
;
```
## ThemeProvider
The Styled Components `ThemeProvider` should be replaced with the new `ThemeProvider` from `@lifesg/react-design-system/theme`.
**V3 usage**
```tsx
import { LifeSGTheme } from "@lifesg/react-design-system/theme";
import { ThemeProvider } from "styled-components";
;
```
**V4 usage**
```tsx
import { ThemeProvider } from "@lifesg/react-design-system/theme";
;
```
1. The `theme` prop can be mapped as follows:
| V3 `ThemeSpec` | V4 `theme` |
| ---------------------- | ------------------- |
| `LifeSGTheme` | `lifesg` |
| `BookingSGTheme` | `bookingsg` |
| `RBSTheme` | `rbs` |
| `MyLegacyTheme` | `mylegacy` |
| `CCubeTheme` | `ccube` |
| `OneServiceTheme` | `oneservice` |
| `PATheme` | `pa` |
| `A11yPlaygroundTheme` | `a11y-playground` |
| `SupportGoWhereTheme` | `supportgowhere` |
| `SGWDigitalLobbyTheme` | `sgw-digital-lobby` |
| `IMDATheme` | `imda` |
| `SPFTheme` | `spf` |
| `SMGSTheme` | (not available yet) |
2. Make sure to import the corresponding theme stylesheet through one of these methods
1. In your Javascript entrypoint:
```ts
import "@lifesg/react-design-system/theme/styles/lifesg.css";
```
2. Or global CSS:
```css
@import url("@lifesg/react-design-system/theme/styles/lifesg.css");
```
## ThemeSpec and customisation
The `ThemeSpec` object and the `overrides`/`componentOverrides` API have been removed in V4.
**V3 usage**
```tsx
import { LifeSGTheme, ThemeSpec } from "@lifesg/react-design-system/theme";
import { ThemeProvider } from "styled-components";
const customTheme: ThemeSpec = {
...LifeSGTheme,
overrides: {
primitiveColour: {
"primary-10": "#F3C85C",
},
font: {
"heading-size-xxl": "4rem",
"heading-lh-xxl": "4.5rem",
"heading-ls-xxl": "0.056rem",
},
},
};
const App = () => {
return (
);
};
```
**V4 usage**
For ad hoc customisation, override CSS variables directly. Choose an approach based on your project's constraints:
1. **Option 1: CSS stylesheet — override entire theme**
Override the theme globally by targeting the `data-fds-theme` attribute set by `ThemeProvider`.
```css
[data-fds-theme="lifesg"] {
--fds-colour-bg-primary: #0043ce;
--fds-colour-text-inverse: #ffffff;
}
[data-fds-theme="lifesg"][data-fds-theme-mode="dark"] {
--fds-colour-bg-primary: #0043ce;
--fds-colour-text-inverse: #000000;
}
```
2. **Option 2: Inline style**
Suitable for dynamic overrides driven by runtime values.
```tsx
const App = ({ customTheme }) => {
return (
);
};
```
3. **Option 3: `useApplyStyle`**
Use this if your project enforces a strict Content Security Policy that disallows inline styles.
```tsx
import { useRef } from "react";
import { useApplyStyle } from "@lifesg/react-design-system/theme/utils";
const App = ({ customTheme }) => {
const themeRef = useRef(null);
useApplyStyle(themeRef, {
"--fds-colour-bg-primary": customTheme.primaryColour,
"--fds-colour-text-inverse": "#ffffff",
});
return (
);
};
```
A mapping of V3 design tokens to the corresponding CSS variable is provided in the following sections:
### Colour token map
**`primitiveColour`, `primitiveColourDark`**
| V3 | V4 |
| ----------------- | ------------------------------ |
| `brand-10` | `--fds-colour-brand-10` |
| `brand-20` | `--fds-colour-brand-20` |
| `brand-30` | `--fds-colour-brand-30` |
| `brand-40` | `--fds-colour-brand-40` |
| `brand-50` | `--fds-colour-brand-50` |
| `brand-60` | `--fds-colour-brand-60` |
| `brand-70` | `--fds-colour-brand-70` |
| `brand-80` | `--fds-colour-brand-80` |
| `brand-90` | `--fds-colour-brand-90` |
| `brand-95` | `--fds-colour-brand-95` |
| `brand-100` | `--fds-colour-brand-100` |
| `primary-10` | `--fds-colour-primary-10` |
| `primary-20` | `--fds-colour-primary-20` |
| `primary-30` | `--fds-colour-primary-30` |
| `primary-40` | `--fds-colour-primary-40` |
| `primary-50` | `--fds-colour-primary-50` |
| `primary-60` | `--fds-colour-primary-60` |
| `primary-70` | `--fds-colour-primary-70` |
| `primary-80` | `--fds-colour-primary-80` |
| `primary-90` | `--fds-colour-primary-90` |
| `primary-95` | `--fds-colour-primary-95` |
| `primary-100` | `--fds-colour-primary-100` |
| `secondary-10` | `--fds-colour-secondary-10` |
| `secondary-20` | `--fds-colour-secondary-20` |
| `secondary-30` | `--fds-colour-secondary-30` |
| `secondary-40` | `--fds-colour-secondary-40` |
| `secondary-50` | `--fds-colour-secondary-50` |
| `secondary-60` | `--fds-colour-secondary-60` |
| `secondary-70` | `--fds-colour-secondary-70` |
| `secondary-80` | `--fds-colour-secondary-80` |
| `secondary-90` | `--fds-colour-secondary-90` |
| `secondary-95` | `--fds-colour-secondary-95` |
| `secondary-100` | `--fds-colour-secondary-100` |
| `neutral-10` | `--fds-colour-neutral-10` |
| `neutral-20` | `--fds-colour-neutral-20` |
| `neutral-30` | `--fds-colour-neutral-30` |
| `neutral-40` | `--fds-colour-neutral-40` |
| `neutral-50` | `--fds-colour-neutral-50` |
| `neutral-60` | `--fds-colour-neutral-60` |
| `neutral-70` | `--fds-colour-neutral-70` |
| `neutral-80` | `--fds-colour-neutral-80` |
| `neutral-90` | `--fds-colour-neutral-90` |
| `neutral-95` | `--fds-colour-neutral-95` |
| `neutral-100` | `--fds-colour-neutral-100` |
| `success-10` | `--fds-colour-success-10` |
| `success-20` | `--fds-colour-success-20` |
| `success-30` | `--fds-colour-success-30` |
| `success-40` | `--fds-colour-success-40` |
| `success-50` | `--fds-colour-success-50` |
| `success-60` | `--fds-colour-success-60` |
| `success-70` | `--fds-colour-success-70` |
| `success-80` | `--fds-colour-success-80` |
| `success-90` | `--fds-colour-success-90` |
| `success-95` | `--fds-colour-success-95` |
| `success-100` | `--fds-colour-success-100` |
| `warning-10` | `--fds-colour-warning-10` |
| `warning-20` | `--fds-colour-warning-20` |
| `warning-30` | `--fds-colour-warning-30` |
| `warning-40` | `--fds-colour-warning-40` |
| `warning-50` | `--fds-colour-warning-50` |
| `warning-60` | `--fds-colour-warning-60` |
| `warning-70` | `--fds-colour-warning-70` |
| `warning-80` | `--fds-colour-warning-80` |
| `warning-90` | `--fds-colour-warning-90` |
| `warning-95` | `--fds-colour-warning-95` |
| `warning-100` | `--fds-colour-warning-100` |
| `error-10` | `--fds-colour-error-10` |
| `error-20` | `--fds-colour-error-20` |
| `error-30` | `--fds-colour-error-30` |
| `error-40` | `--fds-colour-error-40` |
| `error-50` | `--fds-colour-error-50` |
| `error-60` | `--fds-colour-error-60` |
| `error-70` | `--fds-colour-error-70` |
| `error-80` | `--fds-colour-error-80` |
| `error-90` | `--fds-colour-error-90` |
| `error-95` | `--fds-colour-error-95` |
| `error-100` | `--fds-colour-error-100` |
| `info-10` | `--fds-colour-info-10` |
| `info-20` | `--fds-colour-info-20` |
| `info-30` | `--fds-colour-info-30` |
| `info-40` | `--fds-colour-info-40` |
| `info-50` | `--fds-colour-info-50` |
| `info-60` | `--fds-colour-info-60` |
| `info-70` | `--fds-colour-info-70` |
| `info-80` | `--fds-colour-info-80` |
| `info-90` | `--fds-colour-info-90` |
| `info-95` | `--fds-colour-info-95` |
| `info-100` | `--fds-colour-info-100` |
| `white` | `--fds-colour-white` |
| `black` | `--fds-colour-black` |
| `primary-inverse` | `--fds-colour-primary-inverse` |
**`semanticColour`, `semanticColourDark`**
| V3 | V4 |
| ------------------------------- | -------------------------------------------- |
| `text` | `--fds-colour-text` |
| `text-subtle` | `--fds-colour-text-subtle` |
| `text-subtler` | `--fds-colour-text-subtler` |
| `text-subtlest` | `--fds-colour-text-subtlest` |
| `text-primary` | `--fds-colour-text-primary` |
| `text-primary-strongest` | `--fds-colour-text-primary-strongest` |
| `text-hover` | `--fds-colour-text-hover` |
| `text-selected` | `--fds-colour-text-selected` |
| `text-selected-hover` | `--fds-colour-text-selected-hover` |
| `text-disabled` | `--fds-colour-text-disabled` |
| `text-disabled-subtle` | `--fds-colour-text-disabled-subtle` |
| `text-disabled-subtlest` | `--fds-colour-text-disabled-subtlest` |
| `text-selected-disabled` | `--fds-colour-text-selected-disabled` |
| `text-success` | `--fds-colour-text-success` |
| `text-warning` | `--fds-colour-text-warning` |
| `text-error` | `--fds-colour-text-error` |
| `text-info` | `--fds-colour-text-info` |
| `text-inverse` | `--fds-colour-text-inverse` |
| `icon` | `--fds-colour-icon` |
| `icon-subtle` | `--fds-colour-icon-subtle` |
| `icon-strongest` | `--fds-colour-icon-strongest` |
| `icon-primary` | `--fds-colour-icon-primary` |
| `icon-primary-subtle` | `--fds-colour-icon-primary-subtle` |
| `icon-primary-subtlest` | `--fds-colour-icon-primary-subtlest` |
| `icon-hover` | `--fds-colour-icon-hover` |
| `icon-selected` | `--fds-colour-icon-selected` |
| `icon-selected-hover` | `--fds-colour-icon-selected-hover` |
| `icon-disabled` | `--fds-colour-icon-disabled` |
| `icon-disabled-subtle` | `--fds-colour-icon-disabled-subtle` |
| `icon-selected-disabled` | `--fds-colour-icon-selected-disabled` |
| `icon-success` | `--fds-colour-icon-success` |
| `icon-warning` | `--fds-colour-icon-warning` |
| `icon-error` | `--fds-colour-icon-error` |
| `icon-error-strong` | `--fds-colour-icon-error-strong` |
| `icon-info` | `--fds-colour-icon-info` |
| `icon-inverse` | `--fds-colour-icon-inverse` |
| `icon-primary-inverse` | `--fds-colour-icon-primary-inverse` |
| `border` | `--fds-colour-border` |
| `border-strong` | `--fds-colour-border-strong` |
| `border-stronger` | `--fds-colour-border-stronger` |
| `border-primary` | `--fds-colour-border-primary` |
| `border-primary-subtle` | `--fds-colour-border-primary-subtle` |
| `border-hover` | `--fds-colour-border-hover` |
| `border-hover-strong` | `--fds-colour-border-hover-strong` |
| `border-selected` | `--fds-colour-border-selected` |
| `border-selected-subtle` | `--fds-colour-border-selected-subtle` |
| `border-selected-subtlest` | `--fds-colour-border-selected-subtlest` |
| `border-selected-hover` | `--fds-colour-border-selected-hover` |
| `border-focus` | `--fds-colour-border-focus` |
| `border-focus-strong` | `--fds-colour-border-focus-strong` |
| `border-disabled` | `--fds-colour-border-disabled` |
| `border-selected-disabled` | `--fds-colour-border-selected-disabled` |
| `border-success` | `--fds-colour-border-success` |
| `border-warning` | `--fds-colour-border-warning` |
| `border-error` | `--fds-colour-border-error` |
| `border-error-focus` | `--fds-colour-border-error-focus` |
| `border-error-focus-strong` | `--fds-colour-border-error-focus-strong` |
| `border-error-strong` | `--fds-colour-border-error-strong` |
| `border-info` | `--fds-colour-border-info` |
| `bg` | `--fds-colour-bg` |
| `bg-strong` | `--fds-colour-bg-strong` |
| `bg-stronger` | `--fds-colour-bg-stronger` |
| `bg-strongest` | `--fds-colour-bg-strongest` |
| `bg-hover` | `--fds-colour-bg-hover` |
| `bg-hover-strong` | `--fds-colour-bg-hover-strong` |
| `bg-hover-subtle` | `--fds-colour-bg-hover-subtle` |
| `bg-hover-neutral` | `--fds-colour-bg-hover-neutral` |
| `bg-hover-neutral-strong` | `--fds-colour-bg-hover-neutral-strong` |
| `bg-selected` | `--fds-colour-bg-selected` |
| `bg-selected-hover` | `--fds-colour-bg-selected-hover` |
| `bg-selected-strong` | `--fds-colour-bg-selected-strong` |
| `bg-selected-stronger` | `--fds-colour-bg-selected-stronger` |
| `bg-selected-strongest` | `--fds-colour-bg-selected-strongest` |
| `bg-selected-strongest-hover` | `--fds-colour-bg-selected-strongest-hover` |
| `bg-disabled` | `--fds-colour-bg-disabled` |
| `bg-selected-disabled` | `--fds-colour-bg-selected-disabled` |
| `bg-selected-stronger-disabled` | `--fds-colour-bg-selected-stronger-disabled` |
| `bg-success` | `--fds-colour-bg-success` |
| `bg-success-hover` | `--fds-colour-bg-success-hover` |
| `bg-success-strong` | `--fds-colour-bg-success-strong` |
| `bg-success-strong-hover` | `--fds-colour-bg-success-strong-hover` |
| `bg-warning` | `--fds-colour-bg-warning` |
| `bg-warning-hover` | `--fds-colour-bg-warning-hover` |
| `bg-warning-strong` | `--fds-colour-bg-warning-strong` |
| `bg-warning-strong-hover` | `--fds-colour-bg-warning-strong-hover` |
| `bg-info` | `--fds-colour-bg-info` |
| `bg-info-hover` | `--fds-colour-bg-info-hover` |
| `bg-info-strong` | `--fds-colour-bg-info-strong` |
| `bg-info-strong-hover` | `--fds-colour-bg-info-strong-hover` |
| `bg-error` | `--fds-colour-bg-error` |
| `bg-error-hover` | `--fds-colour-bg-error-hover` |
| `bg-error-strong` | `--fds-colour-bg-error-strong` |
| `bg-error-strong-hover` | `--fds-colour-bg-error-strong-hover` |
| `bg-inverse` | `--fds-colour-bg-inverse` |
| `bg-inverse-subtle` | `--fds-colour-bg-inverse-subtle` |
| `bg-inverse-subtler` | `--fds-colour-bg-inverse-subtler` |
| `bg-inverse-subtlest` | `--fds-colour-bg-inverse-subtlest` |
| `bg-inverse-hover` | `--fds-colour-bg-inverse-hover` |
| `bg-primary` | `--fds-colour-bg-primary` |
| `bg-primary-subtle` | `--fds-colour-bg-primary-subtle` |
| `bg-primary-subtler` | `--fds-colour-bg-primary-subtler` |
| `bg-primary-subtlest` | `--fds-colour-bg-primary-subtlest` |
| `bg-available` | `--fds-colour-bg-available` |
| `bg-primary-hover` | `--fds-colour-bg-primary-hover` |
| `bg-primary-subtlest-hover` | `--fds-colour-bg-primary-subtlest-hover` |
| `bg-primary-subtlest-selected` | `--fds-colour-bg-primary-subtlest-selected` |
| `overlay-strong` | `--fds-colour-overlay-strong` |
| `overlay-subtle` | `--fds-colour-overlay-subtle` |
| `hyperlink` | `--fds-colour-hyperlink` |
| `hyperlink-hover` | `--fds-colour-hyperlink-hover` |
| `hyperlink-visited` | `--fds-colour-hyperlink-visited` |
| `hyperlink-inverse` | `--fds-colour-hyperlink-inverse` |
| `focus-ring` | `--fds-colour-focus-ring` |
| `focus-ring-inverse` | `--fds-colour-focus-ring-inverse` |
### Font token map
**`fontSpec`**
| V3 | V4 |
| ----------------------- | ---------------------------------- |
| `heading-size-xxl` | `--fds-font-heading-size-xxl` |
| `heading-size-xl` | `--fds-font-heading-size-xl` |
| `heading-size-lg` | `--fds-font-heading-size-lg` |
| `heading-size-md` | `--fds-font-heading-size-md` |
| `heading-size-sm` | `--fds-font-heading-size-sm` |
| `heading-size-xs` | `--fds-font-heading-size-xs` |
| `heading-lh-xxl` | `--fds-font-heading-lh-xxl` |
| `heading-lh-xl` | `--fds-font-heading-lh-xl` |
| `heading-lh-lg` | `--fds-font-heading-lh-lg` |
| `heading-lh-md` | `--fds-font-heading-lh-md` |
| `heading-lh-sm` | `--fds-font-heading-lh-sm` |
| `heading-lh-xs` | `--fds-font-heading-lh-xs` |
| `heading-ls-xxl` | `--fds-font-heading-ls-xxl` |
| `heading-ls-xl` | `--fds-font-heading-ls-xl` |
| `heading-ls-lg` | `--fds-font-heading-ls-lg` |
| `heading-ls-md` | `--fds-font-heading-ls-md` |
| `heading-ls-sm` | `--fds-font-heading-ls-sm` |
| `heading-ls-xs` | `--fds-font-heading-ls-xs` |
| `weight-light` | `--fds-font-weight-light` |
| `weight-regular` | `--fds-font-weight-regular` |
| `weight-semibold` | `--fds-font-weight-semibold` |
| `weight-bold` | `--fds-font-weight-bold` |
| `font-family` | Removed |
| `heading-font-family` | `--fds-font-family-heading` |
| `body-font-family` | `--fds-font-family-body` |
| `body-size-baseline` | `--fds-font-body-size-baseline` |
| `body-size-md` | `--fds-font-body-size-md` |
| `body-size-sm` | `--fds-font-body-size-sm` |
| `body-size-xs` | `--fds-font-body-size-xs` |
| `body-lh-baseline` | `--fds-font-body-lh-baseline` |
| `body-lh-md` | `--fds-font-body-lh-md` |
| `body-lh-sm` | `--fds-font-body-lh-sm` |
| `body-lh-xs` | `--fds-font-body-lh-xs` |
| `body-ls-baseline` | `--fds-font-body-ls-baseline` |
| `body-ls-md` | `--fds-font-body-ls-md` |
| `body-ls-sm` | `--fds-font-body-ls-sm` |
| `body-ls-xs` | `--fds-font-body-ls-xs` |
| `form-label-size` | `--fds-font-form-label-size` |
| `form-description-size` | `--fds-font-form-description-size` |
| `form-label-lh` | `--fds-font-form-label-lh` |
| `form-description-lh` | `--fds-font-form-description-lh` |
| `form-label-ls` | `--fds-font-form-label-ls` |
| `form-description-ls` | `--fds-font-form-description-ls` |
| - | `--fds-font-variant` |
**`font`**
⚠️ Not available in V4, derived from `fontSpec`
### Breakpoint token map
**`breakpoint`**
| V3 | V4 |
| ------------ | ----------------------------- |
| `xxs-min` | `--fds-breakpoint-xxs-min` |
| `xxs-max` | `--fds-breakpoint-xxs-max` |
| `xs-min` | `--fds-breakpoint-xs-min` |
| `xs-max` | `--fds-breakpoint-xs-max` |
| `sm-min` | `--fds-breakpoint-sm-min` |
| `sm-max` | `--fds-breakpoint-sm-max` |
| `md-min` | `--fds-breakpoint-md-min` |
| `md-max` | `--fds-breakpoint-md-max` |
| `lg-min` | `--fds-breakpoint-lg-min` |
| `lg-max` | `--fds-breakpoint-lg-max` |
| `xl-min` | `--fds-breakpoint-xl-min` |
| `xl-max` | `--fds-breakpoint-xl-max` |
| `xxl-min` | `--fds-breakpoint-xxl-min` |
| `xxs-column` | `--fds-breakpoint-column-xxs` |
| `xs-column` | `--fds-breakpoint-column-xs` |
| `sm-column` | `--fds-breakpoint-column-sm` |
| `md-column` | `--fds-breakpoint-column-md` |
| `lg-column` | `--fds-breakpoint-column-lg` |
| `xl-column` | `--fds-breakpoint-column-xl` |
| `xxl-column` | `--fds-breakpoint-column-xxl` |
| `xxs-gutter` | `--fds-breakpoint-gutter-xxs` |
| `xs-gutter` | `--fds-breakpoint-gutter-xs` |
| `sm-gutter` | `--fds-breakpoint-gutter-sm` |
| `md-gutter` | `--fds-breakpoint-gutter-md` |
| `lg-gutter` | `--fds-breakpoint-gutter-lg` |
| `xl-gutter` | `--fds-breakpoint-gutter-xl` |
| `xxl-gutter` | `--fds-breakpoint-gutter-xxl` |
| `xxs-margin` | `--fds-breakpoint-margin-xxs` |
| `xs-margin` | `--fds-breakpoint-margin-xs` |
| `sm-margin` | `--fds-breakpoint-margin-sm` |
| `md-margin` | `--fds-breakpoint-margin-md` |
| `lg-margin` | `--fds-breakpoint-margin-lg` |
| `xl-margin` | `--fds-breakpoint-margin-xl` |
| `xxl-margin` | `--fds-breakpoint-margin-xxl` |
### Other token maps
**`motion`**
| V3 | V4 |
| --------------- | ----------------------------------- |
| `duration-150` | `--fds-motion-duration-150` |
| `duration-250` | `--fds-motion-duration-250` |
| `duration-350` | `--fds-motion-duration-350` |
| `duration-500` | `--fds-motion-duration-500` |
| `duration-800` | `--fds-motion-duration-800` |
| `duration-1000` | `--fds-motion-duration-1000` |
| `ease-default` | `--fds-motion-timing-ease-default` |
| `ease-standard` | `--fds-motion-timing-ease-standard` |
| `ease-entrance` | `--fds-motion-timing-ease-entrance` |
| `ease-exit` | `--fds-motion-timing-ease-exit` |
**`spacing`**
| V3 | V4 |
| ------------- | ------------------- |
| `spacing-0` | `--fds-spacing-0` |
| `spacing-4` | `--fds-spacing-4` |
| `spacing-8` | `--fds-spacing-8` |
| `spacing-12` | `--fds-spacing-12` |
| `spacing-16` | `--fds-spacing-16` |
| `spacing-20` | `--fds-spacing-20` |
| `spacing-24` | `--fds-spacing-24` |
| `spacing-32` | `--fds-spacing-32` |
| `spacing-40` | `--fds-spacing-40` |
| `spacing-48` | `--fds-spacing-48` |
| `spacing-64` | `--fds-spacing-64` |
| `spacing-72` | `--fds-spacing-72` |
| `layout-xs` | `--fds-layout-xs` |
| `layout-sm` | `--fds-layout-sm` |
| `layout-md` | `--fds-layout-md` |
| `layout-lg` | `--fds-layout-lg` |
| `layout-xl` | `--fds-layout-xl` |
| `layout-xxl` | `--fds-layout-xxl` |
| `layout-xxxl` | `--fds-layout-xxxl` |
**`border`**
| V3 | V4 |
| ----------- | -------------------------- |
| `width-005` | `--fds-border-width-005` |
| `width-010` | `--fds-border-width-010` |
| `width-020` | `--fds-border-width-020` |
| `width-040` | `--fds-border-width-040` |
| `solid` | `--fds-border-style-solid` |
**`radius`**
| V3 | V4 |
| ------ | ------------------- |
| `none` | `--fds-radius-none` |
| `xs` | `--fds-radius-xs` |
| `sm` | `--fds-radius-sm` |
| `md` | `--fds-radius-md` |
| `lg` | `--fds-radius-lg` |
| `full` | `--fds-radius-full` |
**`shadow`**
| V3 | V4 |
| ----------------- | ------------------------------ |
| `xs-subtle` | `--fds-shadow-xs-subtle` |
| `xs-strong` | `--fds-shadow-xs-strong` |
| `xs-focus-strong` | `--fds-shadow-xs-focus-strong` |
| `xs-error-strong` | `--fds-shadow-xs-error-strong` |
| `sm-subtle` | `--fds-shadow-sm-subtle` |
| `sm-strong` | `--fds-shadow-sm-strong` |
| `md-subtle` | `--fds-shadow-md-subtle` |
| `md-strong` | `--fds-shadow-md-strong` |
| `lg-subtle` | `--fds-shadow-lg-subtle` |
| `lg-strong` | `--fds-shadow-lg-strong` |
**`resourceScheme`**
⚠️ Not available in V4, based on the theme
### Component token map
**`Button`**
| V3 | V4 |
| -------------------------------- | -------------------------------------- |
| `button-radius` | `--fds-button-radius` |
| `button-default-colour-bg` | `--fds-button-default-colour-bg` |
| `button-default-colour-bg-hover` | `--fds-button-default-colour-bg-hover` |
| `button-default-colour-text` | `--fds-button-default-colour-text` |
| `button-secondary-colour-border` | `--fds-button-secondary-colour-border` |
| `button-secondary-colour-text` | `--fds-button-secondary-colour-text` |
| `button-light-colour-text` | `--fds-button-light-colour-text` |
| `button-link-colour-text` | `--fds-button-link-colour-text` |
**`Animation`**
| V3 | V4 |
| ----------------------------- | --------------------------------------------- |
| `loading-dots-spinner-colour` | `--fds-animation-loading-dots-spinner-colour` |
**`Navbar`**
| V3 | V4 |
| ---------------------------------------- | ---------------------------------------------- |
| `navbar-full-height` | `--fds-navbar-full-height` |
| `navbar-full-logo-height` | `--fds-navbar-full-logo-height` |
| `navbar-compressed-height` | `--fds-navbar-compressed-height` |
| `navbar-compressed-logo-height` | `--fds-navbar-compressed-logo-height` |
| `navbar-mobile-height` | `--fds-navbar-mobile-height` |
| `navbar-mobile-logo-height` | `--fds-navbar-mobile-logo-height` |
| `navbar-colour-bg` | `--fds-navbar-colour-bg` |
| `navbar-colour-icon` | `--fds-navbar-colour-icon` |
| `navbar-link-colour-text` | `--fds-navbar-link-colour-text` |
| `navbar-link-colour-text-hover` | `--fds-navbar-link-colour-text-hover` |
| `navbar-link-colour-text-selected-hover` | `--fds-navbar-link-colour-text-selected-hover` |
**`Footer`**
| V3 | V4 |
| ------------------------------------------ | ------------------------------------------------ |
| `footer-colour-bg` | `--fds-footer-colour-bg` |
| `footer-colour-text` | `--fds-footer-colour-text` |
| `footer-link-colour-text` | `--fds-footer-link-colour-text` |
| `footer-link-colour-text-hover` | `--fds-footer-link-colour-text-hover` |
| `footer-disclaimer-link-colour-text` | `--fds-footer-disclaimer-link-colour-text` |
| `footer-disclaimer-link-colour-text-hover` | `--fds-footer-disclaimer-link-colour-text-hover` |
## Subpath imports
V4 restricts imports to the root `@lifesg/react-design-system` or pre-defined subpath entrypoints i.e. `@lifesg/react-design-system/`. Internal file paths that were previously accessible (e.g. deep imports into `@lifesg/react-design-system//*`) are no longer reachable.
If you were importing a type directly from an internal path, update it to use the nearest public entrypoint instead:
```diff
- import type { AlertProps } from "@lifesg/react-design-system/alert/types";
+ import type { AlertProps } from "@lifesg/react-design-system/alert";
```
If the type you need is not yet exported from any public entrypoint, raise a PR to expose it.
## V2 modules removed
Modules under the following paths have been removed:
- `@lifesg/react-design-system/v2_color`
- `@lifesg/react-design-system/v2_design-token`
- `@lifesg/react-design-system/v2_layout`
- `@lifesg/react-design-system/v2_media`
- `@lifesg/react-design-system/v2_spec`
- `@lifesg/react-design-system/v2_text-list`
- `@lifesg/react-design-system/v2_text`
- `@lifesg/react-design-system/v2_theme`
- `@lifesg/react-design-system/v2_transition`
## TypeScript theme declaration
If you have a `styled.d.ts` file to extend the TypeScript definitions for Styled Components, that can now be removed.
```
import "styled-components";
import { ThemeSpec } from "@lifesg/react-design-system/theme/types";
declare module "styled-components" {
export interface DefaultTheme extends ThemeSpec {
}
}
```
## Cascade layers
The normalisation stylesheet at `main.css` is now wrapped in `@layer main`.
Theme stylesheets and component styles are wrapped in `@layer fds`.
## CSS imports
Each component declares its own styles and imports them automatically. This means your project will need to support loading of CSS from within `node_module` dependencies.
This is typically supported out-of-the-box in modern frameworks such as NextJS 16. Otherwise, you would need to configure your bundler with a CSS loader.
## Border
`Border.Util` has been removed. For dashed border styling, use the `DashedBorder` utility component instead.
**V3 usage**
`Border.Util["dashed-default"]` could be applied directly in Styled Components declarations.
```tsx
import { Border } from "@lifesg/react-design-system/theme";
const ExampleComponent = styled.div`
${Border.Util["dashed-default"]({
radius: Radius["sm"],
thickness: Border["width-040"],
colour: Colour["border"],
})}
`;
```
**V4 usage**
A generic utility component is provided.
```tsx
import { DashedBorder } from "@lifesg/react-design-system/dashed-border";
{/* original children here */}
;
```
## BoxContainer
The default `32px` bottom margin on the parent container has been removed.
## Breakpoint
The viewport tokens are now CSS variables that include the length unit.
**V3 usage**
```css
.container {
width: calc(${Breakpoint["xl-max"]}px - 48px);
}
```
**V4 usage**
```css
.container {
width: calc(${Breakpoint["xl-max"]} - 48px);
}
```
## Button
1. `Button.Default`, `Button.Small` and `Button.Large` have been deprecated. Use `Button` with the `sizeType` prop instead.
```diff
- Click me
- Click me
- Click me
+
+
+
```
| Deprecated | `sizeType` value |
| ---------------- | ------------------- |
| `Button.Large` | `large` |
| `Button.Small` | `small` |
| `Button.Default` | `default` (or omit) |
2. The default 100% width on `<= sm` has been removed
## ButtonWithIcon
`ButtonWithIcon` has been merged into `Button`. Update the import and replace the component.
```diff
- import { ButtonWithIcon } from "@lifesg/react-design-system/button-with-icon";
+ import { Button } from "@lifesg/react-design-system/button";
- }>Label
+ }>Label
```
## Divider
The V2 `desktopCols`, `tabletCols` and `mobileCols` props have been removed. Switch to the V3 `xxsCols` - `xlCols`.
Refer to the [Layout section](https://github.com/LifeSG/react-design-system/wiki/Migrating-to-V3#layout) in the V3 migration docs for more information.
## DSThemeProvider
`DSThemeProvider` has been removed. Replace with the new `ThemeProvider`. See [ThemeProvider](#themeprovider).
## ErrorDisplay
`ErrorDisplay.ImagePathAttributes` has been renamed to `ErrorDisplayImagePathAttributes`.
```diff
- import { ErrorDisplay } from "@lifesg/react-design-system/error-display";
- type MyProps = ErrorDisplay.ImagePathAttributes;
+ import { ErrorDisplayImagePathAttributes } from "@lifesg/react-design-system/error-display";
+ type MyProps = ErrorDisplayImagePathAttributes;
```
## Form.CustomField
The `disabled` prop is unused and has been removed.
## Form.Label
The `disabled` prop is unused and has been removed.
## IconButton
`IconButton` has been merged into `Button`. Update the import and replace the component.
```diff
- import { IconButton } from "@lifesg/react-design-system/icon-button";
+ import { Button } from "@lifesg/react-design-system/button";
- } />
+ } />
```
## Markup
The underlying `baseTextSize` prop type has been moved from the internal `TypographySizeType` to `FontSize`.
```diff
- import type { TypographySizeType } from "@lifesg/react-design-system/theme/font/types";
+ import type { FontSize } from "@lifesg/react-design-system/theme";
```
The accepted values are unchanged:
```
"heading-xxl" | "heading-xl" | "heading-lg" | "heading-md" | "heading-sm" | "heading-xs"
| "body-baseline" | "body-md" | "body-sm" | "body-xs"
```
## Masonry
This deprecated module has been removed.
## Modal
The variables for controlling the position of the close button have been renamed.
| V3 | V4 |
| ---------------------------- | --------------------------------------------- |
| `--close-button-top-inset` | `--fds-internal-modal-closeButton-topInset` |
| `--close-button-right-inset` | `--fds-internal-modal-closeButton-rightInset` |
## Popover (deprecated)
The deprecated components have been replaced with the PopoverV2 implementation, which offers improved positioning and easier usage.
**V3 usage**
```tsx
import { withPopover, Popover } from "@lifesg/react-design-system/popover";
const Trigger = () =>
);
```
**Codemod**
A codemod is available to migrate to the new `Popover`.
```console
$ npx lifesg-react-design-system
Select codemods to run:
❯◉ migrate-popover
```
## PopoverV2
This version is now the default implementation and the import path has been updated:
```diff
- import { PopoverTrigger } from "@lifesg/react-design-system/popover-v2";
+ import { PopoverTrigger } from "@lifesg/react-design-system/popover";
```
The following interfaces have been renamed:
| Old | New |
| --------------------- | ------------------- |
| PopoverV2 | Popover |
| PopoverV2Props | PopoverProps |
| PopoverV2TriggerType | PopoverTriggerType |
| PopoverV2Position | PopoverPosition |
| PopoverV2TriggerProps | PopoverTriggerProps |
**Codemod**
A codemod is available to update the imports.
```console
$ npx lifesg-react-design-system
Select codemods to run:
❯◉ migrate-popover
```
## ProgressIndicator
The deprecated `fadeColor` and `fadePosition` props have been removed.
## TextList
The underlying `size` prop type has been moved from the internal `TypographySizeType` to `TextListSize`.
```diff
- import type { TypographySizeType } from "@lifesg/react-design-system/theme/font/types";
+ import type { TextListSize } from "@lifesg/react-design-system/text-list";
```
The accepted values are unchanged:
```
"heading-xxl" | "heading-xl" | "heading-lg" | "heading-md" | "heading-sm" | "heading-xs"
| "body-baseline" | "body-md" | "body-sm" | "body-xs"
```
## TimeSlotBar
Function interpolation support for the timeslot colour props (`backgroundColor`, `backgroundColor2`, `hoverBackgroundColor`, `hoverBackgroundColor2`) has been removed.
**V3 usage**
```tsx
theme.colourMode === "dark" ? "black" : "white",
// colour token - still works
backgroundColor: Colour["bg"],
// css value - still works
backgroundColor: "#E3F2FD",
},
},
]}
/>
```
**V4 usage**
```tsx
const { mode } = useTheme();
;
```
## TimeSlotBarWeek
Function interpolation support for the timeslot colour props has been removed. See [TimeSlot](#timeslotbar).
## TimeSlotWeekView
Function interpolation support for the timeslot colour props has been removed. See [TimeSlot](#timeslotbar).
## Timetable
Function interpolation support for the timeslot colour props (`backgroundColor`, `altBackgroundColor`, `hoverBackgroundColor`, `altHoverBackgroundColor`) has been removed.
**V3 usage**
```tsx
theme.colourMode === "dark" ? "black" : "white",
// colour token - still works
backgroundColor: Colour["bg"],
// css value - still works
backgroundColor: "#E3F2FD",
},
},
],
},
]}
/>
```
**V4 usage**
```tsx
const { mode } = useTheme();
;
```
## Tooltip
This deprecated module has been removed.
## useDSTheme
This has been replaced with the `useTheme` hook from `@lifesg/react-design-system/theme`. See [useTheme](#usetheme).
## useTheme
A hook is provided to access the new theme context.
**V3 usage**
```
import { ThemeSpec } from "@lifesg/react-design-system/theme";
import { useTheme } from "styled-components";
const Example = () => {
const theme: ThemeSpec = useTheme();
}
```
**V4 usage**
```
import { useTheme, ThemeContextValue } from "@lifesg/react-design-system/theme";
const Example = () => {
const theme: ThemeContextValue = useTheme();
}
```
| V3 value | V4 equivalent |
| -------------------- | ------------- |
| `resourceScheme` | `theme` |
| `colourMode` | `mode` |
| `colourScheme` | not available |
| `colourMode` | not available |
| `fontScheme` | not available |
| `motionScheme` | not available |
| `borderScheme` | not available |
| `spacingScheme` | not available |
| `radiusScheme` | not available |
| `shadowScheme` | not available |
| `breakpointScheme` | not available |
| `resourceScheme` | not available |
| `componentScheme` | not available |
| `componentOverrides` | not available |
| `overrides` | not available |
| `maxColumns` | not available |
# Troubleshooting
## MediaQuery does not work / is overriding my styles
If you are using Styled Components v5, upgrade to v6
## Specificity
Depending on your project setup, you may encounter specificity issues where styles from `main.css` are overriding component styles.
In your CSS entrypoint, include the following cascade layer declaration at the top. This line must be loaded **before** any CSS declarations:
```css
@layer main, fds;
```
## CSS styles are out of order
If you are using NextJS with Turbopack, this is a known issue. CSS resolution order differs between `dev` and `build`, which may result in broken styles on production.
Review the [recommendations](https://nextjs.org/docs/app/getting-started/css#recommendations) from Next.js for possible workarounds.
Alternatively, switch to [Webpack](https://nextjs.org/docs/app/api-reference/turbopack#using-webpack-instead) instead.
## Jest
### My unit test style assertions are failing!
Note that Jest does not handle CSS transforms by default. If you have jsdom assertions that depend on the styles to be loaded, this is an example of the changes required for the config:
```diff
{
moduleNameMapper: {
- "\\.(jpg|jpeg|png|gif|css)$": "identity-obj-proxy"
+ "\\.(jpg|jpeg|png|gif)$": "identity-obj-proxy"
},
+ transformIgnorePatterns: ["/node_modules/(?!@lifesg/react-design-system)"],
+ transform: {
+ "\\.[jt]sx?$": ["babel-jest", { excludeJestPreset: true }],
+ "^.+\\.css$": "jest-transform-css"
+ }
}
```
Also note that layers do not fully work in jsdom at the moment. As a workaround, you can patch the library for rudimentary support.
```diff
diff --git a/node_modules/jsdom/lib/jsdom/living/css/helpers/computed-style.js b/node_modules/jsdom/lib/jsdom/living/css/helpers/computed-style.js
index a8da57f..5698f96 100644
--- a/node_modules/jsdom/lib/jsdom/living/css/helpers/computed-style.js
+++ b/node_modules/jsdom/lib/jsdom/living/css/helpers/computed-style.js
@@ -4,6 +4,7 @@ const fs = require("node:fs");
const path = require("node:path");
const Specificity = require("@bramus/specificity").default;
const CSSImportRule = require("../../../../generated/idl/CSSImportRule.js");
+const CSSLayerBlockRule = require("../../../../generated/idl/CSSLayerBlockRule.js");
const CSSMediaRule = require("../../../../generated/idl/CSSMediaRule.js");
const CSSStyleProperties = require("../../../../generated/idl/CSSStyleProperties.js");
const CSSStyleRule = require("../../../../generated/idl/CSSStyleRule.js");
@@ -86,6 +87,10 @@ function handleSheet(sheetImpl, elementImpl, declaration, specificities) {
handleRule(innerRule, elementImpl, declaration, specificities);
}
}
+ } else if (CSSLayerBlockRule.isImpl(ruleImpl)) {
+ for (const innerRule of ruleImpl.cssRules._list) {
+ handleRule(innerRule, elementImpl, declaration, specificities);
+ }
} else if (CSSMediaRule.isImpl(ruleImpl)) {
if (evaluateMediaList(ruleImpl.media._list)) {
for (const innerRule of ruleImpl.cssRules._list) {
```
## I'm encountering a different problem!
Encountered other errors or problems? File a GitHub issue, or reach out to the maintainers in Slack.