130 Widgets

Building tools. Learning to build tools. Learning to build learning tools.

6. The Color Picker UI

Building a QuickPick with named presets, live swatches, and custom hue input.

The QuickPick API

VS Code’s showQuickPick is the standard way to present a list of choices to the user. It’s the same widget behind the Command Palette, file switcher, and symbol search. ColorIdentity uses it to build a color picker that adapts to the current mode.

The function signature tells the story:

export async function showColorPicker(
    currentHue: number,
    themeProfile: ThemeProfile,
    swatchDir: string,
    colorMode: ColorMode = 'simple'
): Promise<ColorPickResult | undefined>

The ColorPickResult carries the chosen hue and, in harmonized mode, the angular offset from the theme’s base hue:

export interface ColorPickResult {
    hue: number | null;
    harmonyOffset?: number;  // only set in harmonized mode
}

Three possible outcomes:

Simple Mode vs. Harmonized Mode

The picker delegates to one of two internal functions based on the colorMode setting:

Color Science

The harmony groups come from classical color theory. Analogous colors are neighbors on the wheel — they blend naturally but still provide distinction. Complementary colors sit opposite and create maximum contrast. Triadic colors are evenly spaced at 120° intervals for balanced variety. Split-complementary flanks the complement for a distinctive but less jarring effect. The themeAnalyzer.ts module handles all of this — extracting the theme’s base hue and computing the harmony suggestions.

Building the Picker Items

Each preset becomes a QuickPickItem with an icon, description, and optional “currently selected” indicator:

for (const preset of COLOR_PRESETS) {
    const previewHex = hslToHex(
        preset.hue,
        themeProfile.baseSaturation,
        themeProfile.baseLightness
    );
    const swatchUri = generateColorSwatch(swatchDir, previewHex);

    items.push({
        label: preset.label,
        description: `hue ${preset.hue}°  ·  ${previewHex}`,
        detail: currentHue === preset.hue
            ? '$(check) Currently selected' : undefined,
        iconPath: swatchUri,
        _hue: preset.hue,
    });
}

Several things happening here:

VS Code Concept

QuickPickItem supports label, description (shown to the right of the label, muted), and detail (shown below the label, smaller). The kind property can be set to QuickPickItemKind.Separator to add visual dividers between groups of items. ColorIdentity uses separators to set apart the “Automatic” option, the color presets, and the “Custom Hue” option.

Custom Hue Input

If the user picks “Custom Hue…”, the flow chains into an input box:

if ((picked as PickItem)._action === 'custom') {
    const input = await vscode.window.showInputBox({
        title: 'ColorIdentity: Custom Hue',
        prompt: 'Enter a hue value (0 = red, 120 = green, 240 = blue)',
        value: String(currentHue),
        validateInput: (value) => {
            const n = Number(value);
            if (isNaN(n) || n < 0 || n > 360) {
                return 'Please enter a number between 0 and 360';
            }
            return undefined;
        },
    });
    if (input === undefined) {
        return undefined;
    }
    return { hue: Number(input) };
}

The validateInput callback runs on every keystroke and shows an inline error message if the input is invalid. The user can’t submit an invalid value — the OK button is disabled while the validation message is visible. The value field pre-populates the input with the current hue, so the user can adjust from their current position rather than starting from scratch.

Tip

Notice the flow: QuickPick → InputBox is a two-step interaction. The user first picks from a list, and only sees the text input if they explicitly choose the custom option. This keeps the common case (picking a preset) fast, while still supporting power users who want precise control.

Checkpoint

The color picker is a dual-mode UI: simple mode for quick preset picks, harmonized mode for theme-aware suggestions grounded in color theory. Both share the same swatch rendering and custom input flow. But where do those swatch PNG files come from? That’s the most surprising part of the whole extension.