Skip to content

Migrating to V4

Ruo Ling edited this page Jul 31, 2026 · 54 revisions

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

- @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):

import "@lifesg/react-design-system/theme/styles/lifesg.css";

Or in an existing global .css file:

@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:

- import { DSThemeProvider, LifeSGTheme } from "@lifesg/react-design-system/theme";
- import { ThemeProvider } from "styled-components";
+ import { ThemeProvider } from "@lifesg/react-design-system/theme";

- <ThemeProvider theme={LifeSGTheme}>{children}</ThemeProvider>
- <DSThemeProvider theme={LifeSGTheme}>{children}</DSThemeProvider>
+ <ThemeProvider theme="lifesg">{children}</ThemeProvider>

Important

If DSThemeProvider was not previously used, add mode="light" to disable the default dark mode in V4.

<ThemeProvider theme="lifesg" mode="light">
    {children}
</ThemeProvider>

5. Run codemod to migrate imports

$ npx lifesg-react-design-system
Select codemods to run:
❯◉ migrate-popover

6. Migrate design token usages

Refer to the Design tokens in styling and Design tokens at runtime sections.

Tip

If you encounter errors or issues, refer to the troubleshooting section

Summary of changes

General

Components

Changes

Design tokens in styling

V3 usage

In V3, design tokens were functions that accepted props. They could be used directly in Styled Components.

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):

    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:

    import { Spacing } from "@lifesg/react-design-system/theme";
    
    const ExampleComponent = styled.div`
        margin: calc(${Spacing["spacing-8"] * -1};
    `;
  2. In inline styles:

    <div style={{ color: Colour["text"] }} />
  3. In CSS modules:

    .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:

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 <ThirdPartyChart color={color} />;
};

For tokens that resolve to a numeric length unit (e.g. radius, spacing or breakpoints), use parsePxOrRemValue to get a number:

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 <ThirdPartyChart gap={gap} />;
};

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

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:

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:

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 <Container data-mobile-landscape={isMobileLandscape} />;
};

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

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:

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:

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:

import { ThemeProvider } from "@lifesg/react-design-system/theme";

<ThemeProvider theme="lifesg" mode="light">
    {children}
</ThemeProvider>;

ThemeProvider

The Styled Components ThemeProvider should be replaced with the new ThemeProvider from @lifesg/react-design-system/theme.

V3 usage

import { LifeSGTheme } from "@lifesg/react-design-system/theme";
import { ThemeProvider } from "styled-components";

<ThemeProvider theme={LifeSGTheme}>
    <Component />
</ThemeProvider>;

V4 usage

import { ThemeProvider } from "@lifesg/react-design-system/theme";

<ThemeProvider theme="lifesg">
    <Component />
</ThemeProvider>;
  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:

      import "@lifesg/react-design-system/theme/styles/lifesg.css";
    2. Or global 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

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 (
        <ThemeProvider theme={customTheme}>
            <Component />
        </ThemeProvider>
    );
};

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.

    [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.

    const App = ({ customTheme }) => {
        return (
            <ThemeProvider
                style={
                    {
                        "--fds-colour-bg-primary": customTheme.primaryColour,
                        "--fds-colour-text-inverse": "#ffffff",
                    } as React.CSSProperties
                }
            >
                <Component />
            </ThemeProvider>
        );
    };
  3. Option 3: useApplyStyle

    Use this if your project enforces a strict Content Security Policy that disallows inline styles.

    import { useRef } from "react";
    import { useApplyStyle } from "@lifesg/react-design-system/theme/utils";
    
    const App = ({ customTheme }) => {
        const themeRef = useRef<HTMLDivElement>(null);
    
        useApplyStyle(themeRef, {
            "--fds-colour-bg-primary": customTheme.primaryColour,
            "--fds-colour-text-inverse": "#ffffff",
        });
    
        return (
            <ThemeProvider ref={themeRef}>
                <Component />
            </ThemeProvider>
        );
    };

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/<component>. Internal file paths that were previously accessible (e.g. deep imports into @lifesg/react-design-system/<component>/*) are no longer reachable.

If you were importing a type directly from an internal path, update it to use the nearest public entrypoint instead:

- 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.

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.

import { DashedBorder } from "@lifesg/react-design-system/dashed-border";

<DashedBorder
    radius={Radius["sm"]}
    thickness={Border["width-040"]}
    colour={Colour["border"]}
>
    {/* original children here */}
</DashedBorder>;

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

.container {
  width: calc(${Breakpoint["xl-max"]}px - 48px);
}

V4 usage

.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.

    - <Button.Large>Click me</Button.Large>
    - <Button.Small>Click me</Button.Small>
    - <Button.Default>Click me</Button.Default>
    + <Button sizeType="large">Click me</Button>
    + <Button sizeType="small">Click me</Button>
    + <Button>Click me</Button>
    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.

- import { ButtonWithIcon } from "@lifesg/react-design-system/button-with-icon";
+ import { Button } from "@lifesg/react-design-system/button";

- <ButtonWithIcon icon={<Icon />}>Label</ButtonWithIcon>
+ <Button icon={<Icon />}>Label</Button>

Divider

The V2 desktopCols, tabletCols and mobileCols props have been removed. Switch to the V3 xxsCols - xlCols.

Refer to the Layout section in the V3 migration docs for more information.

DSThemeProvider

DSThemeProvider has been removed. Replace with the new ThemeProvider. See ThemeProvider.

ErrorDisplay

ErrorDisplay.ImagePathAttributes has been renamed to ErrorDisplayImagePathAttributes.

- 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.

- import { IconButton } from "@lifesg/react-design-system/icon-button";
+ import { Button } from "@lifesg/react-design-system/button";

- <IconButton><Icon /></IconButton>} />
+ <Button icon={<Icon />} />

