Code Block

Provides syntax highlighting, line numbers, and copy to clipboard functionality for code blocks.

The CodeBlock component provides syntax highlighting, line numbers, and copy to clipboard functionality for code blocks.

Install using CLI

AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add code-block

Install Manually

Copy and paste the following files into the same folder.

CodeBlock.vue
CodeBlockCopyButton.vue
context.ts
utils.ts
index.ts
<script setup lang="ts">
import type { BundledLanguage } from 'shiki'
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { reactiveOmit } from '@vueuse/core'
import { computed, onBeforeUnmount, provide, ref, watch } from 'vue'
import { CodeBlockKey } from './context'
import { highlightCode } from './utils'

const props = withDefaults(
  defineProps<{
    code: string
    language: BundledLanguage
    showLineNumbers?: boolean
    class?: HTMLAttributes['class']
  }>(),
  {
    showLineNumbers: false,
  },
)

const delegatedProps = reactiveOmit(props, 'code', 'language', 'showLineNumbers', 'class')

const html = ref('')
const darkHtml = ref('')

provide(CodeBlockKey, {
  code: computed(() => props.code),
})

let requestId = 0
let isUnmounted = false

watch(
  () => [props.code, props.language, props.showLineNumbers] as const,
  async ([code, language, showLineNumbers]) => {
    requestId += 1
    const currentId = requestId

    try {
      const [light, dark] = await highlightCode(code, language, showLineNumbers)

      if (currentId === requestId && !isUnmounted) {
        html.value = light
        darkHtml.value = dark
      }
    }
    catch (error) {
      console.error('[CodeBlock] highlight failed', error)
    }
  },
  { immediate: true },
)

onBeforeUnmount(() => {
  isUnmounted = true
})
</script>

<template>
  <div
    data-slot="code-block"
    v-bind="delegatedProps"
    :class="cn('group relative w-full overflow-hidden rounded-md border bg-background text-foreground', props.class)"
  >
    <div class="relative">
      <div
        class="overflow-hidden dark:hidden [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
        v-html="html"
      />
      <div
        class="hidden overflow-hidden dark:block [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
        v-html="darkHtml"
      />
      <div v-if="$slots.default" class="absolute top-2 right-2 flex items-center gap-2">
        <slot />
      </div>
    </div>
  </div>
</template>

Usage with AI SDK

Build a simple code generation tool using the experimental_useObject hook.

Add the following component to your frontend:

app/page.vue
<script setup lang="ts">
import { experimental_useObject as useObject } from '@ai-sdk/vue'
import { ref } from 'vue'
import { z } from 'zod'
import { CodeBlock, CodeBlockCopyButton } from '@/components/ai-elements/code-block'
import { PromptInput, PromptInputSubmit, PromptInputTextarea } from '@/components/ai-elements/prompt-input'

const codeBlockSchema = z.object({
  language: z.string(),
  filename: z.string(),
  code: z.string(),
})

const input = ref('')
const { object, submit, isLoading } = useObject({
  api: '/api/codegen',
  schema: codeBlockSchema,
})

function handleSubmit(e: Event) {
  e.preventDefault()
  if (input.value.trim()) {
    submit(input.value)
  }
}
</script>

<template>
  <div class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
    <div class="flex flex-col h-full">
      <div class="flex-1 overflow-auto mb-4">
        <CodeBlock
          v-if="object?.code && object?.language"
          :code="object.code"
          :language="object.language"
          :show-line-numbers="true"
        >
          <CodeBlockCopyButton />
        </CodeBlock>
      </div>

      <PromptInput
        class="mt-4 w-full max-w-2xl mx-auto relative"
        @submit="handleSubmit"
      >
        <PromptInputTextarea
          v-model="input"
          placeholder="Generate a Vue todolist component"
          class="pr-12"
        />
        <PromptInputSubmit
          :status="isLoading ? 'streaming' : 'ready'"
          :disabled="!input.trim()"
          class="absolute bottom-1 right-1"
        />
      </PromptInput>
    </div>
  </div>
</template>

Add the following route to your backend:

server/api/codegen.post.ts
import { openai } from '@ai-sdk/openai'
import { streamObject } from 'ai'
import { z } from 'zod'

const codeBlockSchema = z.object({
  language: z.string(),
  filename: z.string(),
  code: z.string(),
})

export default defineEventHandler(async (event) => {
  const { prompt } = await readBody(event)

  const result = streamObject({
    model: openai('gpt-4o'),
    schema: codeBlockSchema,
    prompt:
      `You are a helpful coding assitant. Only generate code, no markdown formatting or backticks, or text.${
        prompt}`,
  })

  return result.toTextStreamResponse()
})

Features

  • Syntax highlighting with Shiki
  • Line numbers (optional)
  • Copy to clipboard functionality
  • Automatic light/dark theme switching
  • Customizable styles
  • Accessible design

Examples

Dark Mode

To use the CodeBlock component in dark mode, you can wrap it in a div with the dark class.

Props

<CodeBlock />

coderequiredstring
The code content to display.
languagerequiredBundledLanguage
The programming language for syntax highlighting.
showLineNumbersboolean
Whether to show line numbers.
classstring
Additional CSS classes to apply to the root container.

<CodeBlockCopyButton />

timeoutnumber
How long to show the copied state (ms).
classstring
Additional CSS classes to apply to the button.
@copy() => void
Callback fired after a successful copy.
@error(error: Error) => void
Callback fired if copying fails.