Prism.js Syntax Highlighting in Extensions: Complete Implementation Guide
When building Chrome extensions that display or manipulate code, implementing professional syntax highlighting becomes essential for user experience. Prism.js stands as one of the most popular and lightweight syntax highlighting libraries available, making it an ideal choice for Chrome extension development. This comprehensive guide walks you through implementing Prism.js syntax highlighting in extensions, covering everything from basic setup to advanced customization techniques.
Whether you are building a code viewer extension, a documentation tool, or any extension that displays code snippets, this guide provides the knowledge and practical examples needed to create polished, professional code presentation in your Chrome extension.
Understanding Prism.js and Its Role in Chrome Extensions
Prism.js is a lightweight, robust syntax highlighting library written in JavaScript. It supports over 200 programming languages and comes with various themes out of the box. What makes Prism.js particularly well-suited for Chrome extensions is its minimal footprint, modular architecture, and excellent performance characteristics.
Why Choose Prism.js for Your Extension
The decision to use Prism.js in your Chrome extension comes with several compelling advantages. First, its file size is remarkably small even with multiple language support, which matters significantly when every kilobyte counts in extension performance. Second, Prism.js uses a simple API that makes integration straightforward regardless of your extension’s complexity. Third, the library is actively maintained with regular updates and improvements.
Chrome extensions often need to work across different web pages and contexts. Prism.js handles this gracefully because it can run in both content scripts and popup contexts, giving you flexibility in where and how you implement syntax highlighting. The library also supports automatic language detection and manual language specification, allowing users to control how code is highlighted.
Core Components of Prism.js
Understanding the core components helps you make informed decisions during implementation. The Prism core provides the highlighting engine, tokenization system, and plugin framework. Language components add support for specific programming languages, with each language file containing the grammar rules for that syntax. Themes control the visual appearance of highlighted code, with options ranging from minimal light themes to vibrant dark themes.
Plugins extend Prism’s functionality in various ways. The Copy to Clipboard plugin adds convenient copy buttons to code blocks. The Line Numbers plugin adds line numbering. The Highlight Keywords plugin allows custom keyword highlighting. These plugins integrate seamlessly and can be included or excluded based on your extension’s needs.
Setting Up Prism.js in Your Chrome Extension
Implementing Prism.js in a Chrome extension requires careful consideration of how the library will be loaded and applied. The approach differs slightly depending on whether you are highlighting code in content scripts, popup pages, or options pages.
Including Prism.js Files in Your Extension
The first step involves adding the necessary Prism.js files to your extension’s directory structure. You need the core library file, language components for the languages you want to support, and a theme CSS file. For a typical extension supporting JavaScript, HTML, and CSS, your extension directory would include prism-core.min.js, prism-javascript.min.js, prism-css.min.js, prism-markup.min.js, and a theme like prism-tomorrow.min.css.
Download these files from the official Prism.js website or repository. Pay attention to the minified versions for production use, as they significantly reduce the extension’s total size. You might also consider using a build tool to create a custom Prism bundle that includes only the languages your extension actually needs.
Loading Prism.js in Content Scripts
Content scripts run in the context of web pages, which presents unique challenges for loading external libraries. The recommended approach involves bundling Prism.js directly with your extension rather than loading it from external CDNs. This ensures reliability regardless of internet connectivity and avoids potential conflicts with page scripts.
// In your content script
(function() {
// Wait for DOM to be ready
document.addEventListener('DOMContentLoaded', function() {
// Find code elements that need highlighting
const codeElements = document.querySelectorAll('pre code');
codeElements.forEach(function(codeElement) {
// Add Prism classes if not already present
codeElement.classList.add('language-javascript');
// Apply highlighting
if (typeof Prism !== 'undefined') {
Prism.highlightElement(codeElement);
}
});
});
})();
This approach ensures that Prism.js is applied to code elements on the page. Remember that your content script must properly inject Prism and handle cases where the library might already exist on the page or where the DOM elements appear dynamically.
Loading Prism.js in Extension Pages
For popup pages and options pages, loading Prism.js is more straightforward since these pages are not subject to the same constraints as content scripts. You can include Prism.js files directly in your HTML using standard script and link tags.
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="prism-tomorrow.min.css">
</head>
<body>
<pre><code class="language-javascript">
function greet(name) {
console.log('Hello, ' + name + '!');
}
</code></pre>
<script src="prism-core.min.js"></script>
<script src="prism-javascript.min.js"></script>
</body>
</html>
This simple setup works perfectly for static content in your extension pages. For dynamic content, you would call Prism.highlightElement() after inserting new code elements into the DOM.
Implementing Code Highlighting in Content Scripts
Creating a robust content script implementation requires handling various edge cases and ensuring reliable operation across different websites. This section provides a production-ready approach to syntax highlighting in content scripts.
Creating a Reusable Highlighting Module
A well-designed content script separates the Prism integration logic into a reusable module. This makes your code more maintainable and easier to test. Consider creating a dedicated file that handles all Prism-related functionality.
// prism-highlighter.js
var PrismHighlighter = (function() {
'use strict';
// Supported language mappings
var languageMap = {
'js': 'javascript',
'ts': 'typescript',
'py': 'python',
'rb': 'ruby',
'java': 'java',
'cpp': 'cpp',
'c': 'c',
'csharp': 'csharp',
'php': 'php',
'swift': 'swift',
'kotlin': 'kotlin',
'go': 'go',
'rust': 'rust',
'sql': 'sql',
'html': 'markup',
'xml': 'markup',
'css': 'css',
'scss': 'scss',
'json': 'json',
'yaml': 'yaml',
'bash': 'bash',
'shell': 'bash',
'docker': 'docker',
'markdown': 'markdown'
};
function highlightCodeElement(element) {
if (!element || !element.classList.contains('needs-highlight')) {
return;
}
// Detect language from class or data attribute
var language = detectLanguage(element);
if (language) {
element.classList.add('language-' + language);
}
// Apply Prism highlighting
if (typeof Prism !== 'undefined' && Prism.highlightElement) {
Prism.highlightElement(element);
}
// Mark as processed
element.classList.remove('needs-highlight');
element.classList.add('highlighted');
}
function detectLanguage(element) {
// Check for language class first
for (var i = 0; i < element.classList.length; i++) {
var className = element.classList[i];
if (className.startsWith('language-')) {
return className.replace('language-', '');
}
}
// Check data attribute
var dataLang = element.getAttribute('data-language') ||
element.getAttribute('data-lang');
if (dataLang && languageMap[dataLang.toLowerCase()]) {
return languageMap[dataLang.toLowerCase()];
}
return null;
}
function highlightAll(container, selector) {
var elements = container.querySelectorAll(selector || 'pre code.needs-highlight');
elements.forEach(function(element) {
highlightCodeElement(element);
});
}
return {
highlight: highlightCodeElement,
highlightAll: highlightAll,
languageMap: languageMap
};
})();
This module provides flexible language detection and handles the highlighting process cleanly. You can include this in your content script and call it as needed.
Handling Dynamic Content
Modern web applications frequently load content dynamically through JavaScript. Your content script needs to handle this scenario to ensure all code gets highlighted. The MutationObserver API provides an elegant solution for detecting new content.
// In your content script
(function() {
'use strict';
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the new node is a code element
if (node.tagName === 'PRE' && node.querySelector('code')) {
Prism.highlightElement(node.querySelector('code'));
}
// Check for code elements within the new node
var codeElements = node.querySelectorAll &&
node.querySelectorAll('pre code');
if (codeElements) {
codeElements.forEach(function(codeEl) {
Prism.highlightElement(codeEl);
});
}
}
});
});
});
// Configure and start observing
var config = {
childList: true,
subtree: true
};
observer.observe(document.body, config);
// Initial highlighting
document.addEventListener('DOMContentLoaded', function() {
Prism.highlightAll();
});
})();
This observer ensures that any dynamically added code elements get highlighted automatically, providing a seamless experience for users regardless of how the page loads content.
Styling and Customization
Prism.js themes control the visual appearance of highlighted code. Understanding how to customize these themes and create your own ensures your extension presents code in a way that matches your design vision.
Choosing and Applying Themes
Prism.js ships with several themes that you can use directly or customize. The themes range from minimalist to elaborate, with options like Tomorrow Night, Monokai, and Solarized Light being particularly popular. To use a theme, simply include its CSS file in your extension.
For most extensions, one of the existing themes will work well without modification. However, you might find that the default theme doesn’t perfectly match your extension’s color scheme or that certain token colors are difficult to read. In these cases, customization becomes necessary.
Customizing Theme Colors
Customizing a Prism theme involves overriding the token color definitions. Each token type—keyword, string, comment, function, and so on—has an associated CSS class that controls its appearance. You can create a custom CSS file that overrides specific colors.
/* custom-prism-theme.css */
/* Override token colors for better readability */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #6a9955;
}
.token.punctuation {
color: #d4d4d4;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #b5cea8;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #ce9178;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #d4d4d4;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #569cd6;
}
.token.function,
.token.class-name {
color: #dcdcaa;
}
.token.regex,
.token.important,
.token.variable {
color: #d16969;
}
/* Custom background */
pre[class*="language-"] {
background: #1e1e1e;
border-radius: 8px;
padding: 1em;
margin: 1em 0;
overflow: auto;
}
code[class*="language-"] {
background: transparent;
text-shadow: none;
}
This custom theme uses a dark background with carefully chosen colors optimized for readability. You can adjust these values to match your extension’s design language or to improve accessibility.
Responsive Code Display
Code blocks need special handling on smaller screens. Long lines of code can break your layout or require excessive horizontal scrolling, which provides a poor user experience. Implementing responsive code display ensures your highlighted code looks good on all devices.
/* Responsive code display */
pre[class*="language-"] {
white-space: pre-wrap;
word-wrap: break-word;
max-width: 100%;
}
code[class*="language-"] {
white-space: pre-wrap;
word-break: break-all;
}
/* Line numbers responsiveness */
pre.line-numbers {
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
}
pre.line-numbers > code {
position: relative;
white-space: inherit;
}
.line-numbers .line-numbers-rows {
position: absolute;
pointer-events: none;
top: 1em;
left: 0;
width: 3em;
letter-spacing: -1px;
border-right: 1px solid #555;
user-select: none;
}
.line-numbers-rows > span {
display: block;
counter-increment: linenumber;
}
.line-numbers-rows > span:before {
content: counter(linenumber);
color: #555;
display: block;
padding-right: 0.8em;
text-align: right;
}
These responsive styles ensure that code wraps appropriately on smaller screens while maintaining readability and structure.
Advanced Features and Optimization
Once you have the basic Prism.js implementation working, you can enhance your extension with advanced features that improve usability and performance.
Implementing Copy-to-Clipboard Functionality
Adding a copy button to code blocks provides significant user value. Users can easily copy code snippets without manually selecting and copying text, which is particularly useful on mobile devices or with long code blocks.
// Copy to Clipboard functionality
function addCopyButtons() {
document.querySelectorAll('pre').forEach(function(pre) {
// Check if button already exists
if (pre.querySelector('.copy-button')) {
return;
}
var button = document.createElement('button');
button.className = 'copy-button';
button.textContent = 'Copy';
button.setAttribute('aria-label', 'Copy code to clipboard');
button.addEventListener('click', function() {
var code = pre.querySelector('code');
var text = code ? code.textContent : pre.textContent;
navigator.clipboard.writeText(text).then(function() {
button.textContent = 'Copied!';
button.classList.add('copied');
setTimeout(function() {
button.textContent = 'Copy';
button.classList.remove('copied');
}, 2000);
}).catch(function(err) {
console.error('Failed to copy:', err);
button.textContent = 'Error';
});
});
pre.style.position = 'relative';
pre.appendChild(button);
});
}
This implementation adds a copy button to each code block and provides visual feedback when copying succeeds or fails.
Performance Optimization Techniques
When working with many code blocks or large code snippets, performance becomes a concern. Several optimization techniques help maintain smooth user experience.
First, consider lazy loading Prism language components. Instead of loading all language support upfront, load languages on demand when code requiring that language is encountered. This reduces initial load time and memory usage.
// Lazy language loading
var loadedLanguages = {};
function loadLanguage(language) {
if (loadedLanguages[language]) {
return Promise.resolve();
}
return new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = '/path/to/prism-' + language + '.min.js';
script.onload = function() {
loadedLanguages[language] = true;
resolve();
};
script.onerror = reject;
document.head.appendChild(script);
});
}
Second, debounce highlighting operations when processing dynamic content. This prevents excessive processing when multiple mutations occur in quick succession.
// Debounced highlighting
var highlightTimeout;
function debouncedHighlight() {
clearTimeout(highlightTimeout);
highlightTimeout setTimeout(function() {
Prism.highlightAll();
}, 250);
}
// Use with MutationObserver
observer.observe(document.body, {
childList: true,
subtree: true
});
observer.callback = function() {
debouncedHighlight();
};
Handling Multiple Languages
Extensions that display code from various sources need robust multi-language support. Implement automatic language detection to improve user experience without requiring manual language specification.
// Automatic language detection
function detectCodeLanguage(code) {
// Simple heuristics based on code patterns
// TypeScript indicators
if (code.includes(': string') || code.includes(': number') ||
code.includes('interface ') || code.includes('type ')) {
return 'typescript';
}
// Python indicators
if (code.includes('def ') || code.includes('import ') ||
code.includes('class ') || code.includes('print(')) {
return 'python';
}
// HTML indicators
if (code.includes('<html') || code.includes('<div') ||
code.includes('</')) {
return 'markup';
}
// CSS indicators
if (code.includes('{') && (code.includes('color:') ||
code.includes('margin:') || code.includes('padding:'))) {
return 'css';
}
// JavaScript indicators
if (code.includes('function ') || code.includes('const ') ||
code.includes('let ') || code.includes('=>')) {
return 'javascript';
}
return null;
}
While automatic detection is not perfect, it provides a reasonable default that users can override manually if needed.
Troubleshooting Common Issues
Even with careful implementation, you may encounter issues when integrating Prism.js into your Chrome extension. Understanding common problems and their solutions helps you resolve issues quickly.
Prism Not Defined Errors
One of the most common issues involves Prism not being available when your content script runs. This happens because the loading order of scripts is not guaranteed. Using the manifest’s “run_at” property to control when your content script loads can resolve this.
{
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["prism-core.min.js", "prism-javascript.min.js",
"your-content-script.js"],
"run_at": "document_idle"
}
]
}
By specifying “run_at”: “document_idle”, you ensure that your script runs after the DOM is complete but before the “load” event fires, giving the page time to settle.
Conflicts with Page Scripts
Sometimes, the website where your content script runs already uses Prism.js, and conflicts can arise. In these cases, scoping your Prism usage to avoid interfering with the page’s instance becomes necessary.
// Isolated Prism usage
(function() {
// Store original Prism if it exists
var originalPrism = window.Prism;
// Your isolated Prism implementation
// ... load and use your bundled Prism ...
// Restore original after your work is done
// window.Prism = originalPrism;
})();
This approach allows you to work with your own Prism instance without affecting the page’s syntax highlighting.
Styles Not Applying
If highlighting appears to work but styles are not applied correctly, check for CSS specificity issues or missing theme files. Ensure your extension’s CSS is properly loaded and that the theme file is included in your manifest.
{
"content_scripts": [
{
"matches": ["<all_urls>"],
"css": ["prism-theme.css"],
"js": ["prism-core.min.js", "content-script.js"]
}
]
}
Also verify that your CSS selectors match Prism’s expected class names, as the library uses specific naming conventions that must be followed.
Conclusion
Implementing Prism.js syntax highlighting in your Chrome extension transforms raw code blocks into beautiful, readable, and professional-looking content. This guide covered the essential aspects of integration, from initial setup through advanced customization and optimization.
Remember to bundle Prism.js files directly with your extension rather than relying on external CDNs. Design your content scripts to handle both static and dynamically loaded content. Customize the visual theme to match your extension’s design language while ensuring responsiveness across devices.
The techniques and code examples provided here give you a solid foundation for creating polished code presentation in your Chrome extension. As you develop your extension, you will discover additional customization possibilities that further enhance the user experience.
With Prism.js properly integrated, your extension will provide users with the professional code viewing experience they expect from modern browser extensions.