Markup

The underlying baseTextSize prop type has been moved from the internal TypographySizeType to FontSize.

- 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

import { withPopover, Popover } from "@lifesg/react-design-system/popover";

const Trigger = () => <div>Trigger</div>;
const PopoverHOC = withPopover(Trigger, {
    content: "This is the popover content",
    trigger: "hover",
});

const ExampleComponent = () => (
    <div>
        <PopoverHOC />
    </div>
);

V4 usage

import { withPopover, Popover } from "@lifesg/react-design-system/popover";

const Trigger = () => <div>Trigger</div>;
const PopoverHOC = (
    <PopoverTrigger
        trigger="hover"
        popoverContent="This is the popover content"
    >
        <Trigger />
    </PopoverTrigger>
);

const ExampleComponent = () => (
    <div>
        <PopoverHOC />
    </div>
);

Codemod

A codemod is available to migrate to the new Popover.

$ 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:

- 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.

$ 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.

- 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

<TimeSlotBar
    slots={[
        {
            id: "1",
            startTime: "09:00",
            endTime: "09:30",
            styleAttributes: {
                // accessing the theme prop - no longer works
                backgroundColor: ({ theme }) =>
                    theme.colourMode === "dark" ? "black" : "white",
                // colour token - still works
                backgroundColor: Colour["bg"],
                // css value - still works
                backgroundColor: "#E3F2FD",
            },
        },
    ]}
/>

V4 usage

const { mode } = useTheme();

<TimeSlotBar
    slots={[
        {
            id: "1",
            startTime: "09:00",
            endTime: "09:30",
            styleAttributes: {
                backgroundColor: mode === "dark" ? "black" : "white",
                backgroundColor: Colour["bg"],
                backgroundColor: "#E3F2FD",
            },
        },
    ]}
/>;

TimeSlotBarWeek

Function interpolation support for the timeslot colour props has been removed. See TimeSlot.

TimeSlotWeekView

Function interpolation support for the timeslot colour props has been removed. See TimeSlot.

Timetable

Function interpolation support for the timeslot colour props (backgroundColor, altBackgroundColor, hoverBackgroundColor, altHoverBackgroundColor) has been removed.

V3 usage

<TimeTable
    rowData={[
        {
            id: "1",
            rowCells: [
                {
                    startTime: "08:00",
                    endTime: "09:30",
                    cellStyleAttributes: {
                        // accessing the theme prop - no longer works
                        backgroundColor: ({ theme }) =>
                            theme.colourMode === "dark" ? "black" : "white",
                        // colour token - still works
                        backgroundColor: Colour["bg"],
                        // css value - still works
                        backgroundColor: "#E3F2FD",
                    },
                },
            ],
        },
    ]}
/>

V4 usage

const { mode } = useTheme();

<TimeSlotBar
    slots={[
        {
            id: "1",
            startTime: "09:00",
            endTime: "09:30",
            styleAttributes: {
                backgroundColor: mode === "dark" ? "black" : "white",
                backgroundColor: Colour["bg"],
                backgroundColor: "#E3F2FD",
            },
        },
    ]}
/>;

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

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:

@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 from Next.js for possible workarounds.

Alternatively, switch to Webpack 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:

{
    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 --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.

Clone this wiki locally