diff --git a/packages/css/src/dialog/README.md b/packages/css/src/dialog/README.md
new file mode 100644
index 0000000000..7b0ee41d51
--- /dev/null
+++ b/packages/css/src/dialog/README.md
@@ -0,0 +1,23 @@
+# Dialog
+
+A Dialog allows the user to focus on one task or a piece of information, by popping-up and blocking the page content until the modal task is completed, or until the user dismisses the action.
+
+## Guidelines
+
+- Use Dialog sparingly, because it interrupts the user's workflow.
+- Use Dialog for short and non-frequent tasks. For common tasks consider using the main flow.
+
+## Keyboard Support
+
+| key | function |
+| :---------- | :----------------------------------------------------------- |
+| Tab | Moves focus to next focusable element inside the dialog. |
+| Shift + Tab | Moves focus to previous focusable element inside the dialog. |
+| Escape | Closes the dialog. |
+
+## References
+
+- [HTMLDialogElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement)
+- [Return value](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue)
+- [Modal & Nonmodal Dialogs: When (& When Not) to Use Them](https://www.nngroup.com/articles/modal-nonmodal-dialog/)
+- [Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
diff --git a/packages/css/src/dialog/dialog.scss b/packages/css/src/dialog/dialog.scss
new file mode 100644
index 0000000000..7259222b25
--- /dev/null
+++ b/packages/css/src/dialog/dialog.scss
@@ -0,0 +1,92 @@
+/**
+ * @license EUPL-1.2+
+ * Copyright (c) 2023 Gemeente Amsterdam
+ */
+
+@import "../../utils/breakpoint";
+
+.amsterdam-dialog {
+ background-color: var(--amsterdam-dialog-background-color);
+ border: var(--amsterdam-dialog-border);
+ inset: 0;
+ max-inline-size: var(--amsterdam-dialog-max-inline-size);
+ padding-block: 0;
+ padding-inline: 0;
+ position: fixed;
+
+ /* no token because dialog does not inherit from any element */
+ &::backdrop {
+ background: #0006;
+ }
+}
+
+.amsterdam-dialog__form {
+ display: grid;
+ gap: var(--amsterdam-dialog-form-gap);
+ grid-template-rows: auto 1fr auto;
+ max-block-size: var(--amsterdam-dialog-form-max-block-size);
+ padding-block: var(--amsterdam-dialog-form-padding-block);
+ padding-inline: var(--amsterdam-dialog-form-padding-inline);
+}
+
+.amsterdam-dialog__article {
+ display: grid;
+ gap: 1.5rem; /* Until we have a consistent way of spacing text elements */
+ max-block-size: 100%; /* safari */
+ overflow-y: auto;
+ overscroll-behavior-y: contain;
+ padding-inline-end: var(--amsterdam-dialog-article-padding-inline-end);
+}
+
+.amsterdam-dialog__header {
+ align-items: flex-start;
+ display: flex;
+ gap: var(--amsterdam-dialog-header-gap);
+}
+
+@mixin reset {
+ -webkit-text-size-adjust: 100%;
+}
+
+.amsterdam-dialog__title {
+ color: var(--amsterdam-dialog-title-color);
+ flex: auto;
+ font-family: var(--amsterdam-dialog-title-font-family);
+ font-size: var(--amsterdam-dialog-title-narrow-font-size);
+ font-weight: var(--amsterdam-dialog-title-font-weight);
+ line-height: var(--amsterdam-dialog-title-line-height);
+
+ @media screen and (min-width: $amsterdam-breakpoint-typography) {
+ font-size: var(--amsterdam-dialog-title-wide-font-size);
+ }
+
+ @include reset;
+}
+
+.amsterdam-dialog__footer {
+ display: flex;
+ flex-direction: column;
+ grid-gap: var(--amsterdam-dialog-footer-gap);
+ padding-block: var(--amsterdam-dialog-footer-padding-block);
+
+ @media screen and (min-width: $amsterdam-breakpoint-medium) {
+ flex-direction: row;
+ justify-content: end;
+ }
+}
+
+.amsterdam-dialog__close {
+ background-color: var(--amsterdam-dialog-close-background-color);
+ border: 0;
+ cursor: pointer;
+ padding-block: 0;
+ padding-inline: 0;
+}
+
+.amsterdam-dialog__close svg {
+ fill: var(--amsterdam-dialog-close-fill);
+}
+
+.amsterdam-dialog__close:hover svg {
+ fill: var(--amsterdam-dialog-close-hover-fill);
+}
diff --git a/packages/css/src/index.scss b/packages/css/src/index.scss
index f2a4aa7fd4..7d6321e474 100644
--- a/packages/css/src/index.scss
+++ b/packages/css/src/index.scss
@@ -4,6 +4,7 @@
*/
/* Append here */
+@import "./dialog/dialog";
@import "./image/image";
@import "./pagination/pagination";
@import "./accordion/accordion";
diff --git a/packages/react/src/Dialog/Dialog.test.tsx b/packages/react/src/Dialog/Dialog.test.tsx
new file mode 100644
index 0000000000..fdd20d4d87
--- /dev/null
+++ b/packages/react/src/Dialog/Dialog.test.tsx
@@ -0,0 +1,94 @@
+import { render, screen } from '@testing-library/react'
+import { createRef } from 'react'
+import { Dialog } from './Dialog'
+import '@testing-library/jest-dom'
+
+describe('Dialog', () => {
+ it('renders', () => {
+ render()
+
+ const component = screen.getByRole('dialog')
+
+ expect(component).toBeInTheDocument()
+ expect(component).toBeVisible()
+ })
+
+ it('renders a design system BEM class name', () => {
+ render()
+
+ const component = screen.getByRole('dialog', { hidden: true })
+
+ expect(component).toHaveClass('amsterdam-dialog')
+ })
+
+ it('renders an additional class name', () => {
+ render()
+
+ const component = screen.getByRole('dialog', { hidden: true })
+
+ expect(component).toHaveClass('extra')
+
+ expect(component).toHaveClass('amsterdam-dialog')
+ })
+
+ it('supports ForwardRef in React', () => {
+ const ref = createRef()
+
+ render()
+
+ const component = screen.getByRole('dialog', { hidden: true })
+
+ expect(ref.current).toBe(component)
+ })
+
+ it('is not visible when open attribute is not used', () => {
+ render()
+
+ const component = screen.getByRole('dialog', { hidden: true })
+
+ expect(component).toBeInTheDocument()
+ expect(component).not.toBeVisible()
+ })
+
+ it('renders a title', () => {
+ const { getByText } = render()
+
+ expect(getByText('Dialog Title')).toBeInTheDocument()
+ })
+
+ it('renders children', () => {
+ const { getByText } = render()
+
+ expect(getByText('Dialog Content')).toBeInTheDocument()
+ })
+
+ it('renders actions when provided', () => {
+ const { getByText } = render(