Code Block Component

Aug 20, 2023

Dependencies

Built with react, shikiji, and tailwind.

Component

import { codeToHtml } from 'shikiji'
import { useState, useEffect } from 'react'
import { cn } from '@/shared/utils'
import styles from './code.module.css'

const preClass = 'rounded border-slate-500 border text-sm p-4 overflow-scroll !bg-[#fafafa]'

export function Code({ className, content, language = 'javascript' }: { className: string, content: string, language: Language }) {
  const trimmed = content.trim()
  const colored = useShikiji(trimmed, language)
  const [copied, write] = useClipboard(trimmed)

  return (
    <form className="relative" onSubmit={(e) => {
      e.preventDefault()
      write()
    }}>
      <button type="submit" className="rounded-sm top-2 right-2 text-slate-700 bg-[#fafafa] border-slate-500 border absolute p-1 px-2 text-xs font-semibold uppercase hover:text-white hover:bg-slate-700 transition">
        {copied ? 'Copied!' : 'Copy'}
      </button>
      {colored ? (
        <div className={className} dangerouslySetInnerHTML={{ __html: colored }} />
      ) : (
        <div className={className}>
          <pre className={cn(
            preClass,
            "select-none pl-12 text-transparent"
          )}>
            <code>{trimmed}</code>
          </pre>
        </div>
      )}
    </form>
  )
}

Hooks

function useShikiji(content: string, language: Language) {
  const [colored, setColored] = useState('')

  useEffect(() => {
    const func = async () => {
      const html = await codeToHtml(content, {
        lang: language,
        theme: 'github-light',
        transforms: {
          pre(node) {
            node.properties.class = preClass
          },
          code(node) {
            node.properties.class = `${styles['code']} language-${language}`
          },
          line(node, _line) {
            node.properties.class = `${styles['line']}`
          },
        },
      })

      setColored(html)
    }

    func()
  }, [content, language])

  return colored
}

Grab useClipboard here.

Types

// Add languages you wish to use.
type Language = 
  | 'javascript' | 'js'
  | 'typescript' | 'ts'
  | 'html' 
  | 'css' 
  | 'go' 
  | 'elixir'

Check out all shiki's supported type.

Styles

To add line number, we use custom CSS. Credits to this comment.

.code {
  tab-size: 2;
  counter-reset: step;
  counter-increment: step 0;
}

.code .line::before {
  content: counter(step);
  counter-increment: step;
  width: 1rem;
  margin-right: 1rem;
  display: inline-block;
  text-align: right;
  color: rgba(115,138,148,.4);
}