Input Impedance

19 Apr 2026

There and back again

#Pick a number


Type some numbers into the text box. Try: 3, -1, .1, 5e-10, -0.5e-3.

Notice how the validation state changes with your keystrokes. Typing out a number character by character may produce intermediate strings that cannot be parsed as numbers. To enter -5, you have type - first (unless you want to type 5 -, but that's not very ergonomic). The substring -0.5e- is not a valid number, but the full string -0.5e-3 is.

For a form that's submitted once, this is not a huge problem. Just forbid the submission until all form inputs are valid. But if the number controls some property that is supposed to update immediately, this is very inconvenient. Each time the text changes, we have to run textToValue(), a function of type string -> number | null. Since we can't set a numeric property to null, we have to filter out those updates.

In other words: There are valid UI states (partially entered numbers) that do not map to valid internal states.

#Pick another number


By using a slider, we avoid the string parsing entirely. Re-mapping the (sub-)pixel offset of the slider handle into the desired range [-100.0, 100.0] directly produces the value:

// Constants
const PIXEL_RANGE = SLIDER_RIGHT - SLIDER_LEFT; // pixels
const VALUE_RANGE = SLIDER_MAX - SLIDER_MIN;    // unitless
const PIXEL_TO_VALUE = VALUE_RANGE / PIXEL_RANGE;

function sliderToValue(handlePos: number): number {
    // Transform the slider handle position into the slider's local coordinate system [0, PIXEL_RANGE]
    const handleOffset = handlePos - SLIDER_LEFT;

    // Scale to value scale [0, VALUE_RANGE]
    const valueOffset = handleOffset * PIXEL_TO_VALUE;

    // Translate to output range [SLIDER_MIN, SLIDER_MAX]
    return SLIDER_MIN + valueOffset;
}

This is just a linear transform, which is a much nicer operation than parsing strings. Both input and output are of type number. Each possible input yields a valid output. Therefore, all UI states produce valid internal states, as long as we correctly clamp the pixel values of the slider correctly. If we expand this function from scalar to vector values, we can even read multiple values at the same time:

2D point picker

Combining the ability to quickly slide through different input values with immediate state updates and feedback can be quite powerful. Color pickers and similar input elements where instant feedback matters often use this approach (try it).

This speed does come at a cost, though: Entering an exact number requires a high degree of precision, or may even be impossible. Even though the slider may look like an interval on the number line R, we're really limited by pixel sizes, digitizer resolutions, and IEE 754 floating point precision. So the type of sliderToValue is actually not number -> number, but i32 -> f32. The output will be discrete, not continuous, and we can't select values between two pixels. Whether that's a problem depends on the specific application.

#Inverting the flow

So far, we looked at the mapping of UI state to internal state. Let's also look at the inverse: building the UI from the internal state. For simplicity's sake, we'll limit our internal state to just a single number.

The sliderToValue() function is invertible. For each state value, we can compute the corresponding slider handle position. That means we can restore the entire UI state from just our internal state. There is a direct bijection between UI and state. We can form a closed loop:

let state = initialState();

while (true) {
    state = runUI(state, inputEvents());
}

That's not possible for the text box case. To reproduce the UI state, we need to store the string contents (and cursor position) of the input along with our state. Our loop would look more like this:

let state = initialState();
let uiState = uiFromState(state);

while (true) {
    let maybeState = null;
    [maybeState, uiState] = runUI(state, uiState, inputEvents());
    if (maybeState) {
        state = maybeState;
    }
}