CustomControl

Do not start here if Monster is still new to you. This tutorial makes sense after First Page, Templating and ideally CustomElement, because it assumes you already know how the library is loaded and how Monster separates templates, options and runtime state.

Tutorial Goal

Build a value-bearing control that behaves like a real form primitive

CustomControl is the form-aware layer on top of Monster custom elements. Use it when your component owns a value, participates in forms and needs validation or ElementInternals behavior instead of acting as a purely visual shell.

Choose the Right Base Class

Use CustomElement when

You need a standalone tag, Shadow DOM and reusable UI, but the element does not really own a form value.

Use CustomControl when

You need value handling, form association, validation and option-driven behavior. That is the right layer for select-like, toggle-like and input-like controls.

What the Base Class Gives You

ElementInternals-backed form behavior

The control can participate in forms like a real field. That is the main reason to start from CustomControl instead of layering ad-hoc hidden inputs onto a plain custom element.

Value and option infrastructure

Defaults, options, internal value handling and status hooks already exist. Extend those contracts instead of inventing parallel state containers.

Build a Minimal Control

1. Import the base class and registration helper

import {
    CustomControl,
    registerCustomElement,
} from "@schukai/monster/source/dom/customcontrol.mjs";

2. Create the class and define the tag

class MyChoice extends CustomControl {
    static getTag() {
        return "monster-my-choice";
    }
}

3. Extend defaults with value-facing options

Keep inherited behavior intact. Add value, labels and templates through the existing options surface.

class MyChoice extends CustomControl {
    static getTag() {
        return "monster-my-choice";
    }

    get defaults() {
        return Object.assign({}, super.defaults, {
            value: "off",
            templates: {
                main: "<button part="button" type="button">${label}</button>",
            },
            templateMapping: {
                label: "Disabled",
            },
        });
    }
}

4. Register the element once

registerCustomElement(MyChoice);

5. Use the tag and options in HTML

<monster-my-choice
    name="newsletter"
    data-monster-option-value="off">
</monster-my-choice>

Result

The point of this example is not the button styling. It is the contract: explicit tag, explicit registration, explicit value handling and form-aware semantics.

Live Control Demo

This demo only talks to the control through its public API. No shadow DOM internals are touched; value and state are read from the control itself.

Newsletter control

Toggle onToggle offSwap values

Public value

off

Logical state: off

The border styling comes from ::part(control), while the logic is still driven through toggleOn(), toggleOff() and setOption().

Public Value Handling

Controls must move value through public contracts. Do not mutate random internal DOM and hope the field state follows.

control.value = "on";
control.setOption("value", "on");

// Avoid: reaching into shadowRoot and toggling internals manually.

Do not split the value contract

If a control exposes one value publicly but stores a second hidden source of truth in arbitrary DOM state, validation and form submission become unreliable.

Lifecycle Hooks You Actually Need

connectedCallback()

Use it for runtime work that depends on the element being in the DOM. Always call super.connectedCallback() so the base class can finish form-related setup.

setOption() and public state hooks

Prefer option-driven updates over manual DOM drift. That keeps state transitions visible and testable.

class MyChoice extends CustomControl {
    connectedCallback() {
        super.connectedCallback();
        // add listeners or runtime setup here
    }
}

Styling Across Shadow DOM

A control can own Shadow DOM internals and still be themeable. The right approach is to expose stable parts and use Monster property styles from outside, not to pierce internals with brittle selectors.

monster-my-choice::part(button) {
    background: var(--monster-bg-color-primary-2);
    color: var(--monster-color-primary-2);
    border-radius: var(--monster-border-radius);
}

Common Mistakes

Starting from CustomElement for a real form field

If the element has value, validity and form semantics, the lighter base class is the wrong abstraction.

Using shadow internals as your public API

Once external code depends on internal DOM structure, refactors become breaking changes immediately.

Letting value and markup drift apart

If the visible selection changes but the public value does not, the control stops being reliable.

Where to Go Next

Continue into Theming

Once the control contract is clear, the next practical step is to style it through tokens, property defaults and stable parts.

Read the base class API

Continue into the reference page when you need the full option, method and contract surface.

The current width of the area is too small to display the content correctly.