<!-- demo:embed example="extensions/3-custom-control" -->

In [Build a custom Element](/docs/09-element-dev/) you wired your Countdown's settings panel from existing controls (`TextControl`). Sometimes the shipped catalogue isn't enough — your brand needs a colour picker that pre-loads brand swatches, a slider that emits `px` AND `em`, a dropdown whose options depend on another field. Then you author a Control. **Time budget: 45 minutes.**

## What a Control IS

A Control is **a settings-panel widget that mutates one or more attributes / formats of the currently-selected element**. The shipped catalogue (≈ 40 control types — `TextControl`, `SelectControl`, `ColorPickerControl`, `DimensionControl`, `BorderControl`, …) lives in `src/includes/<Name>Control.js`.

Two paths to author a new one:

- **Subclass an existing Control** — recommended. You get free wiring (label, container DOM, focus, the `_applyValue` syncing). Start here.
- **Subclass `BaseControl` directly** — only when the existing controls have a fundamentally different input model than what you need (rare).

This doc walks the canonical pattern: a Brand Color Picker that subclasses `ColorPickerControl` and adds 6 quick-access brand swatches.

## Pre-flight

Confirm the demo runs and the canonical example renders:

<!-- buyer:try -->
```bash
php -S localhost:8000 -t demo/
```

Open <http://localhost:8000/examples/extensions/3-custom-control/>. You should see a Brand Badge element with a custom colour picker exposing 6 preset brand swatches. Every step below reproduces what's running there.

## Step 1 — Subclass

<!-- buyer:try -->
```javascript
class BrandColorPickerControl extends ColorPickerControl {
    constructor(label, value, onChange, options = {}) {
        super(label, value, onChange);
        this._brandSwatches = options.swatches || [
            '#5b21b6', // primary
            '#a855f7', // accent
            '#10b981', // success
            '#f59e0b', // warn
            '#ef4444', // error
            '#0f172a'  // ink
        ];
    }
}
```

The constructor accepts the same `(label, value, onChange)` signature as the parent — so the buyer can drop your control into any element's `getControls()` without learning new wiring. Extra options ride on a final `options` arg.

## Step 2 — Override `render()`

The shipped `ColorPickerControl.render()` builds the standard label + colour swatch + popover. We extend it: call super first, then append the brand-swatches strip.

<!-- buyer:try -->
```javascript
class BrandColorPickerControl extends ColorPickerControl {
    /* …constructor… */

    render() {
        super.render();

        // The popover element exists on the parent after super.render().
        const popover = this.popoverEl || this.popover;
        if (!popover) return;

        const strip = document.createElement('div');
        strip.className = 'brand-swatches';
        strip.style.cssText = 'display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px; padding: 8px;';

        this._brandSwatches.forEach((hex) => {
            const btn = document.createElement('button');
            btn.type = 'button';
            btn.title = hex;
            btn.style.cssText = `width: 24px; height: 24px; border-radius: 4px; background: ${hex}; border: 1px solid rgba(0,0,0,0.1); cursor: pointer;`;
            btn.addEventListener('click', () => {
                this._applyValue(hex);
            });
            strip.appendChild(btn);
        });

        popover.appendChild(strip);
    }
}
```

Two patterns to lock in:

1. **Always call `super.render()` first.** The parent builds the DOM your additions live inside.
2. **Always route value changes through `_applyValue`** — the canonical entry point that syncs the control's internal state (the visible hex input, the swatch preview, the parent's `value` property, AND the `onChange` callback the buyer passed in). The footgun: there's also `_notify(value)` which is the change-event path; it fires the callback but DOESN'T sync the visible UI. Use `_applyValue`. (W6.C.13 lesson.)

## Step 3 — Wire it into an Element

You don't register custom Controls anywhere — they're instantiated inline inside an Element's `getControls()`:

<!-- buyer:try -->
```javascript
class BrandBadgeElement extends BaseElement {
    /* …getData / parse / _doRender / requiredTemplateKeys… */

    getControls() {
        return [
            new TextControl('Label', this.label, (v) => {
                this.label = v;
                this.notifySyncListeners();
            }),
            new BrandColorPickerControl(
                'Brand color',
                this.brand_color,
                (v) => {
                    this.brand_color = v;
                    this.notifySyncListeners();
                },
                { swatches: ['#5b21b6', '#a855f7', /* … */] }
            ),
        ];
    }
}
```

The buyer sees one extra widget in the settings panel — labelled "Brand color", showing the standard color picker UI plus your 6-swatch strip below. Click any swatch and the canvas updates instantly via `notifySyncListeners()`.

## Step 4 — There's no `Builder.registerControl()`

Custom Controls aren't registry-based today (W6.C.13 lesson). You instantiate them inline inside `getControls()`. That works for any project where the Control's constructor args are statically known.

A future engine API (`Builder.registerControl(name, klass)`) would unlock JSON-described custom panels — buyers would author `getControls()` in JSON instead of JS — but that's a CD-2 follow-up, not shipped today. The roadmap for engine-side improvements is tracked in the BuilderJS repo's open issues.

## Step 5 — Test

<!-- buyer:verify -->
1. Open the builder with a sample that uses your `BrandBadgeElement`.
2. Click the badge → settings panel shows your `BrandColorPickerControl`.
3. Click any of the 6 swatches → the canvas updates with the new colour.
4. Open DevTools → Console: `builder.getData()`. Confirm your `brand_color` value is the hex you clicked.
5. Reload the page → the badge re-renders with the saved colour (round-trip).

If step 3 paints the canvas but step 5 forgets the colour, your `getData()` is missing the `brand_color` field. Re-read [Build a custom Element](/docs/09-element-dev/) — every domain attribute must be in `getData()` AND `parse()`.

## Did you make it?

You should now have:

- ✅ A `BrandColorPickerControl` class subclassing `ColorPickerControl`.
- ✅ Override `render()` that calls `super.render()` first, then appends the brand strip.
- ✅ Each swatch click routes through `this._applyValue(hex)` — NOT `_notify(hex)`.
- ✅ The custom Control instantiated inline inside an Element's `getControls()`.
- ✅ The round-trip: setting a swatch persists across save / load.

If a step didn't work:

- 🩹 *Click on swatch updates the visible hex input but the actual saved value is stale* → you used `_notify` instead of `_applyValue`. They look similar; `_applyValue` is the syncing entry.
- 🩹 *Custom Control doesn't render at all* → confirm you called `super.render()` first. The parent builds the container your `popover` element lives inside.
- 🩹 *I want to subclass `BaseControl` directly, not an existing Control* → that's the rare path; `BaseControl` is mostly an interface (label, value, container, callback). Read `src/includes/BaseControl.js` line by line.

What's next?

- → [Build a custom Widget](/docs/11-widget-dev/) — palette items the buyer drags onto the canvas.
- 🔗 [`examples/extensions/3-custom-control/`](/examples/extensions/3-custom-control/) — runnable Brand Color Picker end-to-end.
- 🔗 `src/includes/ColorPickerControl.js` — canonical parent + the `_applyValue` vs `_notify` contract.
- 🔗 `docs/core/CONTROL_DEFINITION.md` — the full control catalogue (≈ 40 shipped types).
