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.
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
Public value
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.