Building tools. Learning to build tools. Learning to build learning tools.
Building a QuickPick with named presets, live swatches, and custom hue input.
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:
{ hue: 220 } — user picked a specific hue (simple mode preset or custom).{ hue: 245, harmonyOffset: 25 } — user picked an analogous warm color in harmonized mode. The offset is stored so the color adapts on theme change.{ hue: null } — user picked “Automatic.”undefined — user dismissed the picker without choosing.
The picker delegates to one of two internal functions based on the colorMode setting:
showSimplePicker) — shows 14 fixed color presets
spaced around the color wheel: Red, Orange, Yellow, Lime, Green, Mint, Teal, Cyan, Blue, Indigo,
Purple, Magenta, Pink, Rose. These are absolute hues that don’t change with the theme.
showHarmonizedPicker) — reads the theme’s
base hue from themeAnalyzer.ts and generates suggestions grouped by color harmony:
Analogous (±25°, ±40°), Complementary (~180°), Triadic (±120°),
and Split-Complementary (±150°). Each suggestion carries an offset from
the theme base, which gets persisted so the color relationship adapts on theme change.
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.
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:
generateColorSwatch creates a 16×16
solid-color PNG file and returns its URI. QuickPick items support iconPath
for displaying small images. We’ll see how these PNGs are generated in Section 7.
$(check) syntax is a VS Code
Codicon —
a built-in icon set. It renders as a checkmark in the detail line.
_hue is a custom property added via
a type intersection. It piggybacks the hue value on the QuickPick item so we can
retrieve it when the user makes a selection.
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.
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.
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.
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.