Skip to content

Migrating to V4

Ruo Ling edited this page Apr 21, 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

Migrating from V3

If your project uses V3, follow these steps to install V4.

Recommended step:

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.

npm i @lifesg/react-design-system@3

1. Upgrade to the V4 Design System

npm i @lifesg/react-design-system@4

2. 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 .css file:

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

3. 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 avoid issues with the default dark mode in V4.

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

4. Migrate design token usages

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

Summary of changes

General

Components

Changes

Design tokens in styling

In V3, design tokens could be called as functions that accepted props.

V3 usage

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

const ExampleComponent = styled.div`
    background: ${(props) => Colour["bg"](props)};
`;

V4 usage

In V4, design tokens are CSS variable strings. Update usages depending on the context:

  1. In Styled Components:

    import { Colour } from "@lifesg/react-design-system/theme";
    
    const ExampleComponent = styled.div`
        background: ${Colour["bg"]};
    `;
  2. In inline styles:

    <div style={{ color: Colour["text"] }} />

Design token 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 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";

const ExampleComponent = () => {
    const theme = useTheme();
    const tablet = Breakpoint["lg-max"]({ theme });
    const isTablet = useMediaQuery({ maxWidth: tablet });
};

V4 usage

In V4, media query hooks are provided out of the box:

import {
    Breakpoint,
    useSafeMaxWidthMediaQuery,
} from "@lifesg/react-design-system/theme";

const ExampleComponent = () => {
    const isTablet = useSafeMaxWidthMediaQuery(Breakpoint["lg-max"]);
};

If the actual pixel value is required at runtime:

import { Breakpoint, useDesignToken } from "@lifesg/react-design-system/theme";
import { parsePxOrRemValue } from "@lifesg/react-design-system/theme";

const ExampleComponent = () => {
    const tablet = parsePxOrRemValue(useDesignToken(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>;

Import the corresponding theme stylesheet in your entrypoint:

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

ThemeSpec and customisation

The ThemeSpec object and the overrides API have been removed in V4. There is no longer mixing and matching of colour schemes via props.

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-theme attribute set by ThemeProvider.

    [data-theme="lifesg"] {
        --fds-colour-bg-primary: #0043ce;
        --fds-colour-text-inverse: #ffffff;
    }
  2. Option 2: CSS stylesheet — override for a specific context

    Suitable for static overrides scoped to a class or selector.

    .custom-theme {
        --fds-colour-bg-primary: #0043ce;
        --fds-colour-text-inverse: #ffffff;
    }
    <div className="custom-theme">
        <Component />
    </div>
  3. Option 3: Inline style

    Suitable for dynamic overrides driven by runtime values.

    const App = ({ customTheme }) => {
        return (
            <div
                style={
                    {
                        "--fds-colour-bg-primary": customTheme.primaryColour,
                        "--fds-colour-text-inverse": "#ffffff",
                    } as React.CSSProperties
                }
            >
                <Component />
            </div>
        );
    };
  4. Option 4: 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 rootRef = useRef<HTMLDivElement>(null);
    
        useApplyStyle(rootRef, {
            "--fds-colour-bg-primary": customTheme.primaryColour,
            "--fds-colour-text-inverse": "#ffffff",
        });
    
        return (
            <div ref={rootRef}>
                <Component />
            </div>
        );
    };

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

Button

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)

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>

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;

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={<Icon />} />
+ <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"

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"

Clone this wiki locally