Custom Themes and Styling
Create distinctive, branded documentation experiences through custom DocFX themes and advanced styling techniques.
Overview
Custom themes allow you to transform the standard DocFX appearance into a unique, branded experience that aligns with your organization's visual identity and user experience requirements.
Theme Architecture
DocFX Theme Structure
custom-theme/
├── styles/
│ ├── main.css
│ ├── bootstrap.min.css
│ └── highlight.js.css
├── templates/
│ ├── conceptual.html.primary.tmpl
│ ├── toc.html.tmpl
│ └── partials/
│ ├── head.tmpl
│ ├── navbar.tmpl
│ ├── footer.tmpl
│ └── scripts.tmpl
├── fonts/
│ ├── custom-font.woff2
│ └── icons.woff
├── images/
│ ├── logo.svg
│ ├── favicon.ico
│ └── background-patterns/
└── scripts/
├── main.js
└── analytics.js
Theme Configuration
{
"template": [
"default",
"custom-theme"
],
"globalMetadata": {
"_appTitle": "Your Documentation",
"_appLogoPath": "images/logo.svg",
"_appFaviconPath": "images/favicon.ico",
"_enableSearch": true,
"_enableNewTab": true,
"_disableContribution": false,
"_gitContribute": {
"repo": "https://github.com/your-org/docs",
"branch": "main"
}
},
"fileMetadata": {
"_layout": {
"docs/**/*.md": "Conceptual",
"api/**/*.yml": "ManagedReference"
}
}
}
Advanced CSS Customization
CSS Variables for Theme Consistency
/* styles/main.css */
:root {
/* Brand Colors */
--primary-color: #0066cc;
--secondary-color: #004d99;
--accent-color: #ff6b35;
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
/* Neutral Colors */
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--text-muted: #868e96;
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
/* Typography */
--font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-family-mono: 'JetBrains Mono', 'Fira Code', 'Monaco', monospace;
--font-size-base: 16px;
--line-height-base: 1.6;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 3rem;
/* Borders and Shadows */
--border-radius: 0.375rem;
--border-color: #dee2e6;
--shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--shadow-md: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
}
/* Dark Theme Variables */
[data-theme="dark"] {
--text-primary: #e9ecef;
--text-secondary: #adb5bd;
--text-muted: #6c757d;
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #404040;
--border-color: #404040;
}
Modern Navigation Design
/* Enhanced Navigation */
.navbar {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
box-shadow: var(--shadow-md);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.navbar-brand {
display: flex;
align-items: center;
font-weight: 600;
color: white !important;
text-decoration: none;
}
.navbar-brand img {
height: 32px;
margin-right: var(--spacing-sm);
filter: brightness(0) invert(1);
}
.navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.9) !important;
font-weight: 500;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius);
transition: all 0.3s ease;
}
.navbar-nav .nav-link:hover {
color: white !important;
background: rgba(255, 255, 255, 0.1);
transform: translateY(-1px);
}
/* Search Enhancement */
.navbar-form {
position: relative;
}
.navbar-form .form-control {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
border-radius: 25px;
padding-left: 40px;
width: 300px;
transition: all 0.3s ease;
}
.navbar-form .form-control::placeholder {
color: rgba(255, 255, 255, 0.7);
}
.navbar-form .form-control:focus {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.4);
box-shadow: 0 0 0 0.2rem rgba(255, 255, 255, 0.1);
width: 350px;
}
.navbar-form::before {
content: '🔍';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: rgba(255, 255, 255, 0.7);
}
Content Layout Enhancements
/* Main Content Area */
.content-wrapper {
display: grid;
grid-template-columns: 280px 1fr 240px;
grid-template-areas:
"sidebar content toc";
gap: var(--spacing-xl);
max-width: 1400px;
margin: 0 auto;
padding: var(--spacing-xl);
}
@media (max-width: 1200px) {
.content-wrapper {
grid-template-columns: 1fr;
grid-template-areas:
"content";
}
.sidebar, .toc {
display: none;
}
}
/* Article Styling */
.article {
grid-area: content;
background: var(--bg-primary);
border-radius: var(--border-radius);
box-shadow: var(--shadow-sm);
padding: var(--spacing-xl);
line-height: var(--line-height-base);
}
.article h1, .article h2, .article h3, .article h4, .article h5, .article h6 {
font-family: var(--font-family-sans);
font-weight: 600;
line-height: 1.3;
margin-top: var(--spacing-lg);
margin-bottom: var(--spacing-md);
color: var(--text-primary);
}
.article h1 {
font-size: 2.5rem;
color: var(--primary-color);
border-bottom: 3px solid var(--primary-color);
padding-bottom: var(--spacing-sm);
}
.article h2 {
font-size: 2rem;
position: relative;
}
.article h2::before {
content: '';
position: absolute;
left: -var(--spacing-md);
top: 0;
bottom: 0;
width: 4px;
background: var(--accent-color);
border-radius: 2px;
}
/* Code Blocks */
.article pre {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--spacing-lg);
overflow-x: auto;
font-family: var(--font-family-mono);
font-size: 0.875rem;
line-height: 1.5;
position: relative;
}
.article pre::before {
content: attr(data-lang);
position: absolute;
top: var(--spacing-sm);
right: var(--spacing-sm);
background: var(--primary-color);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.article code {
background: rgba(var(--primary-color), 0.1);
color: var(--primary-color);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-family: var(--font-family-mono);
font-size: 0.875em;
}
/* Tables */
.article table {
width: 100%;
border-collapse: collapse;
margin: var(--spacing-lg) 0;
background: var(--bg-primary);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.article th {
background: var(--primary-color);
color: white;
padding: var(--spacing-md);
text-align: left;
font-weight: 600;
}
.article td {
padding: var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
.article tr:nth-child(even) {
background: var(--bg-secondary);
}
.article tr:hover {
background: rgba(var(--primary-color), 0.05);
}
Interactive Components
/* Collapsible Sections */
.collapsible {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
margin: var(--spacing-md) 0;
overflow: hidden;
}
.collapsible-header {
padding: var(--spacing-md);
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
color: white;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s ease;
}
.collapsible-header:hover {
background: linear-gradient(90deg, var(--secondary-color), var(--primary-color));
}
.collapsible-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.collapsible.active .collapsible-content {
max-height: 500px;
}
.collapsible-body {
padding: var(--spacing-lg);
}
/* Alert Boxes */
.alert {
padding: var(--spacing-md);
border-radius: var(--border-radius);
margin: var(--spacing-md) 0;
border-left: 4px solid;
position: relative;
display: flex;
align-items: flex-start;
}
.alert::before {
font-size: 1.5rem;
margin-right: var(--spacing-sm);
}
.alert-info {
background: rgba(13, 202, 240, 0.1);
border-color: #0dcaf0;
color: #055160;
}
.alert-info::before {
content: 'ℹ️';
}
.alert-warning {
background: rgba(255, 193, 7, 0.1);
border-color: var(--warning-color);
color: #664d03;
}
.alert-warning::before {
content: '⚠️';
}
.alert-danger {
background: rgba(220, 53, 69, 0.1);
border-color: var(--danger-color);
color: #721c24;
}
.alert-danger::before {
content: '🚫';
}
.alert-success {
background: rgba(40, 167, 69, 0.1);
border-color: var(--success-color);
color: #0f5132;
}
.alert-success::before {
content: '✅';
}
/* Tabs */
.tabs {
margin: var(--spacing-lg) 0;
}
.tab-headers {
display: flex;
border-bottom: 2px solid var(--border-color);
margin-bottom: var(--spacing-md);
}
.tab-header {
padding: var(--spacing-sm) var(--spacing-lg);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-bottom: none;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
}
.tab-header:first-child {
border-top-left-radius: var(--border-radius);
}
.tab-header:last-child {
border-top-right-radius: var(--border-radius);
}
.tab-header.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.tab-content {
display: none;
padding: var(--spacing-lg);
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
.tab-content.active {
display: block;
}
JavaScript Enhancements
Theme Switcher
// scripts/theme-switcher.js
class ThemeSwitcher {
constructor() {
this.currentTheme = localStorage.getItem('theme') || 'light';
this.init();
}
init() {
this.applyTheme(this.currentTheme);
this.createToggleButton();
this.bindEvents();
}
createToggleButton() {
const button = document.createElement('button');
button.className = 'theme-toggle';
button.innerHTML = this.currentTheme === 'dark' ? '☀️' : '🌙';
button.setAttribute('aria-label', 'Toggle theme');
const navbar = document.querySelector('.navbar-nav');
if (navbar) {
navbar.appendChild(button);
}
}
bindEvents() {
document.addEventListener('click', (e) => {
if (e.target.classList.contains('theme-toggle')) {
this.toggleTheme();
}
});
}
toggleTheme() {
this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
this.applyTheme(this.currentTheme);
localStorage.setItem('theme', this.currentTheme);
const button = document.querySelector('.theme-toggle');
if (button) {
button.innerHTML = this.currentTheme === 'dark' ? '☀️' : '🌙';
}
}
applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
}
// Initialize theme switcher
document.addEventListener('DOMContentLoaded', () => {
new ThemeSwitcher();
});
Enhanced Search
// scripts/enhanced-search.js
class EnhancedSearch {
constructor() {
this.searchIndex = null;
this.searchResults = [];
this.init();
}
async init() {
await this.loadSearchIndex();
this.bindEvents();
this.createSearchSuggestions();
}
async loadSearchIndex() {
try {
const response = await fetch('/search-index.json');
this.searchIndex = await response.json();
} catch (error) {
console.error('Failed to load search index:', error);
}
}
bindEvents() {
const searchInput = document.querySelector('#search-query');
if (searchInput) {
searchInput.addEventListener('input', this.debounce(this.handleSearch.bind(this), 300));
searchInput.addEventListener('keydown', this.handleKeyDown.bind(this));
}
}
handleSearch(event) {
const query = event.target.value.trim();
if (query.length < 2) {
this.hideSearchResults();
return;
}
this.searchResults = this.performSearch(query);
this.displaySearchResults(this.searchResults);
}
performSearch(query) {
if (!this.searchIndex) return [];
const results = [];
const queryLower = query.toLowerCase();
for (const item of this.searchIndex) {
let score = 0;
// Title match (highest weight)
if (item.title.toLowerCase().includes(queryLower)) {
score += 10;
}
// Content match
if (item.content.toLowerCase().includes(queryLower)) {
score += 5;
}
// Tag match
if (item.tags && item.tags.some(tag => tag.toLowerCase().includes(queryLower))) {
score += 3;
}
if (score > 0) {
results.push({ ...item, score });
}
}
return results.sort((a, b) => b.score - a.score).slice(0, 10);
}
displaySearchResults(results) {
const container = this.getOrCreateResultsContainer();
if (results.length === 0) {
container.innerHTML = '<div class="search-no-results">No results found</div>';
} else {
container.innerHTML = results.map(result => `
<div class="search-result-item">
<h4><a href="${result.url}">${this.highlightText(result.title, query)}</a></h4>
<p>${this.highlightText(this.getExcerpt(result.content, query), query)}</p>
<div class="search-result-meta">
${result.tags ? result.tags.map(tag => `<span class="tag">${tag}</span>`).join('') : ''}
</div>
</div>
`).join('');
}
container.style.display = 'block';
}
highlightText(text, query) {
const regex = new RegExp(`(${query})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
getExcerpt(content, query, length = 150) {
const index = content.toLowerCase().indexOf(query.toLowerCase());
if (index === -1) return content.substring(0, length) + '...';
const start = Math.max(0, index - 50);
const end = Math.min(content.length, start + length);
return (start > 0 ? '...' : '') +
content.substring(start, end) +
(end < content.length ? '...' : '');
}
getOrCreateResultsContainer() {
let container = document.querySelector('.search-results');
if (!container) {
container = document.createElement('div');
container.className = 'search-results';
const searchForm = document.querySelector('.navbar-form');
if (searchForm) {
searchForm.appendChild(container);
}
}
return container;
}
hideSearchResults() {
const container = document.querySelector('.search-results');
if (container) {
container.style.display = 'none';
}
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
// Initialize enhanced search
document.addEventListener('DOMContentLoaded', () => {
new EnhancedSearch();
});
Interactive Features
// scripts/interactive-features.js
class InteractiveFeatures {
constructor() {
this.init();
}
init() {
this.initCollapsibles();
this.initTabs();
this.initCodeCopyButtons();
this.initScrollSpy();
this.initSmoothScrolling();
}
initCollapsibles() {
document.querySelectorAll('.collapsible-header').forEach(header => {
header.addEventListener('click', () => {
const collapsible = header.closest('.collapsible');
collapsible.classList.toggle('active');
});
});
}
initTabs() {
document.querySelectorAll('.tab-header').forEach(header => {
header.addEventListener('click', () => {
const tabContainer = header.closest('.tabs');
const tabId = header.dataset.tab;
// Remove active class from all headers and contents
tabContainer.querySelectorAll('.tab-header').forEach(h => h.classList.remove('active'));
tabContainer.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
// Add active class to clicked header and corresponding content
header.classList.add('active');
tabContainer.querySelector(`[data-tab-content="${tabId}"]`).classList.add('active');
});
});
}
initCodeCopyButtons() {
document.querySelectorAll('pre code').forEach(codeBlock => {
const pre = codeBlock.parentElement;
const button = document.createElement('button');
button.className = 'copy-code-button';
button.innerHTML = '📋';
button.setAttribute('aria-label', 'Copy code');
button.addEventListener('click', () => {
navigator.clipboard.writeText(codeBlock.textContent).then(() => {
button.innerHTML = '✅';
setTimeout(() => {
button.innerHTML = '📋';
}, 2000);
});
});
pre.style.position = 'relative';
pre.appendChild(button);
});
}
initScrollSpy() {
const headers = document.querySelectorAll('h2, h3, h4');
const tocLinks = document.querySelectorAll('.toc a');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.id;
tocLinks.forEach(link => {
link.classList.toggle('active', link.getAttribute('href') === `#${id}`);
});
}
});
}, { rootMargin: '-10% 0% -85% 0%' });
headers.forEach(header => {
if (header.id) {
observer.observe(header);
}
});
}
initSmoothScrolling() {
document.querySelectorAll('a[href^="#"]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const target = document.querySelector(link.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth' });
}
});
});
}
}
// Initialize interactive features
document.addEventListener('DOMContentLoaded', () => {
new InteractiveFeatures();
});
Advanced Template Customization
Custom Template Processor
<!-- templates/conceptual.html.primary.tmpl -->
{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license.}}
{{!include(/^styles/.*/)}}
{{!include(/^fonts/.*/)}}
{{!include(favicon.ico)}}
{{!include(logo.svg)}}
<!DOCTYPE html>
<html{{#_lang}} lang="{{_lang}}"{{/_lang}}>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{{_appTitle}}}{{/_appTitle}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{{_appTitle}}}{{/_appTitle}}">
<meta name="generator" content="docfx {{_docfxVersion}}">
{{#_description}}<meta name="description" content="{{_description}}">{{/_description}}
{{#author}}<meta name="author" content="{{author}}">{{/author}}
<!-- Open Graph Tags -->
<meta property="og:title" content="{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}}">
{{#_description}}<meta property="og:description" content="{{_description}}">{{/_description}}
<meta property="og:type" content="article">
<meta property="og:url" content="{{_currentUrl}}">
<!-- Twitter Card Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}}">
{{#_description}}<meta name="twitter:description" content="{{_description}}">{{/_description}}
<link rel="shortcut icon" href="{{_rel}}{{{_appFaviconPath}}}{{^_appFaviconPath}}favicon.ico{{/_appFaviconPath}}">
<link rel="stylesheet" href="{{_rel}}styles/docfx.vendor.css">
<link rel="stylesheet" href="{{_rel}}styles/docfx.css">
<link rel="stylesheet" href="{{_rel}}styles/main.css">
<!-- Custom Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<meta property="docfx:navrel" content="{{_navRel}}">
<meta property="docfx:tocrel" content="{{_tocRel}}">
{{#_enableSearch}}<meta property="docfx:rel" content="{{_rel}}">{{/_enableSearch}}
{{#_enableNewTab}}<meta property="docfx:newtab" content="true">{{/_enableNewTab}}
<!-- Custom head content -->
{{>partials/head}}
</head>
<body data-spy="scroll" data-target="#affix" data-offset="120">
<div id="wrapper">
<header>
{{>partials/navbar}}
</header>
{{#_enableSearch}}
<div class="container body-content">
{{>partials/searchResults}}
</div>
{{/_enableSearch}}
<div class="content-wrapper">
{{#_tocPath}}
<div class="sidebar">
{{>partials/toc}}
</div>
{{/_tocPath}}
<div role="main" class="article">
{{>partials/breadcrumb}}
<article id="_content" data-uid="{{uid}}">
{{#conceptual}}
{{{conceptual}}}
{{/conceptual}}
{{^conceptual}}
{{!body}}
{{/conceptual}}
</article>
{{>partials/contributor}}
</div>
{{#_tocPath}}
<div class="toc">
{{>partials/affix}}
</div>
{{/_tocPath}}
</div>
{{>partials/footer}}
</div>
{{>partials/scripts}}
<!-- Custom Analytics -->
{{#_enableAnalytics}}
<script>
// Custom analytics implementation
(function() {
// Track page views
if (typeof gtag !== 'undefined') {
gtag('config', '{{_analyticsTrackingId}}', {
page_title: document.title,
page_location: window.location.href
});
}
// Track search queries
document.addEventListener('search', function(e) {
if (typeof gtag !== 'undefined') {
gtag('event', 'search', {
search_term: e.detail.query
});
}
});
})();
</script>
{{/_enableAnalytics}}
</body>
</html>
Performance Optimization
CSS Optimization
/* Critical CSS for above-the-fold content */
/* This should be inlined for better performance */
/* Layout skeleton for fast rendering */
.content-wrapper {
display: grid;
grid-template-columns: 280px 1fr 240px;
gap: var(--spacing-xl);
}
.navbar {
height: 60px;
background: var(--primary-color);
}
.article {
min-height: 400px;
background: var(--bg-primary);
}
/* Lazy load non-critical styles */
.fancy-animations,
.decorative-elements {
/* Load these after initial render */
}
Resource Loading Optimization
<!-- Optimized resource loading -->
<head>
<!-- Critical resources -->
<link rel="preload" href="fonts/Inter-Regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="styles/critical.css" as="style">
<!-- DNS prefetch for external resources -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//www.google-analytics.com">
<!-- Preconnect to critical third-party origins -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Critical CSS (inlined) -->
<style>
/* Critical above-the-fold styles */
</style>
<!-- Non-critical CSS (async) -->
<link rel="preload" href="styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles/main.css"></noscript>
</head>
Testing and Quality Assurance
Visual Regression Testing
// test/visual-regression.test.js
const puppeteer = require('puppeteer');
const pixelmatch = require('pixelmatch');
const PNG = require('pngjs').PNG;
const fs = require('fs');
describe('Visual Regression Tests', () => {
let browser, page;
beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
await page.setViewport({ width: 1200, height: 800 });
});
afterAll(async () => {
await browser.close();
});
test('Homepage renders correctly', async () => {
await page.goto('http://localhost:8080');
const screenshot = await page.screenshot();
const baseline = fs.readFileSync('test/baselines/homepage.png');
const diff = compareImages(baseline, screenshot);
expect(diff.mismatchedPixels).toBeLessThan(100);
});
test('Dark theme renders correctly', async () => {
await page.goto('http://localhost:8080');
await page.click('.theme-toggle');
await page.waitForTimeout(500); // Wait for theme transition
const screenshot = await page.screenshot();
const baseline = fs.readFileSync('test/baselines/homepage-dark.png');
const diff = compareImages(baseline, screenshot);
expect(diff.mismatchedPixels).toBeLessThan(100);
});
function compareImages(baseline, current) {
const img1 = PNG.sync.read(baseline);
const img2 = PNG.sync.read(current);
const { width, height } = img1;
const diff = new PNG({ width, height });
const mismatchedPixels = pixelmatch(
img1.data, img2.data, diff.data, width, height,
{ threshold: 0.1 }
);
return { mismatchedPixels, diff };
}
});
Accessibility Testing
// test/accessibility.test.js
const { AxePuppeteer } = require('@axe-core/puppeteer');
const puppeteer = require('puppeteer');
describe('Accessibility Tests', () => {
let browser, page;
beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
});
afterAll(async () => {
await browser.close();
});
test('Homepage meets WCAG AA standards', async () => {
await page.goto('http://localhost:8080');
const results = await new AxePuppeteer(page)
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toHaveLength(0);
});
test('Navigation is keyboard accessible', async () => {
await page.goto('http://localhost:8080');
// Test tab navigation
await page.keyboard.press('Tab');
const focusedElement = await page.evaluate(() => document.activeElement.tagName);
expect(['A', 'BUTTON', 'INPUT']).toContain(focusedElement);
});
});
Deployment and Distribution
Theme Packaging
{
"name": "custom-docfx-theme",
"version": "1.0.0",
"description": "Custom branded theme for DocFX documentation",
"main": "template/",
"files": [
"template/",
"README.md",
"LICENSE"
],
"keywords": ["docfx", "theme", "documentation"],
"repository": {
"type": "git",
"url": "https://github.com/your-org/custom-docfx-theme"
},
"scripts": {
"build": "npm run build:css && npm run build:js",
"build:css": "sass styles/main.scss template/styles/main.css --style compressed",
"build:js": "webpack --mode production",
"watch": "npm run watch:css & npm run watch:js",
"watch:css": "sass styles/main.scss template/styles/main.css --watch",
"watch:js": "webpack --mode development --watch",
"test": "jest",
"lint": "stylelint styles/**/*.scss && eslint scripts/**/*.js"
}
}
Build Pipeline
# .github/workflows/build-theme.yml
name: Build and Test Theme
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint styles
run: npm run lint:css
- name: Lint scripts
run: npm run lint:js
- name: Build theme
run: npm run build
- name: Run tests
run: npm test
- name: Visual regression tests
run: npm run test:visual
- name: Accessibility tests
run: npm run test:a11y
- name: Package theme
run: npm pack
- name: Upload theme package
uses: actions/upload-artifact@v3
with:
name: theme-package
path: "*.tgz"
Best Practices
Theme Development Guidelines
- Performance First: Optimize for fast loading and rendering
- Accessibility: Ensure WCAG AA compliance
- Responsive Design: Support all device sizes
- Progressive Enhancement: Work without JavaScript
- Maintainability: Use modern CSS features and methodologies
Brand Consistency
- Design System: Create and maintain a comprehensive design system
- Component Library: Build reusable UI components
- Style Guide: Document usage guidelines and patterns
- Version Control: Track theme changes and maintain backwards compatibility
Testing Strategy
- Visual Regression: Automated screenshot comparisons
- Accessibility: Regular WCAG compliance testing
- Performance: Monitor Core Web Vitals
- Cross-browser: Test on multiple browsers and devices
This comprehensive guide provides the foundation for creating sophisticated, branded DocFX themes that enhance the user experience while maintaining performance and accessibility standards.