diff --git a/components/Common/ShellBox/__tests__/__snapshots__/index.test.tsx.snap b/components/Common/ShellBox/__tests__/__snapshots__/index.test.tsx.snap
new file mode 100644
index 0000000000000..0a18a22852632
--- /dev/null
+++ b/components/Common/ShellBox/__tests__/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ShellBox should render 1`] = `
+
+
+
+
+ SHELL
+
+
+
+
+ test
+
+
+
+`;
diff --git a/components/Common/ShellBox/__tests__/index.test.tsx b/components/Common/ShellBox/__tests__/index.test.tsx
new file mode 100644
index 0000000000000..9d764926a5d86
--- /dev/null
+++ b/components/Common/ShellBox/__tests__/index.test.tsx
@@ -0,0 +1,58 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { IntlProvider } from 'react-intl';
+import ShellBox from '../index';
+
+const mockWriteText = jest.fn();
+const originalNavigator = { ...window.navigator };
+
+describe('ShellBox', () => {
+ beforeEach(() => {
+ Object.defineProperty(window, 'navigator', {
+ value: {
+ clipboard: {
+ writeText: mockWriteText,
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ Object.defineProperty(window, 'navigator', {
+ value: originalNavigator,
+ });
+ });
+
+ it('should render', () => {
+ const { container } = render(
+ {}}>
+ test
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should call clipboard API with `test` once', async () => {
+ const user = userEvent.setup();
+ const navigatorClipboardWriteTextSpy = jest
+ .fn()
+ .mockImplementation(() => Promise.resolve());
+
+ Object.defineProperty(window.navigator, 'clipboard', {
+ writable: true,
+ value: {
+ writeText: navigatorClipboardWriteTextSpy,
+ },
+ });
+
+ render(
+ {}}>
+ test
+
+ );
+ const button = screen.getByRole('button');
+ await user.click(button);
+ expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledTimes(1);
+ expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledWith('test');
+ });
+});
diff --git a/components/Common/ShellBox/index.module.scss b/components/Common/ShellBox/index.module.scss
new file mode 100644
index 0000000000000..f69a169efabc8
--- /dev/null
+++ b/components/Common/ShellBox/index.module.scss
@@ -0,0 +1,75 @@
+.shellBox {
+ background-color: var(--black10);
+ border-radius: 0.4rem;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ font-family: var(--mono);
+ padding: 0 0 var(--space-48) var(--space-16);
+ position: relative;
+
+ code {
+ color: var(--pink5);
+ font-family: inherit;
+ line-height: 30px;
+ overflow-x: hidden;
+ position: absolute;
+ top: 30px;
+ width: calc(100% - 20px);
+
+ &:hover {
+ overflow-x: auto;
+ }
+
+ &::-webkit-scrollbar {
+ height: 0.5em;
+ }
+
+ &::-webkit-scrollbar,
+ &::-webkit-scrollbar-thumb {
+ border-radius: 4px;
+ overflow: visible;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: rgba(0, 0, 0, 0.2);
+ }
+
+ > span.function {
+ color: var(--warning5);
+ }
+ }
+
+ .top {
+ display: inherit;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-bottom: var(--space-08);
+
+ span,
+ button {
+ align-items: center;
+ display: inherit;
+ font-size: var(--font-size-code);
+ height: 23px;
+ justify-content: center;
+ width: 86px;
+ }
+
+ span {
+ background-color: var(--black3);
+ border-radius: 0 0 0.3rem 0.3rem;
+ color: var(--black9);
+ margin-left: 1.6rem;
+ }
+
+ button {
+ background-color: var(--brand);
+ border-radius: 0 0.3rem 0.3rem 0.3rem;
+ border-width: 0;
+ i {
+ padding: 0;
+ }
+ }
+ }
+}
diff --git a/components/Common/ShellBox/index.stories.tsx b/components/Common/ShellBox/index.stories.tsx
new file mode 100644
index 0000000000000..4e5fa06699427
--- /dev/null
+++ b/components/Common/ShellBox/index.stories.tsx
@@ -0,0 +1,31 @@
+import ShellBox from './index';
+import type { Meta as MetaObj, StoryObj } from '@storybook/react';
+
+type Story = StoryObj;
+type Meta = MetaObj;
+
+export const Default: Story = {
+ args: {
+ children: 'echo hello world',
+ textToCopy: 'echo hello world',
+ },
+};
+
+export const WithoutTextToCopy: Story = {
+ args: {
+ children: 'echo hello world',
+ },
+};
+
+export const WithTextToCopyJsx: Story = {
+ args: {
+ children: (
+
+ $echo hello world
+
+ ),
+ textToCopy: 'echo hello world',
+ },
+};
+
+export default { component: ShellBox } as Meta;
diff --git a/components/Common/ShellBox/index.tsx b/components/Common/ShellBox/index.tsx
new file mode 100644
index 0000000000000..273828e738ba1
--- /dev/null
+++ b/components/Common/ShellBox/index.tsx
@@ -0,0 +1,45 @@
+import { useRef } from 'react';
+import { FormattedMessage } from 'react-intl';
+import styles from './index.module.scss';
+import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
+import type { FC, PropsWithChildren, MouseEvent, ReactNode } from 'react';
+
+type ShellBoxProps = {
+ children: string | ReactNode;
+ textToCopy?: string;
+};
+
+const ShellBox: FC> = ({
+ children,
+ textToCopy,
+}: PropsWithChildren) => {
+ const [copied, copyText] = useCopyToClipboard();
+
+ const shellBoxRef = useRef(null);
+
+ const handleCopyCode = async (event: MouseEvent) => {
+ event.preventDefault();
+ // By default we want to use the textToCopy props but if it's absent
+ // we allow the user to copy by getting the inner HTML content of the Element
+ const _textToCopy = textToCopy || shellBoxRef.current?.innerHTML || '';
+
+ await copyText(_textToCopy.replace('$', ''));
+ };
+
+ return (
+
+
+ SHELL
+
+
+ {children}
+
+ );
+};
+
+export default ShellBox;
diff --git a/components/Common/index.ts b/components/Common/index.ts
new file mode 100644
index 0000000000000..d49eb3b30e135
--- /dev/null
+++ b/components/Common/index.ts
@@ -0,0 +1 @@
+export { default as ShellBox } from './ShellBox';
diff --git a/hooks/__test__/useCopyToClipboard.test.tsx b/hooks/__test__/useCopyToClipboard.test.tsx
new file mode 100644
index 0000000000000..76756f9a3522d
--- /dev/null
+++ b/hooks/__test__/useCopyToClipboard.test.tsx
@@ -0,0 +1,59 @@
+import { render, fireEvent, screen } from '@testing-library/react';
+import { FormattedMessage } from 'react-intl';
+import { IntlProvider } from 'react-intl';
+import { useCopyToClipboard } from '../useCopyToClipboard';
+
+const mockWriteText = jest.fn();
+const originalNavigator = { ...window.navigator };
+
+describe('useCopyToClipboard', () => {
+ beforeEach(() => {
+ Object.defineProperty(window, 'navigator', {
+ value: {
+ clipboard: {
+ writeText: mockWriteText,
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ Object.defineProperty(window, 'navigator', {
+ value: originalNavigator,
+ });
+ });
+
+ const TestComponent = ({ textToCopy }: { textToCopy: string }) => {
+ const [copied, copyText] = useCopyToClipboard();
+
+ return (
+ {}}>
+
+
+ );
+ };
+
+ it('should call clipboard API with `test` once', () => {
+ const navigatorClipboardWriteTextSpy = jest
+ .fn()
+ .mockImplementation(() => Promise.resolve());
+
+ Object.defineProperty(window.navigator, 'clipboard', {
+ writable: true,
+ value: {
+ writeText: navigatorClipboardWriteTextSpy,
+ },
+ });
+
+ render();
+ const button = screen.getByRole('button');
+ fireEvent.click(button);
+ expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledTimes(1);
+ expect(navigatorClipboardWriteTextSpy).toHaveBeenCalledWith('test');
+ });
+});
diff --git a/hooks/useCopyToClipboard.ts b/hooks/useCopyToClipboard.ts
new file mode 100644
index 0000000000000..bd7b3a4cdeb33
--- /dev/null
+++ b/hooks/useCopyToClipboard.ts
@@ -0,0 +1,31 @@
+import { useEffect, useState } from 'react';
+
+const copyToClipboard = (value: string | undefined) => {
+ if (!value || typeof navigator === 'undefined') {
+ return Promise.resolve(false);
+ }
+
+ return navigator.clipboard
+ .writeText(value)
+ .then(() => true)
+ .catch(() => false);
+};
+
+export const useCopyToClipboard = () => {
+ const [copied, setCopied] = useState(false);
+
+ const copyText = (text: string | undefined) =>
+ copyToClipboard(text).then(setCopied);
+
+ useEffect(() => {
+ if (copied) {
+ const timerId = setTimeout(() => setCopied(false), 3000);
+
+ return () => clearTimeout(timerId);
+ }
+
+ return undefined;
+ }, [copied]);
+
+ return [copied, copyText] as const;
+};
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 9ea9b0148717c..9f0bbb6bd6f7e 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -57,5 +57,6 @@
"pages.index.features.openSource.title": "Open Source",
"pages.index.features.openSource.description": "Node.js is open source and actively maintained by contributors all over the world",
"pages.index.features.everywhere.title": "Everywhere",
- "pages.index.features.everywhere.description": "Node.js has been adapted to work in a wide variety of places"
+ "pages.index.features.everywhere.description": "Node.js has been adapted to work in a wide variety of places",
+ "components.common.shellBox.copy": "{copied, select, true {copied}other {copy}}"
}