Repository: scaffold-eth/scaffold-eth-2
Pull request #1084: Fix Address component copying address to clipboard 
Author: login: damianmarti
Created: 2025-04-16T17:08:25Z | Merged: 2025-04-24T06:39:55Z

When the Address component is inside a dialog (like the one created when using a DaisyUI modal), the copy to the clipboard for the address is not working.

You can try it at https://clipboard-broken.vercel.app/ , open the modal, and try to copy the address. The address will not be copied to the clipboard.

This PR does not use react-copy-to-clipboard library to copy to the clipboard. It uses just navigator.clipboard.writeText

https://clipboard-test-sable.vercel.app/

In the previous link, you can test the Address component with the new copy to clipboard implementation. Open the modal and try to copy the address. The address should be copied to the clipboard now. No permission should be asked by the browser.

There is another button "**read from clipboard**" added just for testing the use of navigator.clipboard.readText. This will ask the user for permission to read the clipboard content.

This was tested on Chrome, Firefox, and Edge, on Ubuntu and Windows.

If this works, I think the react-copy-to-clipboard library is not needed anymore since the major advantage was to give browser compatibility, but modern browsers should work using navigator.clipboard.writeText (https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText#browser_compatibility).

We can remove this library dependency if we change 3 components to use navigator.clipboard.writeText: **TransactionHash**, **TxReceipt**, and **AddressInfoDropdown**. 


Diff:
diff --git a/packages/nextjs/app/blockexplorer/_components/TransactionHash.tsx b/packages/nextjs/app/blockexplorer/_components/TransactionHash.tsx
index 2a6ae76f50..6c537fd154 100644
--- a/packages/nextjs/app/blockexplorer/_components/TransactionHash.tsx
+++ b/packages/nextjs/app/blockexplorer/_components/TransactionHash.tsx
@@ -1,35 +1,27 @@
-"use client";
-
-import { useState } from "react";
 import Link from "next/link";
-import CopyToClipboard from "react-copy-to-clipboard";
 import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
+import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
 
 export const TransactionHash = ({ hash }: { hash: string }) => {
-  const [addressCopied, setAddressCopied] = useState(false);
+  const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
+    useCopyToClipboard();
 
   return (
     <div className="flex items-center">
       <Link href={`/blockexplorer/transaction/${hash}`}>
         {hash?.substring(0, 6)}...{hash?.substring(hash.length - 4)}
       </Link>
-      {addressCopied ? (
+      {isAddressCopiedToClipboard ? (
         <CheckCircleIcon
           className="ml-1.5 text-xl font-normal text-base-content h-5 w-5 cursor-pointer"
           aria-hidden="true"
         />
       ) : (
-        <CopyToClipboard
-          text={hash as string}
-          onCopy={() => {
-            setAddressCopied(true);
-            setTimeout(() => {
-              setAddressCopied(false);
-            }, 800);
-          }}
-        >
-          <DocumentDuplicateIcon className="ml-1.5 text-xl font-normal h-5 w-5 cursor-pointer" aria-hidden="true" />
-        </CopyToClipboard>
+        <DocumentDuplicateIcon
+          className="ml-1.5 text-xl font-normal h-5 w-5 cursor-pointer"
+          aria-hidden="true"
+          onClick={() => copyAddressToClipboard(hash)}
+        />
       )}
     </div>
   );
diff --git a/packages/nextjs/app/debug/_components/contract/TxReceipt.tsx b/packages/nextjs/app/debug/_components/contract/TxReceipt.tsx
index e39fb018d8..c5767d3b6b 100644
--- a/packages/nextjs/app/debug/_components/contract/TxReceipt.tsx
+++ b/packages/nextjs/app/debug/_components/contract/TxReceipt.tsx
@@ -1,33 +1,27 @@
-import { useState } from "react";
-import CopyToClipboard from "react-copy-to-clipboard";
 import { TransactionReceipt } from "viem";
 import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
 import { ObjectFieldDisplay } from "~~/app/debug/_components/contract";
+import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
 import { replacer } from "~~/utils/scaffold-eth/common";
 
 export const TxReceipt = ({ txResult }: { txResult: TransactionReceipt }) => {
-  const [txResultCopied, setTxResultCopied] = useState(false);
+  const { copyToClipboard: copyTxResultToClipboard, isCopiedToClipboard: isTxResultCopiedToClipboard } =
+    useCopyToClipboard();
 
   return (
     <div className="flex text-sm rounded-3xl peer-checked:rounded-b-none min-h-0 bg-secondary py-0">
       <div className="mt-1 pl-2">
-        {txResultCopied ? (
+        {isTxResultCopiedToClipboard ? (
           <CheckCircleIcon
             className="ml-1.5 text-xl font-normal text-base-content h-5 w-5 cursor-pointer"
             aria-hidden="true"
           />
         ) : (
-          <CopyToClipboard
-            text={JSON.stringify(txResult, replacer, 2)}
-            onCopy={() => {
-              setTxResultCopied(true);
-              setTimeout(() => {
-                setTxResultCopied(false);
-              }, 800);
-            }}
-          >
-            <DocumentDuplicateIcon className="ml-1.5 text-xl font-normal h-5 w-5 cursor-pointer" aria-hidden="true" />
-          </CopyToClipboard>
+          <DocumentDuplicateIcon
+            className="ml-1.5 text-xl font-normal h-5 w-5 cursor-pointer"
+            aria-hidden="true"
+            onClick={() => copyTxResultToClipboard(JSON.stringify(txResult, replacer, 2))}
+          />
         )}
       </div>
       <div tabIndex={0} className="flex-wrap collapse collapse-arrow">
diff --git a/packages/nextjs/components/scaffold-eth/Address/AddressCopyIcon.tsx b/packages/nextjs/components/scaffold-eth/Address/AddressCopyIcon.tsx
index 2632e642f4..f265fd2e3f 100644
--- a/packages/nextjs/components/scaffold-eth/Address/AddressCopyIcon.tsx
+++ b/packages/nextjs/components/scaffold-eth/Address/AddressCopyIcon.tsx
@@ -1,26 +1,23 @@
-import { useState } from "react";
-import CopyToClipboard from "react-copy-to-clipboard";
 import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
+import { useCopyToClipboard } from "~~/hooks/scaffold-eth/useCopyToClipboard";
 
 export const AddressCopyIcon = ({ className, address }: { className?: string; address: string }) => {
-  const [addressCopied, setAddressCopied] = useState(false);
+  const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
+    useCopyToClipboard();
+
   return (
-    <CopyToClipboard
-      text={address}
-      onCopy={() => {
-        setAddressCopied(true);
-        setTimeout(() => {
-          setAddressCopied(false);
-        }, 800);
+    <button
+      onClick={e => {
+        e.stopPropagation();
+        copyAddressToClipboard(address);
       }}
+      type="button"
     >
-      <button onClick={e => e.stopPropagation()} type="button">
-        {addressCopied ? (
-          <CheckCircleIcon className={className} aria-hidden="true" />
-        ) : (
-          <DocumentDuplicateIcon className={className} aria-hidden="true" />
-        )}
-      </button>
-    </CopyToClipboard>
+      {isAddressCopiedToClipboard ? (
+        <CheckCircleIcon className={className} aria-hidden="true" />
+      ) : (
+        <DocumentDuplicateIcon className={className} aria-hidden="true" />
+      )}
+    </button>
   );
 };
diff --git a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx
index 444b11b5e8..a227c892b9 100644
--- a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx
+++ b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx
@@ -1,6 +1,5 @@
 import { useRef, useState } from "react";
 import { NetworkOptions } from "./NetworkOptions";
-import CopyToClipboard from "react-copy-to-clipboard";
 import { getAddress } from "viem";
 import { Address } from "viem";
 import { useDisconnect } from "wagmi";
@@ -14,7 +13,7 @@ import {
   QrCodeIcon,
 } from "@heroicons/react/24/outline";
 import { BlockieAvatar, isENS } from "~~/components/scaffold-eth";
-import { useOutsideClick } from "~~/hooks/scaffold-eth";
+import { useCopyToClipboard, useOutsideClick } from "~~/hooks/scaffold-eth";
 import { getTargetNetworks } from "~~/utils/scaffold-eth";
 
 const allowedNetworks = getTargetNetworks();
@@ -35,14 +34,16 @@ export const AddressInfoDropdown = ({
   const { disconnect } = useDisconnect();
   const checkSumAddress = getAddress(address);
 
-  const [addressCopied, setAddressCopied] = useState(false);
-
+  const { copyToClipboard: copyAddressToClipboard, isCopiedToClipboard: isAddressCopiedToClipboard } =
+    useCopyToClipboard();
   const [selectingNetwork, setSelectingNetwork] = useState(false);
   const dropdownRef = useRef<HTMLDetailsElement>(null);
+
   const closeDropdown = () => {
     setSelectingNetwork(false);
     dropdownRef.current?.removeAttribute("open");
   };
+
   useOutsideClick(dropdownRef, closeDropdown);
 
   return (
@@ -58,33 +59,22 @@ export const AddressInfoDropdown = ({
         <ul className="dropdown-content menu z-2 p-2 mt-2 shadow-center shadow-accent bg-base-200 rounded-box gap-1">
           <NetworkOptions hidden={!selectingNetwork} />
           <li className={selectingNetwork ? "hidden" : ""}>
-            {addressCopied ? (
-              <div className="h-8 btn-sm rounded-xl! flex gap-3 py-3">
-                <CheckCircleIcon
-                  className="text-xl font-normal h-6 w-4 cursor-pointer ml-2 sm:ml-0"
-                  aria-hidden="true"
-                />
-                <span className=" whitespace-nowrap">Copy address</span>
-              </div>
-            ) : (
-              <CopyToClipboard
-                text={checkSumAddress}
-                onCopy={() => {
-                  setAddressCopied(true);
-                  setTimeout(() => {
-                    setAddressCopied(false);
-                  }, 800);
-                }}
-              >
-                <div className="h-8 btn-sm rounded-xl! flex gap-3 py-3">
-                  <DocumentDuplicateIcon
-                    className="text-xl font-normal h-6 w-4 cursor-pointer ml-2 sm:ml-0"
-                    aria-hidden="true"
-                  />
-                  <span className=" whitespace-nowrap">Copy address</span>
-                </div>
-              </CopyToClipboard>
-            )}
+            <div
+              className="h-8 btn-sm rounded-xl! flex gap-3 py-3 cursor-pointer"
+              onClick={() => copyAddressToClipboard(checkSumAddress)}
+            >
+              {isAddressCopiedToClipboard ? (
+                <>
+                  <CheckCircleIcon className="text-xl font-normal h-6 w-4 ml-2 sm:ml-0" aria-hidden="true" />
+                  <span className="whitespace-nowrap">Copied!</span>
+                </>
+              ) : (
+                <>
+                  <DocumentDuplicateIcon className="text-xl font-normal h-6 w-4 ml-2 sm:ml-0" aria-hidden="true" />
+                  <span className="whitespace-nowrap">Copy address</span>
+                </>
+              )}
+            </div>
           </li>
           <li className={selectingNetwork ? "hidden" : ""}>
             <label htmlFor="qrcode-modal" className="h-8 btn-sm rounded-xl! flex gap-3 py-3">
diff --git a/packages/nextjs/hooks/scaffold-eth/index.ts b/packages/nextjs/hooks/scaffold-eth/index.ts
index 05450b2600..bde39de447 100644
--- a/packages/nextjs/hooks/scaffold-eth/index.ts
+++ b/packages/nextjs/hooks/scaffold-eth/index.ts
@@ -1,5 +1,6 @@
 export * from "./useAnimationConfig";
 export * from "./useContractLogs";
+export * from "./useCopyToClipboard";
 export * from "./useDeployedContractInfo";
 export * from "./useFetchBlocks";
 export * from "./useInitializeNativeCurrencyPrice";
diff --git a/packages/nextjs/hooks/scaffold-eth/useCopyToClipboard.ts b/packages/nextjs/hooks/scaffold-eth/useCopyToClipboard.ts
new file mode 100644
index 0000000000..8f2861efd0
--- /dev/null
+++ b/packages/nextjs/hooks/scaffold-eth/useCopyToClipboard.ts
@@ -0,0 +1,19 @@
+import { useState } from "react";
+
+export const useCopyToClipboard = () => {
+  const [isCopiedToClipboard, setIsCopiedToClipboard] = useState(false);
+
+  const copyToClipboard = async (text: string) => {
+    try {
+      await navigator.clipboard.writeText(text);
+      setIsCopiedToClipboard(true);
+      setTimeout(() => {
+        setIsCopiedToClipboard(false);
+      }, 800);
+    } catch (err) {
+      console.error("Failed to copy text:", err);
+    }
+  };
+
+  return { copyToClipboard, isCopiedToClipboard };
+};
diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json
index 7492ff12aa..320ad8acee 100644
--- a/packages/nextjs/package.json
+++ b/packages/nextjs/package.json
@@ -30,7 +30,6 @@
     "next-themes": "^0.3.0",
     "qrcode.react": "^4.0.1",
     "react": "^19.0.0",
-    "react-copy-to-clipboard": "^5.1.0",
     "react-dom": "^19.0.0",
     "react-hot-toast": "^2.4.0",
     "usehooks-ts": "^3.1.0",
diff --git a/packages/nextjs/types/react-copy-to-clipboard/react-copy-to-clipboard.d.ts b/packages/nextjs/types/react-copy-to-clipboard/react-copy-to-clipboard.d.ts
deleted file mode 100644
index 9e5439ce19..0000000000
--- a/packages/nextjs/types/react-copy-to-clipboard/react-copy-to-clipboard.d.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/25414#issuecomment-1180518151
-declare module "react-copy-to-clipboard" {
-  import React from "react";
-
-  interface Options {
-    debug: boolean;
-    message: string;
-  }
-
-  interface Props {
-    text: string;
-    onCopy?(a: string, b: boolean): void;
-    options?: Options;
-  }
-
-  class CopyToClipboard extends React.Component<PropsWithChildren<Props>, unknown> {}
-  export default CopyToClipboard;
-}
diff --git a/yarn.lock b/yarn.lock
index 6b78cdf20c..8c024b545e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4030,7 +4030,6 @@ __metadata:
     prettier: ^3.5.3
     qrcode.react: ^4.0.1
     react: ^19.0.0
-    react-copy-to-clipboard: ^5.1.0
     react-dom: ^19.0.0
     react-hot-toast: ^2.4.0
     tailwindcss: ^4.1.3
@@ -8203,15 +8202,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"copy-to-clipboard@npm:^3.3.1":
-  version: 3.3.3
-  resolution: "copy-to-clipboard@npm:3.3.3"
-  dependencies:
-    toggle-selection: ^1.0.6
-  checksum: e0a325e39b7615108e6c1c8ac110ae7b829cdc4ee3278b1df6a0e4228c490442cc86444cd643e2da344fbc424b3aab8909e2fec82f8bc75e7e5b190b7c24eecf
-  languageName: node
-  linkType: hard
-
 "core-util-is@npm:~1.0.0":
   version: 1.0.3
   resolution: "core-util-is@npm:1.0.3"
@@ -15955,18 +15945,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-copy-to-clipboard@npm:^5.1.0":
-  version: 5.1.0
-  resolution: "react-copy-to-clipboard@npm:5.1.0"
-  dependencies:
-    copy-to-clipboard: ^3.3.1
-    prop-types: ^15.8.1
-  peerDependencies:
-    react: ^15.3.0 || 16 || 17 || 18
-  checksum: f00a4551b9b63c944a041a6ab46af5ef20ba1106b3bc25173e7ef9bffbfba17a613368682ab8820cfe8d4b3acc5335cd9ce20229145bcc1e6aa8d1db04c512e5
-  languageName: node
-  linkType: hard
-
 "react-dom@npm:^19.0.0":
   version: 19.0.0
   resolution: "react-dom@npm:19.0.0"
@@ -17951,13 +17929,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"toggle-selection@npm:^1.0.6":
-  version: 1.0.6
-  resolution: "toggle-selection@npm:1.0.6"
-  checksum: a90dc80ed1e7b18db8f4e16e86a5574f87632dc729cfc07d9ea3ced50021ad42bb4e08f22c0913e0b98e3837b0b717e0a51613c65f30418e21eb99da6556a74c
-  languageName: node
-  linkType: hard
-
 "toidentifier@npm:1.0.0":
   version: 1.0.0
   resolution: "toidentifier@npm:1.0.0"


Reviews:
user: login: technophile-04
body: Thanks @damianmarti for this and all the research! Yup after research for a while, I think it safe to remove react-copy-clipboard and use the `navigator.clipboard.writeText` I checked the code of some famous website's and docs too they also are using `navigator.clipboard.writeText` only. 

Also lol I remember having `types` issue with `react-copy-clipboard` because of the way it has it's internal dependencies with `*` so it was also causing problems when installed via `npx create-eth@latest` few months ago, so lets remove it and use `navigator.clipboard.writeText`, Thanksss 🙌
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T07:45:01Z
user: login: technophile-04
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-23T05:48:51Z
user: login: damianmarti
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T20:03:01Z
user: login: technophile-04
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-24T06:37:47Z

Review comments:
diff_hunk: @@ -2,12 +2,20 @@
 
 import { useState } from "react";
 import Link from "next/link";
-import CopyToClipboard from "react-copy-to-clipboard";
 import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
 
 export const TransactionHash = ({ hash }: { hash: string }) => {
   const [addressCopied, setAddressCopied] = useState(false);
 
+  const handleCopy = () => {
+    navigator.clipboard.writeText(hash).then(() => {
+      setAddressCopied(true);
+      setTimeout(() => {
+        setAddressCopied(false);
+      }, 800);
+    });
+  };
path: packages/nextjs/app/blockexplorer/_components/TransactionHash.tsx
user: login: technophile-04
body: It would be nice to use async / await instead of .then
created_at: 2025-04-23T05:48:51Z
author_association: COLLABORATOR
diff_hunk: @@ -2,12 +2,20 @@
 
 import { useState } from "react";
 import Link from "next/link";
-import CopyToClipboard from "react-copy-to-clipboard";
 import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline";
 
 export const TransactionHash = ({ hash }: { hash: string }) => {
   const [addressCopied, setAddressCopied] = useState(false);
 
+  const handleCopy = () => {
+    navigator.clipboard.writeText(hash).then(() => {
+      setAddressCopied(true);
+      setTimeout(() => {
+        setAddressCopied(false);
+      }, 800);
+    });
+  };
path: packages/nextjs/app/blockexplorer/_components/TransactionHash.tsx
user: login: damianmarti
body: Implemented in new hook.
created_at: 2025-04-23T20:03:01Z
author_association: MEMBER

PR comments:
user: login: rin-st
created_at: 2025-04-17T10:56:23Z
body: Working great, thank you @damianmarti !

> We can remove this library dependency if we change 3 components to use navigator.clipboard.writeText: TransactionHash, TxReceipt, and AddressInfoDropdown.

It would be great, could you change them too?

> Also lol I remember having types issue with react-copy-clipboard because of the way it has it's internal dependencies with * so it was also causing problems when installed via npx create-eth@latest few months ago, so lets remove it and use navigator.clipboard.writeText, Thanksss 🙌

Yes, and we added type definitions to `/types/react-copy-to-clipboard`. Remove that folder too, please


author_association: MEMBER
user: login: damianmarti
created_at: 2025-04-21T17:47:58Z
body: Thanks @technophile-04 and @rin-st for the review!

All the components are using the Clipboard API to copy content now, and the react-copy-to-clipboard library was removed!
author_association: MEMBER
user: login: rin-st
created_at: 2025-04-22T14:18:50Z
body: Thanks Damu! Tested everything and it's working great!

One more question: is it possible to move logic to `useCopyToClipboard` hook?

So we can use it like

```
const {copyToClipboard, isCopiedToClipboard} = useCopyToClipboard();

...
return <>
  <button onClick={() => copyToClipboard(text)} /> 
  {isCopiedToClipboard ? <CheckIcon /> : <CopyIcon />}
</>
```
author_association: MEMBER
user: login: damianmarti
created_at: 2025-04-22T23:15:26Z
body: > Thanks Damu! Tested everything and it's working great!
> 
> One more question: is it possible to move logic to `useCopyToClipboard` hook?
> 
> So we can use it like
> 
> ```
> const {copyToClipboard, isCopiedToClipboard} = useCopyToClipboard();
> 
> ...
> return <>
>   <button onClick={() => copyToClipboard(text)} /> 
>   {isCopiedToClipboard ? <CheckIcon /> : <CopyIcon />}
> </>
> ```

@rin-st thanks for testing!

Great idea! I will implement this!
author_association: MEMBER
user: login: technophile-04
created_at: 2025-04-23T05:50:07Z
body: Thanks @damianmarti!! Along with @rin-st suggestions on using hook can we also use asyn / await with try / catch and just log the error. Thanks 🙌
author_association: COLLABORATOR
user: login: damianmarti
created_at: 2025-04-23T20:03:52Z
body: @rin-st @technophile-04 Thanks for the review and testing! 

Implemented the new userCopyToClipboard hook!
author_association: MEMBER
user: login: technophile-04
created_at: 2025-04-24T06:37:39Z
body: Tysm @damianmarti!! Just pushed some nitpick on renmaings similar to how they were named earlier in particular components 

Merging this 🙌
author_association: COLLABORATOR