diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status.mdx
new file mode 100644
index 00000000000..99fbf99aed2
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status.mdx
@@ -0,0 +1,25 @@
+---
+title: 'Status'
+description: 'The `Form.Status` can be used to show success or error messages.'
+showTabs: true
+tabs:
+ - title: Info
+ key: '/info'
+ - title: Demos
+ key: '/demos'
+ - title: Properties
+ key: '/properties'
+breadcrumb:
+ - text: Forms
+ href: /uilib/extensions/forms/
+ - text: Form
+ href: /uilib/extensions/forms/Form/
+ - text: Status
+ href: /uilib/extensions/forms/Form/Status
+---
+
+import Info from 'Docs/uilib/extensions/forms/Form/Status/info'
+import Demos from 'Docs/uilib/extensions/forms/Form/Status/demos'
+
+
+
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/Examples.tsx
new file mode 100644
index 00000000000..82a58740665
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/Examples.tsx
@@ -0,0 +1,74 @@
+import ComponentBox from '../../../../../../shared/tags/ComponentBox'
+import { Field, Form } from '@dnb/eufemia/src/extensions/forms'
+import { createRequest } from '../SubmitIndicator/Examples'
+import { Button } from '@dnb/eufemia/src'
+
+const request = createRequest()
+
+export const ErrorMessage = () => {
+ return (
+
+ {() => {
+ // myFormId can be anything, as long as it's a unique instance
+ const myFormId = () => null
+
+ return (
+ {
+ await request(1000) // Simulate a request
+
+ Form.Status.setStatus(myFormId, 'error')
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ )
+ }}
+
+ )
+}
+
+export const SuccessMessage = () => {
+ return (
+
+ {() => {
+ // myFormId can be anything, as long as it's a unique instance
+ const myFormId = () => null
+
+ return (
+ {
+ await request(1000) // Simulate a request
+
+ Form.Status.setStatus(myFormId, 'success')
+ }}
+ >
+
+
+
+
+
+
+
+ )
+ }}
+
+ )
+}
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/demos.mdx
new file mode 100644
index 00000000000..ef05da36b48
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/demos.mdx
@@ -0,0 +1,16 @@
+---
+showTabs: true
+hideInMenu: true
+---
+
+import * as Examples from './Examples'
+
+## Demos
+
+### Error message
+
+
+
+### Success message
+
+
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/info.mdx
new file mode 100644
index 00000000000..eceaadc9dde
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/info.mdx
@@ -0,0 +1,56 @@
+---
+showTabs: true
+hideInMenu: true
+---
+
+## Description
+
+`Form.Status` displays a status message that is fully covering the available space and can be used to show success as a receipt or error messages.
+
+## Usage
+
+By default the given children will be shown.
+
+```tsx
+import { Form } from '@dnb/eufemia/extensions/forms'
+
+render(
+
+ visible content
+ ,
+)
+```
+
+## Showing the success or error message
+
+You can show the success or error message by using the `Form.Status.setStatus` method:
+
+```tsx
+Form.Status.setStatus(id, 'success')
+// or
+Form.Status.setStatus(id, 'error')
+```
+
+You can call it whenever you need to show the success message. Here is an example of how to use it:
+
+```tsx
+import { Form } from '@dnb/eufemia/extensions/forms'
+
+// myFormId can be anything, as long as it's a unique instance
+const myFormId = () => null
+
+render(
+ {
+ Form.Status.setStatus(myFormId, 'success')
+ }}
+ >
+ content
+ ,
+)
+```
+
+## Accessibility
+
+The component will manage focus handling, which is important for screen readers and users using keyboard navigation.
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/properties.mdx
new file mode 100644
index 00000000000..13143f60a98
--- /dev/null
+++ b/packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Form/Status/properties.mdx
@@ -0,0 +1,25 @@
+---
+showTabs: true
+hideInMenu: true
+---
+
+import TranslationsTable from 'dnb-design-system-portal/src/shared/parts/TranslationsTable'
+import PropertiesTable from 'dnb-design-system-portal/src/shared/parts/PropertiesTable'
+import {
+ StatusSuccessProperties,
+ StatusErrorProperties,
+} from '@dnb/eufemia/src/extensions/forms/Form/Status/StatusDocs'
+
+## Properties
+
+### Error
+
+
+
+### Success
+
+
+
+## Translations
+
+
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Status/Status.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Status/Status.tsx
new file mode 100644
index 00000000000..25a94c6acc0
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/Status/Status.tsx
@@ -0,0 +1,167 @@
+import React, { useCallback, useContext, useEffect, useRef } from 'react'
+import classnames from 'classnames'
+import Visibility from '../Visibility'
+import DataContext from '../../DataContext/Context'
+import { useSharedState } from '../../../../shared/helpers/useSharedState'
+import setStatus, { Status } from './setStatus'
+import { Button, Flex, Section } from '../../../../components'
+import { P } from '../../../../elements'
+import { useTranslation } from '../../hooks'
+import MainHeading from '../MainHeading'
+import SubmitButton from '../SubmitButton'
+
+export type Props = {
+ success?: {
+ title?: React.ReactNode
+ description?: React.ReactNode
+ buttonText?: React.ReactNode
+ buttonHref?: string
+ }
+ error?: {
+ title?: React.ReactNode
+ description?: React.ReactNode
+ retryButton?: React.ReactNode
+ cancelButton?: React.ReactNode
+ }
+ onCancel?: () => void
+ children: React.ReactNode
+ className?: string
+}
+
+function StatusContainer(props: Props) {
+ const { success, error, onCancel, className, children, ...restProps } =
+ props
+
+ const translations = useTranslation()
+
+ const { id } = useContext(DataContext) || {}
+
+ const { data } = useSharedState<{
+ activeStatus?: Status
+ }>(id)
+ const { activeStatus } = data || {}
+
+ // To ensure we not animate on first render.
+ // When there are several Examples rendered at the same time,
+ // the first one will animate on the first render.
+ const animateRef = useRef(undefined)
+ useEffect(() => {
+ animateRef.current = true
+ }, [])
+
+ const innerRef = useRef(null)
+ const onVisible = useCallback(() => {
+ if (animateRef.current) {
+ innerRef.current.focus?.()
+ }
+ }, [])
+
+ // To keep the content visible while hiding it with the HightAnimation
+ const currentStatusRef = useRef()
+ if (activeStatus) {
+ currentStatusRef.current = activeStatus
+ }
+
+ const onCancelHandler = useCallback(() => {
+ if (id) {
+ setStatus(id, undefined)
+ }
+ onCancel?.()
+ }, [id, onCancel])
+
+ const childrenAreVisible =
+ typeof activeStatus !== 'undefined'
+ ? !(activeStatus === activeStatus)
+ : undefined
+ const statusContentIsVisible =
+ typeof activeStatus !== 'undefined'
+ ? activeStatus === activeStatus
+ : false
+
+ let statusContent = null
+
+ if (currentStatusRef.current === 'success') {
+ const tr = translations.StatusSuccess
+ const {
+ title,
+ description,
+ buttonText,
+ buttonHref = '/',
+ } = success || {}
+
+ statusContent = (
+
+
+ {title ?? tr.title}
+ {description ?? tr.description}
+ {buttonHref && (
+
+ )}
+
+
+ )
+ } else if (currentStatusRef.current === 'error') {
+ const tr = translations.StatusError
+ const { title, description, cancelButton, retryButton } = error || {}
+
+ statusContent = (
+
+
+ {title ?? tr.title}
+ {description ?? tr.description}
+
+
+ {retryButton ?? tr.retryButton}
+
+
+
+ )
+ }
+
+ return (
+
+
+ {statusContent}
+
+
+
+ {children}
+
+
+ )
+}
+
+StatusContainer.setStatus = setStatus
+StatusContainer._supportsSpacingProps = true
+
+export default StatusContainer
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Status/StatusDocs.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Status/StatusDocs.tsx
new file mode 100644
index 00000000000..b801dc6c449
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/Status/StatusDocs.tsx
@@ -0,0 +1,52 @@
+import { PropertiesTableProps } from '../../../../shared/types'
+
+export const StatusSuccessProperties: PropertiesTableProps = {
+ title: {
+ doc: 'The title of the component.',
+ type: 'React.Node',
+ status: 'optional',
+ },
+ description: {
+ doc: 'The description of the component.',
+ type: 'React.Node',
+ status: 'optional',
+ },
+ buttonText: {
+ doc: 'The text of the button.',
+ type: 'React.Node',
+ status: 'optional',
+ },
+ '[Section](/uilib/components/section/properties)': {
+ doc: 'All Section properties.',
+ type: 'various',
+ status: 'optional',
+ },
+}
+
+export const StatusErrorProperties: PropertiesTableProps = {
+ title: {
+ doc: 'The title of the component.',
+ type: 'React.Node',
+ status: 'optional',
+ },
+ description: {
+ doc: 'The description of the component.',
+ type: 'React.Node',
+ status: 'optional',
+ },
+ cancelButton: {
+ doc: 'The text of the cancel button.',
+ type: 'React.Node',
+ status: 'optional',
+ },
+ retryButton: {
+ doc: 'The text of the retry button.',
+ type: 'React.Node',
+ status: 'optional',
+ },
+ '[Section](/uilib/components/section/properties)': {
+ doc: 'All Section properties.',
+ type: 'various',
+ status: 'optional',
+ },
+}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Status/__tests__/Status.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Status/__tests__/Status.test.tsx
new file mode 100644
index 00000000000..b087c1c5258
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/Status/__tests__/Status.test.tsx
@@ -0,0 +1,268 @@
+import React from 'react'
+import { act, fireEvent, render } from '@testing-library/react'
+import nbNO from '../../../constants/locales/nb-NO'
+import { Form } from '../../..'
+
+describe('Form.Status', () => {
+ it('should render success with correct text', () => {
+ const nb = nbNO['nb-NO'].StatusSuccess
+ const formId = {}
+
+ render(
+
+ content
+
+ )
+
+ expect(document.querySelector('h2')).toBeNull()
+ expect(document.querySelector('p')).toBeNull()
+
+ act(() => {
+ Form.Status.setStatus(formId, 'success')
+ })
+
+ expect(document.querySelector('h2')).toHaveTextContent(nb.title)
+ expect(document.querySelector('p')).toHaveTextContent(nb.description)
+
+ const anchors = document.querySelectorAll('a')
+ expect(anchors).toHaveLength(1)
+
+ const [anchor] = Array.from(anchors)
+ expect(anchor).toHaveTextContent(nb.buttonText)
+ expect(anchor).toHaveAttribute('href', '/')
+ })
+
+ it('should render error with correct text', async () => {
+ const nb = nbNO['nb-NO'].StatusError
+ const formId = {}
+
+ render(
+
+ content
+
+ )
+
+ expect(document.querySelector('h2')).toBeNull()
+ expect(document.querySelector('p')).toBeNull()
+
+ act(() => {
+ Form.Status.setStatus(formId, 'error')
+ })
+
+ expect(document.querySelector('h2')).toHaveTextContent(nb.title)
+ expect(document.querySelector('p')).toHaveTextContent(nb.description)
+
+ const buttons = document.querySelectorAll('button')
+ expect(buttons).toHaveLength(2)
+
+ const [backButton, retryButton] = Array.from(buttons)
+ expect(backButton).toHaveTextContent(nb.cancelButton)
+ expect(retryButton).toHaveTextContent(nb.retryButton)
+ })
+
+ it('should render success with custom text', () => {
+ const formId = {}
+
+ render(
+
+
+ content
+
+
+ )
+
+ expect(document.querySelector('h2')).toBeNull()
+ expect(document.querySelector('p')).toBeNull()
+
+ act(() => {
+ Form.Status.setStatus(formId, 'success')
+ })
+
+ expect(document.querySelector('h2')).toHaveTextContent('Custom title')
+ expect(document.querySelector('p')).toHaveTextContent(
+ 'Custom description'
+ )
+
+ const anchors = document.querySelectorAll('a')
+ expect(anchors).toHaveLength(1)
+
+ const [anchor] = Array.from(anchors)
+ expect(anchor).toHaveTextContent('Custom button text')
+ expect(anchor).toHaveAttribute('href', '/')
+ })
+
+ it('should render error with custom text', async () => {
+ const formId = {}
+
+ render(
+
+
+ content
+
+
+ )
+
+ expect(document.querySelector('h2')).toBeNull()
+ expect(document.querySelector('p')).toBeNull()
+
+ act(() => {
+ Form.Status.setStatus(formId, 'error')
+ })
+
+ expect(document.querySelector('h2')).toHaveTextContent('Custom title')
+ expect(document.querySelector('p')).toHaveTextContent(
+ 'Custom description'
+ )
+
+ const buttons = document.querySelectorAll('button')
+ expect(buttons).toHaveLength(2)
+
+ const [backButton, retryButton] = Array.from(buttons)
+ expect(backButton).toHaveTextContent('Custom cancel')
+ expect(retryButton).toHaveTextContent('Custom retry')
+ })
+
+ it('should keep children in the DOM', () => {
+ const formId = {}
+
+ render(
+
+
+
+
+
+ )
+
+ expect(document.querySelector('h2')).toBeNull()
+ expect(document.querySelector('output')).toHaveTextContent('content')
+
+ act(() => {
+ Form.Status.setStatus(formId, 'success')
+ })
+
+ expect(document.querySelector('h2')).toBeInTheDocument()
+ expect(document.querySelector('output')).toHaveTextContent('content')
+ })
+
+ it('should handle focus', () => {
+ const formId = {}
+
+ render(
+
+
+
+
+
+ )
+
+ expect(document.querySelector('body')).toHaveFocus()
+
+ act(() => {
+ Form.Status.setStatus(formId, 'success')
+ })
+
+ expect(
+ document.querySelector('.dnb-forms-status--success')
+ ).toHaveFocus()
+
+ act(() => {
+ document.querySelector('body').focus()
+ Form.Status.setStatus(formId, undefined)
+ })
+
+ expect(document.querySelector('.dnb-forms-status')).toHaveFocus()
+ expect(document.querySelector('.dnb-forms-status')).not.toHaveClass(
+ 'dnb-forms-status--success'
+ )
+
+ act(() => {
+ document.querySelector('body').focus()
+ Form.Status.setStatus(formId, 'success')
+ })
+
+ expect(
+ document.querySelector('.dnb-forms-status--success')
+ ).toHaveFocus()
+ expect(document.querySelector('.dnb-forms-status')).toHaveClass(
+ 'dnb-no-focus'
+ )
+ expect(document.querySelector('.dnb-forms-status')).toHaveAttribute(
+ 'tabindex',
+ '-1'
+ )
+ })
+
+ it('should show content when cancel button is clicked', () => {
+ const formId = {}
+
+ render(
+
+
+
+
+
+ )
+
+ act(() => {
+ Form.Status.setStatus(formId, 'error')
+ })
+
+ expect(document.querySelector('.dnb-forms-status')).toHaveClass(
+ 'dnb-forms-status--error'
+ )
+
+ const buttons = document.querySelectorAll('button')
+ expect(buttons).toHaveLength(2)
+
+ const [backButton] = Array.from(buttons)
+
+ fireEvent.click(backButton)
+
+ expect(document.querySelector('.dnb-forms-status')).not.toHaveClass(
+ 'dnb-forms-status--error'
+ )
+ })
+
+ it('should call onCancel when clicking on cancel button', () => {
+ const formId = {}
+ const onCancel = jest.fn()
+
+ render(
+
+
+
+
+
+ )
+
+ act(() => {
+ Form.Status.setStatus(formId, 'error')
+ })
+
+ expect(document.querySelector('.dnb-forms-status')).toHaveClass(
+ 'dnb-forms-status--error'
+ )
+ expect(onCancel).not.toHaveBeenCalled()
+
+ const buttons = document.querySelectorAll('button')
+ expect(buttons).toHaveLength(2)
+
+ const [backButton] = Array.from(buttons)
+
+ fireEvent.click(backButton)
+ expect(onCancel).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Status/index.ts b/packages/dnb-eufemia/src/extensions/forms/Form/Status/index.ts
new file mode 100644
index 00000000000..1ef184788df
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/Status/index.ts
@@ -0,0 +1,2 @@
+export { default } from './Status'
+export * from './Status'
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Status/setStatus.ts b/packages/dnb-eufemia/src/extensions/forms/Form/Status/setStatus.ts
new file mode 100644
index 00000000000..7da1aea5380
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/Status/setStatus.ts
@@ -0,0 +1,11 @@
+import {
+ SharedStateId,
+ createSharedState,
+} from '../../../../shared/helpers/useSharedState'
+
+export type Status = 'success' | 'error' | undefined
+
+export default function setStatus(id: SharedStateId, status: Status) {
+ const sharedState = createSharedState(id)
+ sharedState.extend({ activeStatus: status })
+}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/Status/stories/Status.stories.tsx b/packages/dnb-eufemia/src/extensions/forms/Form/Status/stories/Status.stories.tsx
new file mode 100644
index 00000000000..e2e368342de
--- /dev/null
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/Status/stories/Status.stories.tsx
@@ -0,0 +1,41 @@
+import { Field, Form } from '../../..'
+import { Button } from '../../../../../components'
+
+export default {
+ title: 'Eufemia/Extensions/Forms/Status',
+}
+
+export function BothStatuses() {
+ const formId = () => null
+ return (
+ <>
+ {
+ await new Promise((r) => setTimeout(r, 1000)) // Simulate a request
+
+ Form.Status.setStatus(formId, 'success')
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ ----- Content ----
+ >
+ )
+}
diff --git a/packages/dnb-eufemia/src/extensions/forms/Form/index.ts b/packages/dnb-eufemia/src/extensions/forms/Form/index.ts
index 3b2c6c245b6..7ccf22388b1 100644
--- a/packages/dnb-eufemia/src/extensions/forms/Form/index.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/Form/index.ts
@@ -11,6 +11,7 @@ export { default as MainHeading } from './MainHeading'
export { default as SubHeading } from './SubHeading'
export { default as Visibility } from './Visibility'
export { default as Section } from './Section'
+export { default as Status } from './Status'
export { default as Card } from './Card'
export { default as Isolation } from './Isolation'
export { default as Snapshot } from './Snapshot'
diff --git a/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts b/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts
index d2df5c1b137..e2d77930b17 100644
--- a/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/constants/locales/en-GB.ts
@@ -35,6 +35,17 @@ export default {
text: 'Remove',
confirmRemoveText: 'Are you sure you want to delete this?',
},
+ StatusSuccess: {
+ title: 'Thank you',
+ description: 'We have received your information.',
+ buttonText: 'Back to homepage',
+ },
+ StatusError: {
+ title: 'Something went wrong',
+ description: 'We were unable to submit the form.',
+ retryButton: 'Try again',
+ cancelButton: 'Back',
+ },
SectionViewContainer: {
editButton: 'Edit',
},
diff --git a/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts b/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts
index 1110f5caee2..54edc58fd4f 100644
--- a/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts
+++ b/packages/dnb-eufemia/src/extensions/forms/constants/locales/nb-NO.ts
@@ -34,6 +34,17 @@ export default {
text: 'Fjern',
confirmRemoveText: 'Er du sikker på at du vil slette dette?',
},
+ StatusSuccess: {
+ title: 'Takk skal du ha',
+ description: 'Vi har mottatt din informasjon.',
+ buttonText: 'Tilbake til forsiden',
+ },
+ StatusError: {
+ title: 'Noe gikk galt',
+ description: 'Vi klarte ikke å sende inn skjemaet.',
+ retryButton: 'Prøv igjen',
+ cancelButton: 'Tilbake',
+ },
SectionViewContainer: {
editButton: 'Endre',
},