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.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
How to Build a VSCode Extension that Visualizes All Project Colors

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

WebView

can 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.lock

The 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.

WebViewExtensionregexcolor visualization
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.