NNicheStackTutorials
← All tutorials
Astro

Build a Custom Code Snippet Component in Astro (Copy Button Included)

Shiki highlighting at build time, one-click copy, and fixing the 'Cannot use set:html with user input' scare.

·11 min read·Astro
Build a Custom Code Snippet Component in Astro (Copy Button Included) cover

Tested on: Astro 5.7, Shiki 1.29 Goal: Replace <pre> blocks with highlighted, copyable snippets. Zero runtime highlighter.

Enable Shiki in astro.config.mjs

import { defineConfig } from 'astro/config';

export default defineConfig({
  markdown: {
    shikiConfig: {
      theme: 'github-dark',
      wrap: true
    }
  }
});

MDX/Markdown code fences auto-highlight. For manual use, build a component.

CodeSnippet.astro

---
import { codeToHtml } from 'shiki';

interface Props {
  code: string;
  lang?: string;
  filename?: string;
}

const { code, lang = 'bash', filename } = Astro.props;
const html = await codeToHtml(code, { lang, theme: 'github-dark' });
const id = `snippet-${Math.random().toString(36).slice(2, 9)}`;
---

<figure class="code-snippet" data-snippet-id={id}>
  {filename && <figcaption class="filename">{filename}</figcaption>}
  <div class="code-body" set:html={html} />
  <button class="copy-btn" data-target={id}>Copy</button>
</figure>

<script>
  document.querySelectorAll('.copy-btn').forEach((btn) => {
    btn.addEventListener('click', async () => {
      const fig = btn.closest('.code-snippet');
      const code = fig?.querySelector('code')?.textContent ?? '';
      await navigator.clipboard.writeText(code);
      btn.textContent = 'Copied!';
      setTimeout(() => (btn.textContent = 'Copy'), 2000);
    });
  });
</script>

<style>
  .code-snippet { position: relative; margin: 1.5rem 0; }
  .copy-btn { position: absolute; top: 0.5rem; right: 0.5rem; }
  .filename { font-family: monospace; font-size: 0.85rem; padding: 0.25rem 0.75rem; background: #1e1e1e; color: #aaa; }
</style>

Usage in any page

---
import CodeSnippet from '../components/CodeSnippet.astro';

const installCmd = 'npm install astro';
---

<CodeSnippet code={installCmd} lang="bash" filename="terminal" />

Build error — top-level await in component

Top-level await is not available in client components

Shiki runs in frontmatter (server/build time). The <script> tag is vanilla JS — no Shiki import there. Keep them separate.

Security note

Only pass trusted code strings (your tutorials). Never pass user input to set:html.

CSS clash with global styles

pre { background: white !important; } /* killed Shiki theme */

Scope styles:

.code-snippet :global(pre) { background: transparent !important; }

Output

Highlighting: build time (0ms client cost)
Copy script: ~400 bytes inlined per page with snippets
No highlight.js in browser

Tags: AstroShikiCode BlocksComponents