-
Notifications
You must be signed in to change notification settings - Fork 22
Migrating to 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
If you are an AI coding agent, refer to the following instructions:
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
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-popover6. 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
General
- Design tokens in styling
- Design tokens at runtime
- Breakpoint tokens in media queries
- Breakpoint tokens at runtime
- Dark mode
- ThemeProvider
- ThemeSpec and customisation
- Subpath imports
- V2 modules removed
- TypeScript theme declaration
- Cascade layers
- CSS imports
Components
- Border
- BoxContainer
- Breakpoint
- Button
- ButtonWithIcon
- Divider
- DSThemeProvider
- ErrorDisplay
- Form.CustomField
- Form.Label
- IconButton
- Markup
- Masonry
- Modal
- Popover
- PopoverV2
- ProgressIndicator
- TextList
- TimeSlotBar
- TimeSlotBarWeek
- TimeSlotWeekView
- Timetable
- Tooltip
- useDSTheme
- useTheme
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:
-
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}; `;
-
In inline styles:
<div style={{ color: Colour["text"] }} />
-
In CSS modules:
.example { background: var(--fds-colour-bg-disabled); }
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} />;
};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 */
}
`;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
}
};
};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>;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>;-
The
themeprop can be mapped as follows:V3 ThemeSpecV4 themeLifeSGThemelifesgBookingSGThemebookingsgRBSThemerbsMyLegacyThememylegacyCCubeThemeccubeOneServiceThemeoneservicePAThemepaA11yPlaygroundThemea11y-playgroundSupportGoWhereThemesupportgowhereSGWDigitalLobbyThemesgw-digital-lobbyIMDAThemeimdaSPFThemespfSMGSTheme(not available yet) -
Make sure to import the corresponding theme stylesheet through one of these methods
-
In your Javascript entrypoint:
import "@lifesg/react-design-system/theme/styles/lifesg.css";
-
Or global CSS:
@import url("@lifesg/react-design-system/theme/styles/lifesg.css");
-
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:
-
Option 1: CSS stylesheet — override entire theme
Override the theme globally by targeting the
data-fds-themeattribute set byThemeProvider.[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; }
-
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> ); };
-
Option 3:
useApplyStyleUse 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:
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 |
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
fontSpec
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 |
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
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 |
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.
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
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 {
}
}
The normalisation stylesheet at main.css is now wrapped in @layer main.
Theme stylesheets and component styles are wrapped in @layer fds.
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.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>;The default 32px bottom margin on the parent container has been removed.
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.Default,Button.SmallandButton.Largehave been deprecated. UseButtonwith thesizeTypeprop 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 sizeTypevalueButton.LargelargeButton.SmallsmallButton.Defaultdefault(or omit) -
The default 100% width on
<= smhas been removed
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>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 has been removed. Replace with the new ThemeProvider. See ThemeProvider.
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;The disabled prop is unused and has been removed.
The disabled prop is unused and has been removed.
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 />} />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"
This deprecated module has been removed.
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 |
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-popoverThis 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-popoverThe deprecated fadeColor and fadePosition props have been removed.
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"
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",
},
},
]}
/>;Function interpolation support for the timeslot colour props has been removed. See TimeSlot.
Function interpolation support for the timeslot colour props has been removed. See TimeSlot.
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",
},
},
]}
/>;This deprecated module has been removed.
This has been replaced with the useTheme hook from @lifesg/react-design-system/theme. See 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 |
If you are using Styled Components v5, upgrade to v6
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;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.
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) {Encountered other errors or problems? File a GitHub issue, or reach out to the maintainers in Slack.