Building a Web Component for Code Blocks

Create reusable, encapsulated code display components with native web standards including Shadow DOM, syntax highlighting, and framework-agnostic API design.

Why Code Blocks Are Ideal for Web Components

Code blocks present unique challenges that web components solve elegantly. First, syntax highlighting requires extensive CSS that can easily conflict with a site's existing styles. Shadow DOM provides true encapsulation, ensuring that the complex token styling for different programming languages remains isolated from the rest of the page. Second, code blocks benefit from a clean API--attributes like data-lang or show-line-numbers allow developers to configure the component declaratively without understanding its internal implementation. Third, features like copy-to-clipboard functionality can be implemented once and work consistently regardless of whether the component is used in a React application, Vue project, or plain HTML page.

The sweet spot for native web components lies in design system components where you build out your own little API for the components in your system. Consumers can use them in a way that is safer than copying and pasting chunks of HTML, and if they want to bring their own framework, they can still use the native elements without framework overhead.

Key Benefits of Native Web Components for Code Display

  • Style Isolation: Shadow DOM prevents syntax highlighting CSS from leaking or being affected by page styles
  • Framework Agnostic: Works with React, Vue, Angular, or plain HTML
  • Declarative API: Configure via HTML attributes without JavaScript knowledge
  • Native Integration: Becomes a real HTML element in the browser
  • Reusable: Build once, use across multiple projects and teams
Core Features of a Code Block Web Component

Essential capabilities that every professional code display component should include

Shadow DOM Encapsulation

Isolate syntax highlighting styles from the rest of the page, preventing conflicts and ensuring consistent rendering across all browsers.

Syntax Highlighting

Support for multiple programming languages with popular libraries like Prism.js or Highlight.js for beautiful, readable code.

Copy-to-Clipboard

One-click button to copy code content with visual feedback and error handling for a seamless user experience.

Language Configuration

Declare the programming language via attributes like `language` or `data-lang` for automatic syntax detection and highlighting.

Line Number Support

Optional line numbering with proper styling and alignment to enhance readability of longer code examples.

Framework Integration

Works identically in React, Vue, Angular, or vanilla JavaScript without requiring framework-specific versions.

Creating the Component Foundation

Shadow DOM and Style Encapsulation

The foundation of any code block web component is the shadow DOM, which creates a boundary between the component's internal implementation and the surrounding page. This encapsulation ensures that the syntax highlighting CSS--often thousands of lines for supporting multiple languages--cannot accidentally affect other elements on the page. Equally important, page styles cannot break the component's internal styling.

When creating the component, you should create the shadow root in the constructor. This ensures exclusive ownership of the element during setup and prevents issues that can occur when elements are disconnected and reconnected to the document. Following Google's best practices for custom elements, the constructor is the right moment to configure implementation details that should remain private and not be exposed to external manipulation.

class CodeBlock extends HTMLElement {
 constructor() {
 super();
 this.attachShadow({ mode: 'open' });
 }
}

The :host selector in shadow DOM styles controls the display behavior of the custom element itself. By default, custom elements have display: inline, which often surprises developers trying to set width or height. For a code block component, you typically want display: block or display: flex to ensure proper layout.

API Design and Configuration

A well-designed code block component accepts configuration through HTML attributes, making it declarative and framework-agnostic. Key attributes include language or data-lang to specify the programming language, show-line-numbers to enable line numbering, and file-name to display an optional filename above the code.

Recommended Attributes:

  • language: Programming language for syntax highlighting (e.g., "javascript", "python", "rust")
  • show-line-numbers: Boolean attribute to enable line numbering
  • file-name: Optional filename displayed above the code block
  • theme: Optional theme name for syntax highlighting colors

You should always accept primitive data--strings, numbers, and booleans--as both attributes and properties. This allows developers to configure the component either declaratively in HTML or imperatively in JavaScript. As recommended by Web.dev's custom elements guide, primitive data attributes and properties should be kept in sync, reflecting from property to attribute and vice versa.

Best Practices for Custom Elements

Google's official guidance on building high-quality custom elements provides essential patterns for code block components.

Essential Guidelines

Shadow DOM Setup:

  • Always create a shadow root in the constructor to encapsulate styles
  • Create the shadow root before any child manipulation
  • Use :host display styles to control layout behavior
  • Handle the hidden attribute with :host([hidden]) { display: none }

Attribute and Property Handling:

  • Accept primitive data (strings, numbers, booleans) as both attributes and properties
  • Keep attributes and properties synchronized through reflection
  • Do not reflect rich data (objects, arrays) to attributes
  • Check for author-set attributes before applying defaults
  • Consider properties that may be set before element upgrade

Event Handling:

  • Dispatch events in response to internal component activity
  • Do not dispatch events in response to host-setting properties (prevents infinite loops)
  • Use descriptive event names like code-copied or language-changed

Lifecycle Considerations

class CodeBlock extends HTMLElement {
 static get observedAttributes() {
 return ['language', 'show-line-numbers', 'file-name'];
 }

 constructor() {
 super();
 this.attachShadow({ mode: 'open' });
 this._upgradeProperty('language');
 }

 connectedCallback() {
 this._render();
 this._applySyntaxHighlighting();
 }

