An HTML component holds two buttons, Cancel and Submit, each with its own Click event handler. Clicking Cancel runs both handlers. The same happens with inputs: a change in either text field fires every Change handler on the component. Strip the markup down to one button, and its Click handler does not fire at all.
The cause is one attribute. Each interactive element needs a data-click-target or data-change-target value, and each handler needs a condition that checks it. This note covers why, the pattern that routes every event to the right handler, and what to check when it still misbehaves.
Why use the HTML component at all
Retool's built-in components cover most screens, and they are the right default: tables, forms, filters, and standard button rows are quicker to build with them and easier to maintain. The HTML component is for layouts that need full control of markup and CSS. A hero section with a full-bleed background image and action buttons laid over it is a typical case.
Our rule of thumb: built-in components when their layout fits, the HTML component when the design needs control over every element. Once buttons and inputs live inside that HTML, they need a way to tell Retool which one was used.
How events reach Retool
The HTML component exposes two event types to the inspector: Click and Change. Every handler of a type listens to the whole component. So with five buttons and five Click handlers, any click reaches all five handlers.
To tell them apart, Retool reads a data attribute on the element that raised the event:
data-click-targeton buttons and other clickable elements.data-change-targeton inputs, selects, and checkboxes.
Retool passes the attribute's value to the handler as target.type. The handler's "Only run when" condition compares it against the value it should respond to. The attribute is also required for a handler to run at all, so a single button without one triggers nothing.
The pattern
Give each element its own target value:
<div>
<input
type="text"
data-change-target="Change Action 1"
placeholder="Enter value..."
/>
<input
type="text"
data-change-target="Change Action 2"
placeholder="Enter value..."
/>
<button data-click-target="Click Action 1">Cancel</button>
<button data-click-target="Click Action 2">Submit</button>
</div>Then, in the inspector, give each handler an "Only run when" condition that matches one value exactly:
// Click handler for Cancel
{{ target.type === 'Click Action 1' }}
// Click handler for Submit
{{ target.type === 'Click Action 2' }}
// Change handler for the first input
{{ target.type === 'Change Action 1' }}
// Change handler for the second input
{{ target.type === 'Change Action 2' }}Each handler now runs only for its own element. The strings must match exactly, including case and spaces.
What the target object carries
Inside any HTML component event handler, target is in scope with three properties:
type, the string set in thedata-click-targetordata-change-targetattribute.value, the current value of an input.checked, the state of a checkbox.
That makes the handler useful beyond routing. A Change handler can write the input's value straight into app state:
<label>
<input type="checkbox" data-change-target="Urgent" />
Mark as urgent
</label>
<input type="text" data-change-target="Note" placeholder="Add a note" />// Change handler: Set variable isUrgent, only run when
{{ target.type === 'Urgent' }}
// Value
{{ target.checked }}
// Change handler: Set variable note, only run when
{{ target.type === 'Note' }}
// Value
{{ target.value }}Sharing a handler
Two elements can share a target value on purpose. If a header button and a footer button should both submit, give both data-click-target="Submit". One handler with {{ target.type === 'Submit' }} then serves both, with no duplicated logic.
A layout example
This is the kind of markup that justifies the HTML component: a hero with a background image and two actions over it.
<section class="hero">
<h2>Quarter-end review</h2>
<p>Items waiting for a decision this week.</p>
<button data-click-target="Open queue">Open the queue</button>
<button data-click-target="Export">Export as CSV</button>
</section>.hero {
min-height: 240px;
padding: 32px;
background: url("https://example.com/cover.jpg") center / cover;
color: #ffffff;
}
.hero button {
margin-right: 8px;
}Two Click handlers, one per target value, run the queue query and the export.
When handlers still misbehave
Nothing fires. Check that the element has a
data-click-targetordata-change-targetattribute.Everything fires. Check that each handler has its own "Only run when" condition. One missing condition is enough.
The wrong one fires. Compare the strings character by character. A trailing space is enough to break a match.
A Change handler sees the wrong value. Read
target.valueortarget.checkedinside the handler, rather than a value captured elsewhere.
We found the attribute through a thread on the Retool community forum and confirmed the behavior by testing. The forum is worth searching early when the HTML component behaves unexpectedly.
Our view
We recommend giving every interactive element in an HTML component its own target value from the start, even when there is only one. That keeps each handler tied to one element, because the condition, not the order of handlers, decides what runs. For screens that do not need custom layout, use Retool's built-in components, because they handle events without any of this wiring.
When the layout needs more than HTML and CSS can give, a custom component is the next step. What is a custom Retool component covers when that is worth it, and our open-source components are on GitHub.