Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# NHS.UK React components

## 6.0.0-beta.3 - 27 October 2025

This version provides support for NHS.UK frontend v10.1 and includes:

- [Smaller radios](https://service-manual.nhs.uk/design-system/components/radios#smaller-radios) and [smaller checkboxes](https://service-manual.nhs.uk/design-system/components/checkboxes#smaller-checkboxes)
- [Numbered pagination](https://service-manual.nhs.uk/design-system/components/pagination#for-navigating-between-pages-of-items)
- React strict mode support

For a full list of changes in this release please refer to the [migration doc](https://github.com/NHSDigital/nhsuk-react-components/blob/main/docs/upgrade-to-6.0.md).

## 6.0.0-beta.2 - 13 October 2025

This version provides support for NHS.UK frontend v10.x, React Server Components (RSC) and fixes a Rollup `'use client'` directive issue.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nhsuk-react-components",
"version": "6.0.0-beta.2",
"version": "6.0.0-beta.3",
"license": "MIT",
"author": {
"name": "NHS England"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import {
Children,
createRef,
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
type ComponentPropsWithoutRef,
} from 'react';
Expand All @@ -27,19 +28,22 @@ const NotificationBannerComponent = forwardRef<HTMLDivElement, NotificationBanne
(props, forwardedRef) => {
const { children, className, title, titleId, success, role, disableAutoFocus, ...rest } = props;

const [moduleRef] = useState(() => forwardedRef || createRef<HTMLDivElement>());
const moduleRef = useRef<HTMLDivElement>(null);
const importRef = useRef<Promise<NotificationBannerModule | void>>(null);
const [instanceError, setInstanceError] = useState<Error>();
const [instance, setInstance] = useState<NotificationBannerModule>();

useImperativeHandle(forwardedRef, () => moduleRef.current!, [moduleRef]);

useEffect(() => {
if (!('current' in moduleRef) || !moduleRef.current || instance) {
if (!moduleRef.current || importRef.current || instance) {
return;
}

import('nhsuk-frontend')
importRef.current = import('nhsuk-frontend')
.then(({ NotificationBanner }) => setInstance(new NotificationBanner(moduleRef.current)))
.catch(setInstanceError);
}, [moduleRef, instance]);
}, [moduleRef, importRef, instance]);

const items = Children.toArray(children);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('Table.Cell', () => {
</table>,
);

expect(console.warn).toHaveBeenCalledTimes(1);
expect(console.warn).toHaveBeenCalled();
expect(console.warn).toHaveBeenLastCalledWith(
'Table.Cell used outside of a Table.Head or Table.Body component. Unable to determine section type from context.',
);
Expand Down
14 changes: 9 additions & 5 deletions src/components/content-presentation/tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import classNames from 'classnames';
import { type Tabs as TabsModule } from 'nhsuk-frontend';
import {
createRef,
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
type ComponentPropsWithoutRef,
type FC,
Expand Down Expand Up @@ -55,19 +56,22 @@ export const TabsContents: FC<TabsContentsProps> = ({ children, id, ...rest }) =
const TabsComponent = forwardRef<HTMLDivElement, TabsProps>((props, forwardedRef) => {
const { children, className, ...rest } = props;

const [moduleRef] = useState(() => forwardedRef || createRef<HTMLDivElement>());
const moduleRef = useRef<HTMLDivElement>(null);
const importRef = useRef<Promise<TabsModule | void>>(null);
const [instanceError, setInstanceError] = useState<Error>();
const [instance, setInstance] = useState<TabsModule>();

useImperativeHandle(forwardedRef, () => moduleRef.current!, [moduleRef]);

useEffect(() => {
if (!('current' in moduleRef) || !moduleRef.current || instance) {
if (!moduleRef.current || importRef.current || instance) {
return;
}

import('nhsuk-frontend')
importRef.current = import('nhsuk-frontend')
.then(({ Tabs }) => setInstance(new Tabs(moduleRef.current)))
.catch(setInstanceError);
}, [moduleRef, instance]);
}, [moduleRef, importRef, instance]);

if (instanceError) {
throw instanceError;
Expand Down
25 changes: 16 additions & 9 deletions src/components/form-elements/button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import classNames from 'classnames';
import { type Button as ButtonModule } from 'nhsuk-frontend';
import {
createRef,
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
type ForwardedRef,
type MouseEvent,
Expand Down Expand Up @@ -45,19 +46,22 @@ const ButtonComponent = forwardRef<HTMLButtonElement, ButtonProps>((props, forwa
...rest
} = props;

const [moduleRef] = useState(() => forwardedRef || createRef<HTMLButtonElement>());
const moduleRef = useRef<HTMLButtonElement>(null);
const importRef = useRef<Promise<ButtonModule | void>>(null);
const [instanceError, setInstanceError] = useState<Error>();
const [instance, setInstance] = useState<ButtonModule>();

useImperativeHandle(forwardedRef, () => moduleRef.current!, [moduleRef]);

useEffect(() => {
if (!('current' in moduleRef) || !moduleRef.current || instance) {
if (!moduleRef.current || importRef.current || instance) {
return;
}

import('nhsuk-frontend')
importRef.current = import('nhsuk-frontend')
.then(({ Button }) => setInstance(new Button(moduleRef.current)))
.catch(setInstanceError);
}, [moduleRef, instance]);
}, [moduleRef, importRef, instance]);

if (instanceError) {
throw instanceError;
Expand Down Expand Up @@ -104,19 +108,22 @@ const ButtonLinkComponent = forwardRef<HTMLAnchorElement, ButtonLinkProps>(
...rest
} = props;

const [moduleRef] = useState(() => forwardedRef || createRef<HTMLAnchorElement>());
const moduleRef = useRef<HTMLAnchorElement>(null);
const importRef = useRef<Promise<ButtonModule | void>>(null);
const [instanceError, setInstanceError] = useState<Error>();
const [instance, setInstance] = useState<ButtonModule>();

useImperativeHandle(forwardedRef, () => moduleRef.current!, [moduleRef]);

useEffect(() => {
if (!('current' in moduleRef) || !moduleRef.current || instance) {
if (!moduleRef.current || importRef.current || instance) {
return;
}

import('nhsuk-frontend')
importRef.current = import('nhsuk-frontend')
.then(({ Button }) => setInstance(new Button(moduleRef.current)))
.catch(setInstanceError);
}, [moduleRef, instance]);
}, [moduleRef, importRef, instance]);

if (instanceError) {
throw instanceError;
Expand Down
20 changes: 15 additions & 5 deletions src/components/form-elements/character-count/CharacterCount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

import classNames from 'classnames';
import { type CharacterCount as CharacterCountModule } from 'nhsuk-frontend';
import { createRef, forwardRef, useEffect, useState, type ComponentPropsWithoutRef } from 'react';
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
type ComponentPropsWithoutRef,
} from 'react';
import { FormGroup } from '#components/utils/index.js';
import { type FormElementProps } from '#util/types/FormTypes.js';

Expand All @@ -16,19 +23,22 @@ export interface CharacterCountProps

export const CharacterCount = forwardRef<HTMLTextAreaElement, CharacterCountProps>(
({ maxLength, maxWords, threshold, formGroupProps, ...rest }, forwardedRef) => {
const [moduleRef] = useState(() => formGroupProps?.ref || createRef<HTMLDivElement>());
const moduleRef = useRef<HTMLDivElement>(null);
const importRef = useRef<Promise<CharacterCountModule | void>>(null);
const [instanceError, setInstanceError] = useState<Error>();
const [instance, setInstance] = useState<CharacterCountModule>();

useImperativeHandle(formGroupProps?.ref, () => moduleRef.current!, [moduleRef]);

useEffect(() => {
if (!('current' in moduleRef) || !moduleRef.current || instance) {
if (!moduleRef.current || importRef.current || instance) {
return;
}

import('nhsuk-frontend')
importRef.current = import('nhsuk-frontend')
.then(({ CharacterCount }) => setInstance(new CharacterCount(moduleRef.current)))
.catch(setInstanceError);
}, [moduleRef, instance]);
}, [moduleRef, importRef, instance]);

if (instanceError) {
throw instanceError;
Expand Down
20 changes: 15 additions & 5 deletions src/components/form-elements/checkboxes/Checkboxes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

import classNames from 'classnames';
import { type Checkboxes as CheckboxesModule } from 'nhsuk-frontend';
import { createRef, forwardRef, useEffect, useState, type ComponentPropsWithoutRef } from 'react';
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
type ComponentPropsWithoutRef,
} from 'react';
import { CheckboxesDivider, CheckboxesItem } from './components/index.js';
import { CheckboxesContext, type ICheckboxesContext } from './CheckboxesContext.js';
import { FormGroup } from '#components/utils/index.js';
Expand All @@ -19,23 +26,26 @@ export interface CheckboxesProps
const CheckboxesComponent = forwardRef<HTMLDivElement, CheckboxesProps>((props, forwardedRef) => {
const { children, idPrefix, ...rest } = props;

const [moduleRef] = useState(() => forwardedRef || createRef<HTMLDivElement>());
const moduleRef = useRef<HTMLDivElement>(null);
const importRef = useRef<Promise<CheckboxesModule | void>>(null);
const [instanceError, setInstanceError] = useState<Error>();
const [instance, setInstance] = useState<CheckboxesModule>();

const _boxReferences: string[] = [];
let _boxCount: number = 0;
let _boxIds: Record<string, string> = {};

useImperativeHandle(forwardedRef, () => moduleRef.current!, [moduleRef]);

useEffect(() => {
if (!('current' in moduleRef) || !moduleRef.current || instance) {
if (!moduleRef.current || importRef.current || instance) {
return;
}

import('nhsuk-frontend')
importRef.current = import('nhsuk-frontend')
.then(({ Checkboxes }) => setInstance(new Checkboxes(moduleRef.current)))
.catch(setInstanceError);
}, [moduleRef, instance]);
}, [moduleRef, importRef, instance]);

const getBoxId = (id: string, reference: string): string => {
if (reference in _boxIds) {
Expand Down
7 changes: 5 additions & 2 deletions src/components/form-elements/date-input/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import classNames from 'classnames';
import {
createRef,
forwardRef,
useImperativeHandle,
useRef,
useState,
type ChangeEvent,
type ComponentPropsWithoutRef,
Expand Down Expand Up @@ -43,7 +44,9 @@ export type DateInputType = 'day' | 'month' | 'year';

const DateInputComponent = forwardRef<HTMLDivElement, DateInputProps>(
({ children, onChange, value, defaultValue, formGroupProps, ...rest }, forwardedRef) => {
const [moduleRef] = useState(() => formGroupProps?.ref || createRef<HTMLDivElement>());
const moduleRef = useRef<HTMLDivElement>(null);

useImperativeHandle(formGroupProps?.ref, () => moduleRef.current!, [moduleRef]);

const [internalDate, setInternalDate] = useState<DateInputValue>({
day: value?.day ?? '',
Expand Down
14 changes: 9 additions & 5 deletions src/components/form-elements/error-summary/ErrorSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import classNames from 'classnames';
import { type ErrorSummary as ErrorSummaryModule } from 'nhsuk-frontend';
import {
Children,
createRef,
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
type ComponentPropsWithoutRef,
} from 'react';
Expand All @@ -19,19 +20,22 @@ export interface ErrorSummaryProps extends ComponentPropsWithoutRef<'div'> {

const ErrorSummaryComponent = forwardRef<HTMLDivElement, ErrorSummaryProps>(
({ children, className, disableAutoFocus, ...rest }, forwardedRef) => {
const [moduleRef] = useState(() => forwardedRef || createRef<HTMLDivElement>());
const moduleRef = useRef<HTMLDivElement>(null);
const importRef = useRef<Promise<ErrorSummaryModule | void>>(null);
const [instanceError, setInstanceError] = useState<Error>();
const [instance, setInstance] = useState<ErrorSummaryModule>();

useImperativeHandle(forwardedRef, () => moduleRef.current!, [moduleRef]);

useEffect(() => {
if (!('current' in moduleRef) || !moduleRef.current || instance) {
if (!moduleRef.current || importRef.current || instance) {
return;
}

import('nhsuk-frontend')
importRef.current = import('nhsuk-frontend')
.then(({ ErrorSummary }) => setInstance(new ErrorSummary(moduleRef.current)))
.catch(setInstanceError);
}, [moduleRef, instance]);
}, [moduleRef, importRef, instance]);

const items = Children.toArray(children);

Expand Down
20 changes: 15 additions & 5 deletions src/components/form-elements/radios/Radios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

import classNames from 'classnames';
import { type Radios as RadiosModule } from 'nhsuk-frontend';
import { createRef, forwardRef, useEffect, useState, type ComponentPropsWithoutRef } from 'react';
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
type ComponentPropsWithoutRef,
} from 'react';
import { RadiosDivider, RadiosItem } from './components/index.js';
import { RadiosContext, type IRadiosContext } from './RadiosContext.js';
import { FormGroup } from '#components/utils/index.js';
Expand All @@ -20,7 +27,8 @@ export interface RadiosProps
const RadiosComponent = forwardRef<HTMLDivElement, RadiosProps>((props, forwardedRef) => {
const { children, idPrefix, ...rest } = props;

const [moduleRef] = useState(() => forwardedRef || createRef<HTMLDivElement>());
const moduleRef = useRef<HTMLDivElement>(null);
const importRef = useRef<Promise<RadiosModule | void>>(null);
const [instanceError, setInstanceError] = useState<Error>();
const [instance, setInstance] = useState<RadiosModule>();
const [selectedRadio, setSelectedRadio] = useState<string>();
Expand All @@ -29,15 +37,17 @@ const RadiosComponent = forwardRef<HTMLDivElement, RadiosProps>((props, forwarde
let _radioCount = 0;
let _radioIds: Record<string, string> = {};

useImperativeHandle(forwardedRef, () => moduleRef.current!, [moduleRef]);

useEffect(() => {
if (!('current' in moduleRef) || !moduleRef.current || instance) {
if (!moduleRef.current || importRef.current || instance) {
return;
}

import('nhsuk-frontend')
importRef.current = import('nhsuk-frontend')
.then(({ Radios }) => setInstance(new Radios(moduleRef.current)))
.catch(setInstanceError);
}, [moduleRef, instance]);
}, [moduleRef, importRef, instance]);

const getRadioId = (id: string, reference: string): string => {
if (reference in _radioIds) {
Expand Down
Loading