 attributeChangedCallback(name, oldValue, newValue) {
 if (oldValue !== newValue) {
 this._handleAttributeChange(name, newValue);
 }
 }

 _upgradeProperty(prop) {
 if (this.hasOwnProperty(prop)) {
 let value = this[prop];
 delete this[prop];
 this[prop] = value;
 }
 }
}

These lifecycle patterns ensure your component handles upgrades gracefully and responds appropriately to configuration changes, whether you're building for a small documentation site or a large-scale custom software development project.

Syntax Highlighting Integration

Choosing a Syntax Highlighter

Two popular options for syntax highlighting are Prism.js and Highlight.js. Both provide extensive language support and tokenization, but they differ in their approach to modularity and ES module compatibility. As noted in CSS-Tricks' exploration of code block web components, Prism.js is widely used but traditionally doesn't use ES modules, which can make consuming it within web components more complex. Highlight.js offers similar coverage with different trade-offs.

For modern web component development, consider using the ES module versions of these libraries or bundling them with your component. The HubSpot Developers guide emphasizes creating framework-agnostic code block components that work across different teams and frameworks.

Implementation Pattern

The connectedCallback is the appropriate lifecycle hook for applying syntax highlighting because it fires when the element is added to the DOM.

class CodeBlock extends HTMLElement {
 constructor() {
 super();
 this.attachShadow({ mode: 'open' });
 }

 connectedCallback() {
 this._render();
 this._applySyntaxHighlighting();
 }

 _applySyntaxHighlighting() {
 const codeElement = this.shadowRoot.querySelector('code');
 if (codeElement && this.language) {
 // Apply syntax highlighting based on language
 codeElement.className = `language-${this.language}`;
 Prism.highlightElement(codeElement);
 }
 }

 _render() {
 this.shadowRoot.innerHTML = `
 <style>
 :host { display: block; }
 .container { background: #1e1e1e; border-radius: 8px; overflow: hidden; }
 .header { display: flex; justify-content: space-between; padding: 8px 16px; background: #2d2d2d; }
 .language-badge { color: #fff; font-size: 12px; text-transform: uppercase; }
 .copy-btn { background: transparent; border: 1px solid #444; color: #fff; padding: 4px 12px; border-radius: 4px; cursor: pointer; }
 pre { margin: 0; padding: 16px; overflow-x: auto; }
 code { font-family: 'Fira Code', monospace; font-size: 14px; }
 </style>
 <div class="container">
 <div class="header">
 <span class="language-badge">${this.language || 'text'}</span>
 <button class="copy-btn">Copy</button>
 </div>
 <pre><code>${this._escapeHtml(this.textContent)}</code></pre>
 </div>
 `;
 }

 _escapeHtml(text) {
 return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
 }
}

customElements.define('code-block', CodeBlock);

Interactive Features

Copy-to-Clipboard Implementation

One of the most requested features for code blocks is a button to copy the code content to the clipboard. The implementation should provide visual feedback to users and handle errors gracefully.

_copyCode() {
 const code = this.textContent.trim();
 navigator.clipboard.writeText(code).then(() => {
 const btn = this.shadowRoot.querySelector('.copy-btn');
 const originalText = btn.textContent;
 btn.textContent = 'Copied!';
 
 // Dispatch custom event
 this.dispatchEvent(new CustomEvent('code-copied', {
 bubbles: true,
 composed: true,
 detail: { code }
 }));

 setTimeout(() => {
 btn.textContent = originalText;
 }, 2000);
 }).catch(err => {
 console.error('Failed to copy:', err);
 const btn = this.shadowRoot.querySelector('.copy-btn');
 btn.textContent = 'Error';
 setTimeout(() => {
 btn.textContent = 'Copy';
 }, 2000);
 });
}

Framework Integration Benefits

Building code block components as native web components maximizes reusability across different teams and projects. A React team, Vue team, and plain HTML project can all use the same component without modification. This approach aligns with the concept of design system components that provide a consistent API while remaining agnostic to the consuming technology. Whether you're working on a Next.js application, a Vue.js project, or any other framework, the same component works consistently.

Framework Compatibility:

  • React: Use as <code-block language="javascript"></code-block> with onChange events
  • Vue: Native support for custom elements in recent versions
  • Angular: Works with proper attribute binding syntax
  • Vanilla JS: Full API access via JavaScript properties

Performance Optimization

Syntax highlighting libraries can be large, so consider dynamic imports for lazy loading:

async _applySyntaxHighlighting() {
 if (!this.language) return;
 
 const { Prism } = await import('prismjs');
 await import('prismjs/components/prism-javascript');
 
 const codeElement = this.shadowRoot.querySelector('code');
 codeElement.textContent = this.textContent;
 Prism.highlightElement(codeElement);
}

This pattern reduces initial bundle size and improves page load performance, especially on pages with multiple code blocks where not all languages may be used.

Frequently Asked Questions

Ready to Build Your Own Web Components?

Our team specializes in modern web development using native web standards, React, Next.js, and performance-optimized architectures.

Sources

  1. CSS-Tricks: Web Component for a Code Block - Implementation patterns and API design for code block web components
  2. HubSpot Developers: How to Build a Code Block Web Component - Framework-agnostic component development approach
  3. Web.dev: Custom Elements Best Practices - Google's official best practices checklist for custom elements