How to Build a VSCode Extension that Visualizes All Project Colors
This article walks through the complete process of creating a VSCode extension that scans the workspace for color values, visualizes them in a grid, supports Hex and RGB formats, and provides quick copy and refresh features using WebView panels and TypeScript.
Background
During project development you may need to find a specific color value, but colors are just numbers without semantics, making global search difficult. A tool that quickly displays all colors in the working directory is therefore useful.
Since the tool is needed while coding, an editor plugin is the most convenient solution. This article demonstrates how to develop a VSCode plugin for that purpose.
Requirement Analysis
The plugin should collect all colors in the workspace, update when files change, visualize the colors in a grid, and allow users to click a color to see its Hex and RGB values and copy them with one click.
Collect all colors in the workspace
Visualize all color blocks
Show and copy color values
UI design is shown below:
Implementation
VSCode WebView
WebViewcan be used to create a custom view, essentially an Iframe container that can render any HTML and communicate via message passing, providing powerful rendering and interaction capabilities.
VSCode provides three APIs to create a WebView:
window.createWebviewPanel
Creates an editor panel that is disposed when the editor is closed, suitable for non‑persistent UI.
const panel = vscode.window.createWebviewPanel('webviewId', 'My Webview', vscode.ViewColumn.One, { enableScripts: true });
panel.webview.html = "<html><body><h1>Hello, Webview!</h1></body></html>";window.registerWebviewPanelSerializer
Defines how to serialize and deserialize the panel state so it can be restored after VSCode restarts.
class MyWebviewPanelSerializer implements vscode.WebviewPanelSerializer {
async deserializeWebviewPanel(webviewPanel, state) {
webviewPanel.webview.html = "<html><body><h1>Restored Webview</h1></body></html>";
}
}
context.subscriptions.push(vscode.window.registerWebviewPanelSerializer('webviewId', new MyWebviewPanelSerializer()));window.registerWebviewViewProvider
Creates a persistent view that lives in the sidebar or panel.
class MyWebviewProvider implements vscode.WebviewViewProvider {
resolveWebviewView(webviewView) {
webviewView.webview.html = "<html><body><h1>Hello, Sidebar Webview!</h1></body></html>";
}
}
const provider = new MyWebviewProvider();
context.subscriptions.push(vscode.window.registerWebviewViewProvider('myWebview', provider));For this plugin the view does not need to be persistent, so createWebviewPanel is used.
Project Initialization
The project is scaffolded using the official VSCode extension generator. The directory structure includes typical files such as .eslintrc.json, .gitignore, package.json, src/extension.ts, etc.
.eslintrc.json
.gitignore
.vscode
extensions.json
launch.json
settings.json
tasks.json
.vscode-test.mjs
.vscodeignore
.yarnrc
CHANGELOG.md
README.md
package.json
src
extension.ts
test
extension.test.ts
tsconfig.json
vsc-extension-quickstart.md
webpack.config.js
yarn-error.log
yarn.lockThe main entry in package.json points to dist/extension.js, which is the compiled output of src/extension.ts.
Running yarn watch enables hot‑reloading; pressing F5 or selecting “Run → Start Debugging” launches a new VSCode window where the command can be invoked via the command palette.
After modifying code, the debug window can be refreshed with Ctrl+R (or Command+R on macOS).
Integrate WebView
Add colorToSee Command
In package.json a new command extension.colorToSee is added with the title
"ColorToSee: Show colors of the working directory in a webview panel".
"commands": [
{
"command": "extension.colorToSee",
"title": "ColorToSee: Show colors of the working directory in a webview panel"
}
]The command registration in extension.ts calls registerWebviewViewProvider(context) to set up the UI.
vscode.commands.registerCommand(COMMAND_NAME, () => {
registerWebviewViewProvider(context);
});Create WebView to Display Custom UI
Using window.createWebviewPanel a custom HTML page is rendered. The HTML is generated with a nonce for CSP.
const panel = vscode.window.createWebviewPanel(
CatCodiconsPanel.viewType,
"Cat Codicons",
column || vscode.ViewColumn.One,
{ enableScripts: true }
);
panel.webview.html = _getHtmlForWebview(panel.webview, extensionUri);
function _getHtmlForWebview(webview) {
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your View</title>
</head>
<body>
<h1>Hello from Your View!</h1>
</body>
</html>`;
}
function getNonce() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}The provider class handles rendering and updating the view.
const registerWebviewViewProvider = (context) => {
const provider = new ViewProvider(context.extensionUri, config);
const panel = vscode.window.createWebviewPanel(
ViewProvider.viewType,
PANEL_TITLE,
vscode.ViewColumn.One,
{ enableScripts: true }
);
provider.resolveWebviewView(panel as unknown as vscode.WebviewView);
context.subscriptions.push(panel);
};Page Rendering
The UI is driven by a state object colorInfos that stores color value, file path, and position.
export type ColorItem = {
/** start offset */
start: number;
/** end offset */
end: number;
/** color value */
color: string;
/** file path */
file: string;
};The main rendering function creates a grid of color blocks:
function generateMainDiv(colors) {
return colors.map(info => `<div style="color: ${info.color}" data-colorItem="${encodeURIComponent(JSON.stringify(info))}">${info.color}</div>`).join('');
}The HTML is injected into the WebView:
this._view.webview.html = this._getHtmlForWebview(this._view.webview);
function _getHtmlForWebview(webview) {
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your View</title>
</head>
<body>
${generateMainDiv(this.colorInfos)}
</body>
</html>`;
}Color Matching Strategies
Supported color formats include RGB/A, HSL/A, Hex, and named keywords. Regular expressions are used for matching.
RGB and HSL
const colorRegex = /((rgb|hsl)a?(\\s*[\d]*.?[\d]+%?\s*(?<commaOrSpace>\s|,)\s*[\d]*.?[\d]+%?\k<commaOrSpace>\s*[\d]*.?[\d]+%?(\s*(\k<commaOrSpace>|/)\s*[\d]*.?[\d]+%?)?\s*))/gi;Color Keywords
(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)Hex Colors
const colorHex = /.?((?:#|\b0x)([a-f0-9]{6}([a-f0-9]{2})?|[a-f0-9]{3}([a-f0-9]{1})?))\b/gi;Two strategies are used: text‑based regex matching and AST analysis (the plugin chooses regex for simplicity).
Color Extraction
Files are traversed, their text is read, and each strategy is applied to collect color information.
for (const file of files) {
try {
const document = await vscode.workspace.openTextDocument(file);
const instance = await this.findOrCreateInstance(document);
colorsInfos.push(await instance.getColorInfo());
} catch {
continue;
}
}The getColorInfo method runs all strategies and resolves the result.
getColorInfo(document = this.document) {
const text = this.document.getText();
const file = this.document.uri.fsPath;
const result = await Promise.all(this.strategies.map(fn => fn(text)));
return resolveResult(result);
}Page Update
The UI updates when files are added, deleted, or edited. A refresh button sends a message from the WebView to the extension, which then rescans files and updates colorInfos.
// WebView side
refreshBtn.addEventListener('click', () => {
if (refreshBtn.classList.contains('btn--loading')) return;
refreshBtn.classList.add('btn--loading');
vscode.postMessage({ command: 'refresh' });
});
// Extension side
webviewView.webview.onDidReceiveMessage(message => {
switch (message.command) {
case 'refresh':
const prom = () => this.doUpdateWebView();
prom().finally(() => {
webviewView.webview.postMessage({ command: 'refreshEnd' });
});
break;
}
});The doUpdateWebView method either re‑initializes data for full scans or updates only changed documents.
private async doUpdateWebView() {
try {
if (this.type === 'init' || this.type === 'add' || this.type === 'delete') {
await this.initDataView();
return Promise.resolve();
}
// Incremental update for edited files
for (let index = 0; index < this.instanceMap.length; index++) {
const instance = this.instanceMap[index];
if (instance.changed) {
const colorDocumentItem = await instance.getColorInfo();
this.colorMapArray[index] = colorDocumentItem;
instance.changed = false;
}
}
this.colorInfos = updateColorInfosByMap(this.colorMapArray);
this._view.webview.html = this._getHtmlForWebview(this._view.webview);
return Promise.resolve();
} catch {
return Promise.reject();
}
}Plugin Configuration
The extension allows users to configure which files are scanned via include and exclude glob patterns.
"color-to-see.findFilesRules": {
"default": {
"include": [
"**/*.js",
"**/*.jsx",
"**/*.tsx",
"**/*.css",
"**/*.less",
"**/*.sass",
"**/*.html",
"**/*.vue"
],
"exclude": [
"**/node_modules/**",
"**/dist/**",
".git"
]
}
}The configuration is read with vscode.workspace.getConfiguration and used in workspace.findFiles to obtain the file list.
config = vscode.workspace.getConfiguration(EXTENSION_NAME);
const findFilesUsingConfig = async (config) => {
const { include, exclude } = config.findFilesRules;
const includePattern = `{${include.join(',')}}`;
const excludePattern = `{${exclude.join(',')}}`;
try {
const files = await vscode.workspace.findFiles(includePattern, excludePattern);
return files;
} catch {
return [];
}
};Limitations and Optimizations
File Scan Efficiency
Scanning the entire workspace can be resource‑intensive. Optimizations include bundling code to reduce the number of files and using caching with incremental updates.
User Experience
Long scans block the UI. Implementing asynchronous scanning that updates the UI as soon as new colors are found improves responsiveness.
Color Jump Feature
The current implementation records only the first occurrence of a color and does not scroll to the exact position. Adding a configurable option to enable/disable this feature and improving navigation would enhance usability.
Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
