diff --git a/jest.config.js b/jest.config.js
index 8e0e44a60c4c9604843f2b326369c1145ea8c215..7e3659738234237b5cdaff14783d201e350a035e 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -4,3 +4,4 @@ module.exports = {
     '\\.[as]?css$': 'identity-obj-proxy',
   },
 };
+
diff --git a/src/App.tsx b/src/App.tsx
index 58ac19afb39e681d30e7f62121396800f9a4c222..696c6c4220b5b97631dc53c429b1a6677f74c976 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -13,7 +13,7 @@ import type { Problem, Suggestion } from './types';
 import ContactForm from './components/contact-form/ContactForm';
 import styles from './App.module.scss';
 
-function App(): JSX.Element {
+export const App: React.FC = () => {
   const { t } = useTranslation();
   const [problemGroup, _setProblemGroup] = useState<string>('');
   const [problem, setProblem] = useState<string>('');
@@ -70,7 +70,7 @@ function App(): JSX.Element {
           <Markdown>{t('subtitle')}</Markdown>
         </p>
       </header>
-      <form className={styles.troubleshooting}>
+      <form className={styles.troubleshooting} aria-label={t("Troubleshooting form")}>
         <ProblemGroupDropdown
           id="problemGroup"
           intro={localized.problemDescribe}
diff --git a/src/__mocks__/Modal.tsx b/src/__mocks__/Modal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2ced19791d471df112345b018574e111c69b6c3f
--- /dev/null
+++ b/src/__mocks__/Modal.tsx
@@ -0,0 +1,48 @@
+import React, { useEffect } from 'react';
+import type { ModalProps } from '../components/modal/Modal';
+
+export const Modal = ({
+  children,
+  DOMNode = 'body',
+  curtain = true,
+  closeCallback,
+  defaultActionCallback,
+  closeButton,
+  header,
+  buttons,
+}: ModalProps): JSX.Element => {
+  const keyUpHandler = (event: KeyboardEvent) => {
+    if (event.key == 'Escape' && typeof closeCallback == 'function')
+      closeCallback();
+    if (event.key == 'Enter' && typeof defaultActionCallback == 'function')
+      defaultActionCallback();
+  };
+
+  useEffect(() => {
+    window.addEventListener('keyup', keyUpHandler);
+    return () => {
+      window.removeEventListener('keyup', keyUpHandler);
+    };
+  });
+  const close = () => typeof closeCallback == 'function' && closeCallback();
+  return (
+    <div className="modal">
+      <header>
+        <h5>{header}</h5>
+        {closeButton && (
+          <button className="close" onClick={close}>
+            ⨯
+          </button>
+        )}
+      </header>
+      <main>{children}</main>
+      <footer>{buttons}</footer>
+      <div>
+        {typeof DOMNode == 'string' ? DOMNode : JSON.stringify(DOMNode)} -{' '}
+        {curtain ? 'curtain' : ''}
+      </div>
+    </div>
+  );
+};
+
+export default Modal;
diff --git a/src/components/contact-form/ContactForm.test.tsx b/src/components/contact-form/ContactForm.test.tsx
index 87e93f62b3d479cfc375c3d26cc278e156040cc5..0a3379e3a1bc1f63cd257b135708382baa8a0c9b 100644
--- a/src/components/contact-form/ContactForm.test.tsx
+++ b/src/components/contact-form/ContactForm.test.tsx
@@ -1,41 +1,143 @@
 import * as React from 'react';
-import { render } from '@testing-library/react';
 import ContactForm from './ContactForm';
+import { config as transitionConfig } from 'react-transition-group';
 import userEvent from '@testing-library/user-event';
+import { fireEvent, render, screen } from '@testing-library/react';
+// Disable CSSTransitions to prevent them from messing with test conditions
+transitionConfig.disabled = true;
 
-const mockSetUrgentIntent = jest.fn();
-const mockSendUrgent = jest.fn();
 const problem = ''; // for now makes no difference
 
-describe.each([
-  ['', false],
-  ['administrative', false],
-  ['technical', false],
-  ['technical', true],
-])('<ContactForm problemGroup={%s}>', (problemGroup) => {
-  it(`renders the contact form with problemGroup: ${problemGroup}`, () => {
+const fillInContactForm = () => {
+  const nameInput = screen.getByRole('textbox', { name: /name/i });
+  const phoneInput = screen.getByRole('textbox', { name: /phone/i });
+  const emailInput = screen.getByRole('textbox', { name: /e-?mail/i });
+  const messageInput = screen.getByRole('textbox', { name: /message/i });
+
+  expect(nameInput).toBeInTheDocument();
+  expect(phoneInput).toBeInTheDocument();
+  expect(emailInput).toBeInTheDocument();
+  expect(messageInput).toBeInTheDocument();
+
+  // Note, fields are checked server side so don't expect validation here.
+  userEvent.type(nameInput, 'Maurice Moss');
+  userEvent.type(phoneInput, '0118 999 881 99 9119 7253');
+  userEvent.type(emailInput, 'moss@itcrowd.co.uk');
+  userEvent.type(
+    messageInput,
+    'Dear sir/madam,\n\n' +
+      'Fire! Fire! Help me!\n\n' +
+      '123, Caladin road\n\n' +
+      'Looking forward to hearing from you.\n\n' +
+      'All the best,\n' +
+      'Maurice Moss.',
+  );
+  // Let's have one check here..
+  expect(nameInput).toHaveValue('Maurice Moss');
+};
+
+describe('<ContactForm problemGroup="%s"}>', () => {
+  it.each([[''], ['administrative'], ['technical']])(
+    'renders the contact form with problemGroup: %s',
+    (problemGroup) => {
+      const { getByRole, queryByRole, getByText } = render(
+        <ContactForm {...{ problemGroup, problem }} />,
+      );
+      const contactDetailFieldset = getByRole('group', {
+        name: /Contact details/i,
+      });
+      const messageFieldset = getByRole('group', {
+        name: /what do you want to tell us/i,
+      });
+
+      const urgencyCheckbox = queryByRole('checkbox', {
+        name: /urgent request/i,
+      });
+
+      if (problemGroup == 'technical')
+        expect(urgencyCheckbox).toBeInTheDocument();
+      else expect(urgencyCheckbox).not.toBeInTheDocument();
+
+      const regularSubmit = getByRole('button', { name: /submit request/i });
+      expect(regularSubmit).toBeInTheDocument();
+
+      expect(contactDetailFieldset).toBeInTheDocument();
+      expect(messageFieldset).toBeInTheDocument();
+    },
+  );
+  it.each([
+    ['pressing key', '{esc}'],
+    ['pressing key', '{enter}'],
+    ['clicking button', 'Okay'],
+    ['clicking button', 'close'],
+  ])('confirmation modal closes on %s %s', (action, value) => {
+    const problemGroup = 'technical';
     const { getByRole } = render(
       <ContactForm {...{ problemGroup, problem }} />,
     );
-    const contactDetailFieldset = getByRole('group', {
-      name: /Contact details/i,
-    });
-    const messageFieldset = getByRole('group', {
-      name: /what do you want to tell us/i,
-    });
     const regularSubmit = getByRole('button', { name: /submit request/i });
 
-    expect(contactDetailFieldset).toBeInTheDocument();
-    expect(messageFieldset).toBeInTheDocument();
+    userEvent.click(regularSubmit);
+
+    const modal = getByRole('dialog', { name: /thank you/i });
+    expect(modal).toBeInTheDocument();
+
+    if (/clicking/.test(action)) {
+      // Close modal by clicking a button..
+      const button = getByRole('button', { name: value });
+      userEvent.click(button);
+    } else {
+      // Close modal by pressing a key..
+      const body = document.getElementsByTagName('body')[0];
+      userEvent.type(body, value);
+    }
+    expect(modal).not.toBeInTheDocument();
+  });
+
+  it('can be submitted by a form submit event', ()=>{
+    const problemGroup = 'technical';
+    const { getByRole } = render(
+      <ContactForm {...{ problemGroup, problem }} />,
+    );
+    const form = getByRole("form", {name:"Contact form"});
+    fireEvent.submit(form);
+    const modal = getByRole('dialog', { name: /thank you/i });
+    expect(modal).toBeInTheDocument();
+  });
+
+  it('has an option to make the request urgent', () => {
+    const problemGroup = 'technical';
+    const { getByText, getByRole } = render(
+      <ContactForm {...{ problemGroup, problem }} />,
+    );
+    const urgencyCheckbox = getByRole('checkbox', {
+      name: /urgent request/i,
+    });
+    expect(urgencyCheckbox).toBeInTheDocument();
+
+    const regularSubmit = getByRole('button', { name: /submit request/i });
     expect(regularSubmit).toBeInTheDocument();
 
-    /**
-     *  To test: 
-     *  - Fields are rendered
-     *  - Fill in some fields
-     *  - Not filling in required fields leads to errors -> integration test or mocked, or end-to-end?
-     *  - Form submission, urgent and regular, triggers a call to a function
-     */
-     
+    fillInContactForm();
+
+    userEvent.click(urgencyCheckbox);
+
+    const urgencyInput = getByRole('textbox', { name: /type urgent/i });
+    userEvent.type(urgencyInput, 'URGENT');
+    expect(regularSubmit).not.toBeInTheDocument();
+    const urgentSubmit = getByRole('button', {
+      name: /submit urgent request/i,
+    });
+    expect(urgentSubmit).toBeInTheDocument();
+    userEvent.click(urgentSubmit);
+
+    const dialogMain = getByText(/fire!/i);
+    expect(dialogMain).toBeInTheDocument();
+    // Close modal by pressing okay..
+    const modal = getByRole('dialog', { name: /thank you/i });
+    expect(modal).toBeInTheDocument();
+    const okay = getByRole('button', { name: /okay/i });
+    userEvent.click(okay);
+    expect(modal).not.toBeInTheDocument();
   });
 });
diff --git a/src/components/contact-form/ContactForm.tsx b/src/components/contact-form/ContactForm.tsx
index d58b01b4e9ec481936ac4c10b3509a022a5910e0..3f8dc081945ade47761c4542e65097c3d20a2598 100644
--- a/src/components/contact-form/ContactForm.tsx
+++ b/src/components/contact-form/ContactForm.tsx
@@ -1,4 +1,3 @@
-import Markdown from 'markdown-to-jsx';
 import React, { ChangeEvent, FormEvent, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import UrgentRequest from '../urgent-request/UrgentRequest';
@@ -7,15 +6,19 @@ import styles from './ContactForm.module.scss';
 import panelTransition from '../../styles/transitions/panel.module.scss';
 import Fieldset from '../fieldset/Fieldset';
 import Input from '../input/Input';
+import Modal from '../modal/Modal';
 
 export interface ContactFormProps {
   problemGroup: string;
   problem: string;
 }
-
-function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element {
+export const ContactForm: React.FC<ContactFormProps> = ({
+  problemGroup,
+  problem,
+}: ContactFormProps) => {
   const [urgentIntent, setUrgentIntent] = useState<boolean>(false);
   const [state, setState] = useState<Record<string, string>>({});
+  const [modalContent, setModalContent] = useState<string>('');
   const { t } = useTranslation();
 
   const changeHandler = (
@@ -28,10 +31,10 @@ function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element {
     setUrgentIntent(event.target.checked);
   };
 
-  function submit(event: FormEvent<HTMLFormElement>) {
+  const submit = (event: FormEvent<HTMLFormElement>) => {
     event.preventDefault();
     sendRequest();
-  }
+  };
 
   const sendRequest = (urgent = false) => {
     const formData = { ...state, ...{ urgent: urgent ? 'yes' : 'no' } };
@@ -41,13 +44,12 @@ function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element {
         `SupportForm[${name}]`,
         value,
       ]),
-    ),
-      console.log(formData);
+    );
+    setModalContent(JSON.stringify(formData));
   };
 
   return (
-    <form onSubmit={submit} className={`${styles.ContactForm}`}>
-      
+    <form onSubmit={submit} className={`${styles.ContactForm}`} aria-label={t("Contact form")}>
       <Fieldset legend={t('contact.details')}>
         <Input
           label={t('contact.name')}
@@ -108,6 +110,7 @@ function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element {
             mountOnEnter={true}
             addEndListener={(node: HTMLElement, done: EventListener) => {
               // use the css transitionend event to mark the finish of a transition
+              // istanbul ignore next
               node.addEventListener('transitionend', done, false);
             }}
           >
@@ -117,7 +120,7 @@ function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element {
               <button
                 className="good"
                 onClick={(event) => {
-                  event?.preventDefault();
+                  event.preventDefault();
                   sendRequest();
                 }}
               >
@@ -127,8 +130,22 @@ function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element {
           </CSSTransition>
         </SwitchTransition>
       </div>
+      {modalContent && (
+        <Modal
+          defaultActionCallback={() => setModalContent('')}
+          closeCallback={() => setModalContent('')}
+          buttons={
+            <button className="good" onClick={() => setModalContent('')}>
+              {t('Okay')}
+            </button>
+          }
+          header={t('Thank you')}
+        >
+          {modalContent}
+        </Modal>
+      )}
     </form>
   );
-}
+};
 
 export default ContactForm;
diff --git a/src/components/fieldset/Fieldset.tsx b/src/components/fieldset/Fieldset.tsx
index 067f797f1b1b57d10baa50a0aac9f5028659f5e8..922219412998b2af2d5ce78d6badb59f13c3b653 100644
--- a/src/components/fieldset/Fieldset.tsx
+++ b/src/components/fieldset/Fieldset.tsx
@@ -5,14 +5,15 @@ interface FieldsetProps {
   legend?: React.ReactNode;
 }
 
-export default function Fieldset({
+export const Fieldset: React.FC<FieldsetProps> = ({
   children,
   legend,
-}: FieldsetProps): JSX.Element {
+}: FieldsetProps) => {
   return (
     <fieldset>
       <legend>{legend}</legend>
       {children}
     </fieldset>
   );
-}
+};
+export default Fieldset;
\ No newline at end of file
diff --git a/src/components/input/Input.test.tsx b/src/components/input/Input.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5f08dd35110154094093e36f9cb3696928731d1c
--- /dev/null
+++ b/src/components/input/Input.test.tsx
@@ -0,0 +1,77 @@
+import * as React from 'react';
+import { render } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import Input from './Input';
+
+const changeHandler = jest.fn();
+
+describe('<Input>', () => {
+  it(`has a proper label for a11y`, () => {
+    const { getByRole } = render(<Input id="id" name="name" label="Name" />);
+    // This searches by a11y "name", labels provide names to inputs.
+    const input = getByRole('textbox', { name: 'Name' });
+    expect(input).toBeInTheDocument();
+  });
+
+  it(`can be set to value when uncontroller`, () => {
+    const { getByRole } = render(<Input id="id" name="name" label="Name" />);
+    const input = getByRole('textbox', { name: 'Name' });
+    userEvent.type(input, 'Hello world!');
+    expect(input).toHaveValue('Hello world!');
+  });
+
+  it(`can be set to value when controller`, () => {
+    const { getByRole } = render(
+      <Input
+        id="id"
+        name="name"
+        label="Name"
+        value=""
+        onChange={changeHandler}
+      />,
+    );
+    const input = getByRole('textbox', { name: 'Name' });
+    userEvent.type(input, 'Hello world!');
+    expect(changeHandler).toHaveBeenCalledTimes(12);
+  });
+
+  it(`can receive some extra classes`, () => {
+    const { getByRole } = render(
+      <Input classNames={['bad', 'robot']} id="id" name="name" label="Name" />,
+    );
+    const input = getByRole('textbox', { name: 'Name' });
+    expect(input.parentElement).toHaveClass('bad');
+    expect(input.parentElement).toHaveClass('robot');
+  });
+
+  it(`can be a required field`, () => {
+    const { getByRole } = render(
+      <Input required id="id" name="name" label="Name" />,
+    );
+    const input = getByRole('textbox', { name: 'Name' });
+    expect(input.parentElement).toHaveClass('mandatory');
+    expect(input).toHaveAttribute('required');
+  });
+
+  it(`can be a disabled field`, () => {
+    const { getByRole } = render(
+      <Input disabled id="id" name="name" label="Name" />,
+    );
+    const input = getByRole('textbox', { name: 'Name' });
+    expect(input).toHaveAttribute('disabled');
+    userEvent.type(input, "Hello world!");
+    expect(input).toHaveValue('');
+  });
+  it.each([
+    ['textarea', 'textbox'],
+    ['email', 'textbox'],
+    ['number', 'spinbutton'],
+    ['checkbox', 'checkbox'],
+  ])(`can be another type than text`, (type, role) => {
+    const { getByRole } = render(
+      <Input type={type} id="id" name="name" label="Name" />,
+    );
+    const input = getByRole(role, { name: 'Name' });
+    expect(input).toBeInTheDocument();
+  });
+});
diff --git a/src/components/input/Input.tsx b/src/components/input/Input.tsx
index dae027c7b7a1839ad151a9a3c850814a7b41f9de..875665d5d45615f7d6ec90a730ed93056cc3fece 100644
--- a/src/components/input/Input.tsx
+++ b/src/components/input/Input.tsx
@@ -4,33 +4,29 @@ interface InputProps {
   id: string;
   name?: string;
   label: React.ReactNode;
-  classes?: string[];
+  classNames?: string[];
   value?: string | undefined;
   required?: boolean;
   type?: string;
+  disabled?: boolean;
   onChange?: undefined | ReactEventHandler;
-  ref?: React.RefObject<unknown>;
 }
-export default function Input({
+export const Input: React.FC<InputProps> = ({
   id,
-  name,
   label,
-  classes = [],
-  // Applying this binds value to input.value, making it a controlled component
-  // You must also supply onChange in this case, or the field will be read-only
-  value = undefined, 
-  required = false,
+  classNames = [],
   type = 'text',
+  required,
   onChange = undefined,
-  ref = undefined
-}: InputProps): JSX.Element {
-  classes = ['field', ...classes];
-  if (required) classes.push('mandatory');
+  ...args
+}: InputProps) => {
+  classNames = ['field', ...classNames];
+  if (required) classNames.push('mandatory');
 
-  const attributes = { id, name, type, onChange, value, ref};
+  const attributes = { id, type, onChange, required, ...args };
 
   return (
-    <div className={classes.join(' ')}>
+    <div className={classNames.join(' ')}>
       <label htmlFor={id}>{label}</label>
       {type == 'textarea' ? (
         <textarea {...attributes}></textarea>
@@ -39,4 +35,5 @@ export default function Input({
       )}
     </div>
   );
-}
+};
+export default Input;
diff --git a/src/components/locale-switcher/LocaleSwitcher.test.tsx b/src/components/locale-switcher/LocaleSwitcher.test.tsx
index a671108f30d78ffdb51c8e3e3825b3b1da7cc4d1..b4cf024bab4f603aaed3014b49cdb46ff2768fe3 100644
--- a/src/components/locale-switcher/LocaleSwitcher.test.tsx
+++ b/src/components/locale-switcher/LocaleSwitcher.test.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import { screen, render, fireEvent } from '@testing-library/react';
+import { screen, render } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import LocaleSwitcher from './LocaleSwitcher';
 import i18next, { Resource } from 'i18next';
diff --git a/src/components/locale-switcher/LocaleSwitcher.tsx b/src/components/locale-switcher/LocaleSwitcher.tsx
index 6d04c23b2ea95d8f06dc0eefb08e5d2bfe01a822..c3e8cdcc922c4b96301d3b789cd62428da97921d 100644
--- a/src/components/locale-switcher/LocaleSwitcher.tsx
+++ b/src/components/locale-switcher/LocaleSwitcher.tsx
@@ -1,20 +1,25 @@
 import React from 'react';
 import { useTranslation } from 'react-i18next';
 
-function LocaleSwitcher() {
-  const { t, i18n  } = useTranslation();
+export const LocaleSwitcher: React.FC = () => {
+  const { t, i18n } = useTranslation();
   return (
     <select
       value={i18n.language}
       onChange={(e) => i18n.changeLanguage(e.target.value)}
     >
-      { 
-        Object.entries(i18n.options.resources!).map(([localeCode, _]) => 
-          <option key={localeCode} value={localeCode}>{t("language", {lng: localeCode})}</option>
-        )
+      {
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        Object.entries(i18n.options.resources!).map(([localeCode, _]) => (
+          <option key={localeCode} value={localeCode}>
+            {t('language', { lng: localeCode })}
+          </option>
+        ))
       }
-      <option key="dev" value="dev">localisation keys (dev)</option>
+      <option key="dev" value="dev">
+        localisation keys (dev)
+      </option>
     </select>
   );
-}
+};
 export default LocaleSwitcher;
diff --git a/src/components/modal/Modal.test.tsx b/src/components/modal/Modal.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..96dceb149cbc254486edd500d4efae7ac3ff3f65
--- /dev/null
+++ b/src/components/modal/Modal.test.tsx
@@ -0,0 +1,87 @@
+import * as React from 'react';
+import { render } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import Modal from './Modal';
+
+const mockOkayClick = jest.fn();
+const mockDefaultAction = jest.fn();
+const closeCallback = jest.fn();
+
+jest.unmock('./Modal');
+
+describe('<Modal ..>', () => {
+  it.each([
+    ['esc', closeCallback],
+    ['enter', mockDefaultAction],
+  ])('the Modal can call callbacks for key %s', (key, callback) => {
+    render(
+      <Modal
+        header="Coffee"
+        defaultActionCallback={mockDefaultAction}
+        closeCallback={closeCallback}
+        buttons={<></>}
+      >
+        <strong>double shot</strong>
+      </Modal>,
+    );
+    const body = document.getElementsByTagName('body')[0];
+    userEvent.type(body, `{${key}}`);
+    expect(callback).toBeCalled();
+  });
+
+  test(`The modal renders buttons`, () => {
+    const { getByRole } = render(
+      <Modal
+        header="Coffee"
+        buttons={<button onClick={mockOkayClick}>Make coffee</button>}
+      >
+        <strong>double shot</strong>
+      </Modal>,
+    );
+    expect(getByRole('button', { name: /make coffee/i })).toBeInTheDocument();
+  });
+
+  test(`modal default options`, () => {
+    const { getByRole } = render(
+      <Modal
+        header="Coffee"
+        buttons={<></>}
+        curtain={true}
+        closeButton={true}
+        closeCallback={closeCallback}
+      >
+        <strong>double shot</strong>
+      </Modal>,
+    );
+    const wrapper = document.getElementsByClassName('modal-wrapper')[0];
+    expect(wrapper).toHaveClass('curtain');
+    userEvent.click(getByRole('button', { name: 'close' }));
+    expect(closeCallback).toBeCalled();
+  });
+
+  test(`modal non-default options`, () => {
+    const { queryByRole } = render(
+      <Modal
+        header="Coffee"
+        buttons={<></>}
+        curtain={false}
+        closeButton={false}
+        closeCallback={closeCallback}
+      >
+        <strong>double shot</strong>
+      </Modal>,
+    );
+    const wrapper = document.getElementsByClassName('modal-wrapper')[0];
+    expect(wrapper).not.toHaveClass('curtain');
+    expect(queryByRole('button', { name: 'close' })).not.toBeInTheDocument();
+  });
+
+  it(`Get an error when passed a bad dom node selector`, () => {
+    const { getByText } = render(
+      <Modal header="Coffee" DOMNodeSelector=".badselector" buttons={<></>}>
+        <strong>double shot</strong>
+      </Modal>,
+    );
+    expect(getByText(/did not yield a dom node/i)).toBeInTheDocument();
+  });
+});
diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b7fb6c384b7f1620f158d34ff9599626fe71251e
--- /dev/null
+++ b/src/components/modal/Modal.tsx
@@ -0,0 +1,85 @@
+import React, { useEffect } from 'react';
+import ReactDOM from 'react-dom';
+
+export interface ModalProps {
+  DOMNodeSelector?: string;
+  curtain?: boolean;
+  children: React.ReactNode;
+  header: React.ReactNode;
+  defaultActionCallback?: CallableFunction | undefined;
+  closeCallback?: CallableFunction | undefined;
+  closeButton?: boolean;
+  buttons: React.ReactNode;
+}
+
+export const Modal: React.FC<ModalProps> = ({
+  children,
+  closeCallback,
+  defaultActionCallback,
+  header,
+  buttons,
+  DOMNodeSelector = 'body',
+  curtain = true,
+  closeButton = true,
+}: ModalProps) => {
+  const wrapper = document.createElement('div');
+  wrapper.classList.add('modal-wrapper');
+  if (curtain) wrapper.classList.add('curtain');
+  const portal = document.querySelector(DOMNodeSelector);
+  if (!portal)
+    return (
+      <strong>
+        Querying &quot;{DOMNodeSelector}&quot; did not yield a DOM node
+      </strong>
+    );
+
+  // Mount and unmount the modal in the portal
+  useEffect(() => {
+    portal.appendChild(wrapper);
+    portal.classList.add('modal-active');
+    return () => {
+      portal.removeChild(wrapper);
+      portal.classList.remove('modal-active');
+    };
+  }, []);
+
+  const keyUpHandler = (event: KeyboardEvent) => {
+    if (event.key == 'Escape' && typeof closeCallback == 'function')
+      closeCallback();
+    if (event.key == 'Enter' && typeof defaultActionCallback == 'function')
+      defaultActionCallback();
+  };
+
+  useEffect(() => {
+    window.addEventListener('keyup', keyUpHandler);
+    return () => {
+      window.removeEventListener('keyup', keyUpHandler);
+    };
+  });
+
+  const randomEnough = (+new Date()).toString(36).slice(-10);
+
+  const close = () => typeof closeCallback == 'function' && closeCallback();
+  return ReactDOM.createPortal(
+    <div
+      className="modal"
+      role="dialog"
+      aria-labelledby={`${randomEnough}-header`}
+      aria-describedby={`${randomEnough}-main`}
+    >
+      <header id={`${randomEnough}-header`}>
+        <h5>{header}</h5>
+        {closeButton && (
+          <button className="close" onClick={close} aria-label="close">
+            ⨯
+          </button>
+        )}
+      </header>
+      <main id={`${randomEnough}-main`}>{children}</main>
+      <footer>{buttons}</footer>
+    </div>,
+    wrapper,
+  );
+};
+
+export default Modal;
diff --git a/src/components/problem-dropdown/ProblemDropdown.tsx b/src/components/problem-dropdown/ProblemDropdown.tsx
index 79d1e7ea873466af50ec40c065c3f73ebb53605e..5fe2ecb0ce8b003d4ff73e6edfcaa25356a39cd6 100644
--- a/src/components/problem-dropdown/ProblemDropdown.tsx
+++ b/src/components/problem-dropdown/ProblemDropdown.tsx
@@ -6,9 +6,11 @@ export interface ProblemDropdownProps extends GenericProblemDropdownProps {
   problems: Record<string, Problem>;
 }
 
-export function ProblemDropdown(props: ProblemDropdownProps): JSX.Element {
+export const ProblemDropdown: React.FC<ProblemDropdownProps> = (
+  props: ProblemDropdownProps,
+) => {
   return <GenericProblemDropdown {...props} />;
-}
+};
 
 export interface ProblemGroupDropdownProps {
   id: string;
@@ -19,14 +21,14 @@ export interface ProblemGroupDropdownProps {
   problemGroups: Record<string, ProblemGroup>;
 }
 
-export function ProblemGroupDropdown({
+export const ProblemGroupDropdown: React.FC<ProblemGroupDropdownProps> = ({
   id,
   intro,
   selectProblemGroup,
   problemGroup,
   setProblemGroup,
   problemGroups,
-}: ProblemGroupDropdownProps): JSX.Element {
+}: ProblemGroupDropdownProps) => {
   return (
     <GenericProblemDropdown
       id={id}
@@ -37,7 +39,7 @@ export function ProblemGroupDropdown({
       setProblem={setProblemGroup}
     />
   );
-}
+};
 
 export interface GenericProblemDropdownProps {
   id: string;
@@ -48,14 +50,14 @@ export interface GenericProblemDropdownProps {
   setProblem: CallableFunction;
 }
 
-function GenericProblemDropdown({
+export const GenericProblemDropdown: React.FC<GenericProblemDropdownProps> = ({
   id,
   intro,
   selectProblem,
   problems,
   problem,
   setProblem,
-}: GenericProblemDropdownProps) {
+}: GenericProblemDropdownProps) => {
   return (
     <div className="field">
       <label htmlFor={id}>{intro}</label>
@@ -75,4 +77,4 @@ function GenericProblemDropdown({
       </select>
     </div>
   );
-}
+};
diff --git a/src/components/spinner/Spinner.tsx b/src/components/spinner/Spinner.tsx
index 6bff6b33d6a4945ecceab63b04c3719874f09d7b..b481f3044534ffcbabcb5f93732ab1e804f1f1d5 100644
--- a/src/components/spinner/Spinner.tsx
+++ b/src/components/spinner/Spinner.tsx
@@ -5,7 +5,10 @@ export interface SpinnerProps {
   loading?: boolean;
 }
 
-function Spinner({ big = false, loading = true }: SpinnerProps): JSX.Element {
+export const Spinner: React.FC<SpinnerProps> = ({
+  big = false,
+  loading = true,
+}: SpinnerProps) => {
   if (!loading) return <div className="spinner"></div>;
   const classes = ['spinner', 'loading'];
   if (big) classes.push('big');
@@ -16,6 +19,6 @@ function Spinner({ big = false, loading = true }: SpinnerProps): JSX.Element {
       aria-label="Loading"
     ></div>
   );
-}
+};
 
 export default Spinner;
diff --git a/src/components/suggestion-list/ProblemSuggestionList.tsx b/src/components/suggestion-list/ProblemSuggestionList.tsx
index 2cc6b93f583204e2e26e3d3578d5844f1c57ba61..4da00ba10b67a83e1500091f08dc14cfa7f30496 100644
--- a/src/components/suggestion-list/ProblemSuggestionList.tsx
+++ b/src/components/suggestion-list/ProblemSuggestionList.tsx
@@ -3,28 +3,28 @@ import React from 'react';
 import type { Suggestion } from 'src/types';
 
 export interface ProblemSuggestionListProps {
-  intro: string,
-  suggestions: Suggestion[]
+  intro: string;
+  suggestions: Suggestion[];
 }
 
-function ProblemSuggestionList({
+export const ProblemSuggestionList: React.FC<ProblemSuggestionListProps> = ({
   intro,
   suggestions,
-}: ProblemSuggestionListProps) {
+}: ProblemSuggestionListProps) => {
   return (
     <>
-      <p><Markdown>{intro}</Markdown></p>
+      <p>
+        <Markdown>{intro}</Markdown>
+      </p>
       <ul>
         {suggestions.map((suggestion: Suggestion, i: number) => (
           <li key={i}>
-            <a href={suggestion.link}>
-              {suggestion.description}
-            </a>
+            <a href={suggestion.link}>{suggestion.description}</a>
           </li>
         ))}
       </ul>
     </>
   );
-}
+};
 
 export default ProblemSuggestionList;
diff --git a/src/components/urgent-request/UrgentRequest.module.scss b/src/components/urgent-request/UrgentRequest.module.scss
index b0ae6d28841e3b09fee41df30e596d7a3f53abfe..4c4ce9b50b02724c9934132c19aa93af353a64e2 100644
--- a/src/components/urgent-request/UrgentRequest.module.scss
+++ b/src/components/urgent-request/UrgentRequest.module.scss
@@ -1,3 +1,3 @@
 .UrgentRequest {
-  border: 1px rgb(113, 0, 10) solid;
+  border: 2px rgb(113, 0, 10) solid;
 }
diff --git a/src/components/urgent-request/UrgentRequest.test.tsx b/src/components/urgent-request/UrgentRequest.test.tsx
index 422a9c95da902fea331b0e1ac5b33799b9dc7220..456f4aae43fe4fa140cacd6f2c151602a278e370 100644
--- a/src/components/urgent-request/UrgentRequest.test.tsx
+++ b/src/components/urgent-request/UrgentRequest.test.tsx
@@ -2,7 +2,6 @@ import * as React from 'react';
 import { render } from '@testing-library/react';
 import UrgentRequest from './UrgentRequest';
 import userEvent from '@testing-library/user-event';
-import styles from './UrgentRequest.module.scss';
 
 const mockSendUrgent = jest.fn();
 
diff --git a/src/components/urgent-request/UrgentRequest.tsx b/src/components/urgent-request/UrgentRequest.tsx
index 5e373fcd9821f8724f19a4977a10fdbe297af440..aca85881014483056bb1259e6f53b351ea8fc8a6 100644
--- a/src/components/urgent-request/UrgentRequest.tsx
+++ b/src/components/urgent-request/UrgentRequest.tsx
@@ -7,7 +7,9 @@ export interface UrgentRequestProps {
   sendUrgent: CallableFunction;
 }
 
-function UrgentRequest({ sendUrgent }: UrgentRequestProps): JSX.Element {
+export const UrgentRequest: React.FC<UrgentRequestProps> = ({
+  sendUrgent,
+}: UrgentRequestProps) => {
   const [urgent, setUrgent] = useState<boolean>(false);
   const [urgentText, setUrgentText] = useState<string>('');
   const { t } = useTranslation();
@@ -43,6 +45,6 @@ function UrgentRequest({ sendUrgent }: UrgentRequestProps): JSX.Element {
       </button>
     </fieldset>
   );
-}
+};
 
 export default UrgentRequest;
diff --git a/src/tests/App.test.tsx b/src/tests/App.test.tsx
index aea792a8b0192c4c3529e382c577f93dd3e26999..ea3c3c853375d87dfc1b86afda8812030661eecc 100644
--- a/src/tests/App.test.tsx
+++ b/src/tests/App.test.tsx
@@ -1,10 +1,12 @@
 import * as React from 'react';
-import { act, render } from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
 import App from '../App';
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 import config from '../config.yml';
 import userEvent from '@testing-library/user-event';
-import urgencyStyles from '../components/urgent-request/UrgentRequest.scss';
+import { config as transitionConfig } from 'react-transition-group';
+// Disable CSSTransitions to prevent them from messing with test conditions
+transitionConfig.disabled = true;
 
 // Use a fixture instead of the real config file.
 jest.mock('../config.yml', () => require('../__mocks__/config.json'));
@@ -80,6 +82,15 @@ describe('<App> with mocked config', () => {
       name: 'How would you best describe your technical problem?',
     });
 
+    // For technical issues, an urgent request can be made.
+    const urgencyCheckbox = getByRole('checkbox', {
+      name: /Urgent request/,
+    });
+    expect(urgencyCheckbox).toBeInTheDocument();
+
+    // Assert that the urgency form is still hidden
+    expect(queryByText(/Confirm urgent request/i)).not.toBeInTheDocument();
+
     // We tell the form we have a hosting problem
     userEvent.selectOptions(problemInput, ['hosting']);
 
@@ -91,17 +102,7 @@ describe('<App> with mocked config', () => {
     expect(getByText(/Recyling the gremlin states/i)).toBeInTheDocument();
     expect(getByText(/Generic technical problems fix/i)).toBeInTheDocument();
 
-    // For technical issues, an urgent request can be made.
-    const urgencyCheckbox = getByRole('checkbox', {
-      name: /Urgent request/,
-    });
-    expect(urgencyCheckbox).toBeInTheDocument();
-
-    // Assert that the urgency form is still hidden
-    expect(queryByText(/Confirm urgent request/i)).not.toBeInTheDocument();
-
     userEvent.click(urgencyCheckbox);
-
     // Assert that the urgency form is now visible
     expect(getByText(/Confirm urgent request/i)).toBeInTheDocument();