diff --git a/frontend/src/Components/Link/ClipboardButton.js b/frontend/src/Components/Link/ClipboardButton.js
deleted file mode 100644
index 55843f05f..000000000
--- a/frontend/src/Components/Link/ClipboardButton.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import Clipboard from 'clipboard';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import FormInputButton from 'Components/Form/FormInputButton';
-import Icon from 'Components/Icon';
-import { icons, kinds } from 'Helpers/Props';
-import getUniqueElememtId from 'Utilities/getUniqueElementId';
-import styles from './ClipboardButton.css';
-
-class ClipboardButton extends Component {
-
-  //
-  // Lifecycle
-
-  constructor(props, context) {
-    super(props, context);
-
-    this._id = getUniqueElememtId();
-    this._successTimeout = null;
-    this._testResultTimeout = null;
-
-    this.state = {
-      showSuccess: false,
-      showError: false
-    };
-  }
-
-  componentDidMount() {
-    this._clipboard = new Clipboard(`#${this._id}`, {
-      text: () => this.props.value,
-      container: document.getElementById(this._id)
-    });
-
-    this._clipboard.on('success', this.onSuccess);
-  }
-
-  componentDidUpdate() {
-    const {
-      showSuccess,
-      showError
-    } = this.state;
-
-    if (showSuccess || showError) {
-      this._testResultTimeout = setTimeout(this.resetState, 3000);
-    }
-  }
-
-  componentWillUnmount() {
-    if (this._clipboard) {
-      this._clipboard.destroy();
-    }
-
-    if (this._testResultTimeout) {
-      clearTimeout(this._testResultTimeout);
-    }
-  }
-
-  //
-  // Control
-
-  resetState = () => {
-    this.setState({
-      showSuccess: false,
-      showError: false
-    });
-  };
-
-  //
-  // Listeners
-
-  onSuccess = () => {
-    this.setState({
-      showSuccess: true
-    });
-  };
-
-  onError = () => {
-    this.setState({
-      showError: true
-    });
-  };
-
-  //
-  // Render
-
-  render() {
-    const {
-      value,
-      className,
-      ...otherProps
-    } = this.props;
-
-    const {
-      showSuccess,
-      showError
-    } = this.state;
-
-    const showStateIcon = showSuccess || showError;
-    const iconName = showError ? icons.DANGER : icons.CHECK;
-    const iconKind = showError ? kinds.DANGER : kinds.SUCCESS;
-
-    return (
-      <FormInputButton
-        id={this._id}
-        className={className}
-        {...otherProps}
-      >
-        <span className={showStateIcon ? styles.showStateIcon : undefined}>
-          {
-            showSuccess &&
-              <span className={styles.stateIconContainer}>
-                <Icon
-                  name={iconName}
-                  kind={iconKind}
-                />
-              </span>
-          }
-
-          {
-            <span className={styles.clipboardIconContainer}>
-              <Icon name={icons.CLIPBOARD} />
-            </span>
-          }
-        </span>
-      </FormInputButton>
-    );
-  }
-}
-
-ClipboardButton.propTypes = {
-  className: PropTypes.string.isRequired,
-  value: PropTypes.string.isRequired
-};
-
-ClipboardButton.defaultProps = {
-  className: styles.button
-};
-
-export default ClipboardButton;
diff --git a/frontend/src/Components/Link/ClipboardButton.tsx b/frontend/src/Components/Link/ClipboardButton.tsx
new file mode 100644
index 000000000..09095ae74
--- /dev/null
+++ b/frontend/src/Components/Link/ClipboardButton.tsx
@@ -0,0 +1,69 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import FormInputButton from 'Components/Form/FormInputButton';
+import Icon from 'Components/Icon';
+import { icons, kinds } from 'Helpers/Props';
+import { ButtonProps } from './Button';
+import styles from './ClipboardButton.css';
+
+export interface ClipboardButtonProps extends Omit<ButtonProps, 'children'> {
+  value: string;
+}
+
+export type ClipboardState = 'success' | 'error' | null;
+
+export default function ClipboardButton({
+  id,
+  value,
+  className = styles.button,
+  ...otherProps
+}: ClipboardButtonProps) {
+  const [state, setState] = useState<ClipboardState>(null);
+
+  useEffect(() => {
+    if (!state) {
+      return;
+    }
+
+    const timeoutId = setTimeout(() => {
+      setState(null);
+    }, 3000);
+
+    return () => {
+      if (timeoutId) {
+        clearTimeout(timeoutId);
+      }
+    };
+  }, [state]);
+
+  const handleClick = useCallback(async () => {
+    try {
+      await navigator.clipboard.writeText(value);
+      setState('success');
+    } catch (_) {
+      setState('error');
+    }
+  }, [value]);
+
+  return (
+    <FormInputButton
+      className={className}
+      onClick={handleClick}
+      {...otherProps}
+    >
+      <span className={state ? styles.showStateIcon : undefined}>
+        {state ? (
+          <span className={styles.stateIconContainer}>
+            <Icon
+              name={state === 'error' ? icons.DANGER : icons.CHECK}
+              kind={state === 'error' ? kinds.DANGER : kinds.SUCCESS}
+            />
+          </span>
+        ) : null}
+
+        <span className={styles.clipboardIconContainer}>
+          <Icon name={icons.CLIPBOARD} />
+        </span>
+      </span>
+    </FormInputButton>
+  );
+}
diff --git a/frontend/src/Utilities/getUniqueElementId.js b/frontend/src/Utilities/getUniqueElementId.js
index dae5150b7..1b380851d 100644
--- a/frontend/src/Utilities/getUniqueElementId.js
+++ b/frontend/src/Utilities/getUniqueElementId.js
@@ -1,7 +1,9 @@
 let i = 0;
 
-// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022)
-
+/**
+ * @deprecated Use React's useId() instead
+ * @returns An HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022)
+ */
 export default function getUniqueElementId() {
   return `id-${i++}`;
 }
diff --git a/package.json b/package.json
index 3663a44dc..b3c4378c6 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,6 @@
     "@types/react": "18.2.79",
     "@types/react-dom": "18.2.25",
     "classnames": "2.3.2",
-    "clipboard": "2.0.11",
     "connected-react-router": "6.9.3",
     "element-class": "0.2.2",
     "filesize": "10.0.7",
diff --git a/yarn.lock b/yarn.lock
index c2cda529d..2f8aa3233 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2461,15 +2461,6 @@ clean-stack@^2.0.0:
   resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
   integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
 
-clipboard@2.0.11:
-  version "2.0.11"
-  resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5"
-  integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==
-  dependencies:
-    good-listener "^1.2.2"
-    select "^1.1.2"
-    tiny-emitter "^2.0.0"
-
 clone-deep@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
@@ -2869,11 +2860,6 @@ del@^6.1.1:
     rimraf "^3.0.2"
     slash "^3.0.0"
 
-delegate@^3.1.2:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
-  integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
-
 detect-node-es@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
@@ -3813,13 +3799,6 @@ globjoin@^0.1.4:
   resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
   integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==
 
-good-listener@^1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
-  integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==
-  dependencies:
-    delegate "^3.1.2"
-
 gopd@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@@ -6269,11 +6248,6 @@ section-iterator@^2.0.0:
   resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a"
   integrity sha512-xvTNwcbeDayXotnV32zLb3duQsP+4XosHpb/F+tu6VzEZFmIjzPdNk6/O+QOOx5XTh08KL2ufdXeCO33p380pQ==
 
-select@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
-  integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==
-
 "semver@2 || 3 || 4 || 5", semver@^5.6.0:
   version "5.7.2"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
@@ -6785,11 +6759,6 @@ time-stamp@^1.0.0:
   resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
   integrity sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==
 
-tiny-emitter@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
-  integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
-
 tiny-invariant@^1.0.2:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"