Astro
Adding a Copy button to Astro Code Blocks
Last updated •
You want to add a copy button to your Astro code blocks, but you don’t want to use a third-party library. Here’s how you can do it with a custom component.
Only MDX files support this feature of using custom components in code blocks, so this will only work in MDX files. There are other ways to do it using custom code block configs, but that’s too complicated for my taste.
Step 1: Create a React Component for the Copy Button
Create a new file src/components/react/preComponent.jsx
with the following content:
import { useRef, useState } from 'react' type Props = React.PropsWithChildren export default function PreComponent({ children }: Props) { const snippetRef = useRef<HTMLPreElement>(null) const timeoutRef = useRef<ReturnType<typeof setTimeout>>(null) const [hasBeenCopiedRecently, setHasBeenCopiedRecently] = useState<boolean>(false) const copyClicked = async () => { try { const snippet = snippetRef.current const snippetText = snippet?.innerText ?? '' await navigator.clipboard.writeText(snippetText) setHasBeenCopiedRecently(true) const currentTimeout = timeoutRef.current if (currentTimeout) { clearTimeout(currentTimeout) } timeoutRef.current = setTimeout(() => { timeoutRef.current = null setHasBeenCopiedRecently(false) }, 2000) } catch (error) { console.error('Failed to copy text:', error) } } return ( <div className='group relative'> <pre ref={snippetRef}>{children}</pre> <button className='btn-outline btn-sm absolute right-2 top-2 opacity-0 transition-opacity focus-visible:opacity-100 group-hover:opacity-100' aria-label='copy' onClick={copyClicked} data-tooltip={hasBeenCopiedRecently ? 'Copied!' : 'Copy to clipboard'} data-placement='left'> {hasBeenCopiedRecently ? <CheckIcon /> : <CopyIcon />} </button> </div> ) } function CopyIcon() { return ( <svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' strokeWidth={1.5} stroke='currentColor' className='size-5'> <path strokeLinecap='round' strokeLinejoin='round' d='M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75' /> </svg> ) } function CheckIcon() { return ( <svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' strokeWidth={1.5} stroke='currentColor' className='size-5 text-green-500'> <path strokeLinecap='round' strokeLinejoin='round' d='m4.5 12.75 6 6 9-13.5' /> </svg> ) }
Step 2: Create a Pre Component in Astro
Create a new file src/components/Pre.astro
with the following content:
--- import PreReactComponent from './react/preComponent' const props = Astro.props --- <PreReactComponent {...props} client:load><slot /></PreReactComponent>
Step 3: Use the Pre Component in blog post dyanamic route
You’ll likely have a dynamic route called src/pages/blog/[...slug].astro
. In that file, you can use the Pre
component like this:
--- import { type CollectionEntry, getCollection } from 'astro:content' import BlogPost from '../../layouts/BlogPost.astro' import PreComponent from '../../components/PreComponent.astro' export async function getStaticPaths() { const posts = await getCollection('blog') return posts.map((post) => ({ params: { slug: post.slug }, props: post, })) } type Props = CollectionEntry<'blog'> const post = Astro.props const { Content } = await post.render() --- <BlogPost {...post.data}> <Content components={{ // Use the PreComponent for code blocks pre: PreComponent, }} /> </BlogPost>