Last night I was rooting around in the cellars of a particularly large codebase and stumbled upon our normalize.css
which makes sure that all of our markup renders in a similar way across different browsers. I gave it a quick skim and found styles for a rather peculiar element called <output>
that I’d never seen or even heard of before.
According to MDN, it “represents the result of a calculation or user action” typically used in forms. And rather embarrassingly for me, it isn’t a new and fancy addition to the spec since Chris used it in a post all the way back in 2011.
But regardless! What does output
do and how do we use it? Well, let’s say we have an input with a type
of range
. Then we add an output
element and correlate it to the input with its for
attribute.
<input type="range" name="quantity" id="quantity" min="0" max="100">
<output for="quantity"></output>
See the Pen Input Output #2 by CSS-Tricks (@css-tricks) on CodePen.
It… doesn’t really do anything. By default, output
doesn’t have any styles and doesn’t render a box or anything in the browser. Also, nothing happens when we change the value of our input.
We’ll have to tie everything together with JavaScript. No problem! First we need to find our input in the DOM with JavaScript, like so:
const rangeInput = document.querySelector('input');
Now we can append an event listener onto it so that whenever we edit the value (by sliding left or right on our input) we can detect a change:
const rangeInput = document.querySelector('input');
rangeInput.addEventListener('change', function() {
console.log(this.value);
});
this.value
will always refer to the value of the rangeInput
because we’re using it inside our event handler and we can then return that value to the console to make sure everything works. After that we can then find our output
element in the DOM:
const rangeInput = document.querySelector('input');
const output = document.querySelector('output');
rangeInput.addEventListener('change', function() {
console.log(this.value);
});
And then we edit our event listener to set the value of that output
to change whenever we edit the value of the input:
const rangeInput = document.querySelector('input');
const output = document.querySelector('output');
rangeInput.addEventListener('change', function() {
output.value = this.value;
});
And voilá! There we have it, well mostly anyway. Once you change the value of the input our output will now reflect that:
See the Pen Input Output #3 by Robin Rendle (@robinrendle) on CodePen.
We should probably improve this a bit by settting a default value to our output so that it’s visible as soon as you load the page. We could do that with the HTML itself and set the value inside the output:
<output for="quantity">50</output>
But I reckon that’s not particularly bulletproof. What happens when we want to change the min or max of our input? We’d always have to change our output, too. Let’s set the state of our output in our script. Here’s a new function called setDefaultState
:
function setDefaultState() {
output.value = rangeInput.value;
}
When the DOM has finished loading and then fire that function:
document.addEventListener('DOMContentLoaded', function(){
setDefaultState();
});
See the Pen Input Output #4 by Robin Rendle (@robinrendle) on CodePen.
Now we can style everything! But there’s one more thing. The event listener change
is great and all but it doesn’t update the text immediately as you swipe left or right. Thankfully there’s a new type of event listener called input
with fairly decent browser support that we can use instead. Here’s all our code with that addition in place:
const rangeInput = document.querySelector('input');
const output = document.querySelector('output');
function setDefaultState() {
output.value = rangeInput.value;
}
rangeInput.addEventListener('input', function() {
output.value = this.value;
});
document.addEventListener('DOMContentLoaded', function() {
setDefaultState();
});
See the Pen Input Output #5 by Robin Rendle (@robinrendle) on CodePen.
And there we have it! An input, with an output.
I knew the element already but it raised the question: “Whats its purpose at all!?” Why not use a
<div>
or a or even any other text-based container? Semantics? Hmm, Google won’t read the dynamic content so no use for that I suppose.If it would put out the value without the need of JS, for example a binding and functionality via “for” attribute like a delegated click in element, that would be helpful.
But just another functionless container? Nah, me no like.
It’s for accessibility — it’s allegedly auto-connected to the inputs you indicate with its
for
attribute, and most importantly, it can be<label>
ed.If this provided any baked-in interconnectivity where say… you could modify the final value either via the range slider or by modifying the value of
output
this would be fantastic; because good UX already achieves this through a combination ofrange
andinput
.But you’re right… this is… nothing. Yay it adds clarity for a developer scouring through your code, but at the end of the day it’s just as effective to use any other element which is a shame. Seems like a real missed opportunity.
would make a lot more sense as:
Mind to share why?
@pat, I think reason why is just a small memory efficiency — there’s no need to create anonymous function to call
setDefaultState
(and do nothing else) when you could just passsetDefaultState
in directly.I would do this:
That’s the smartest way of doing it, imho
This is the smart way to do it a.k.a the DRY-way.
I’ve used it to create tooltips for range slider thumbs.
The structure I use is a wrapper element with a range
input
, alabel
and anoutput
(the last two being tied to theinput
via thefor
attribute). I generate this structure with Pug so that I can put the minimum, the maximum and the current value of the range into variables (you’ll see in a moment why):In the CSS, I set
display: flex
andflex-direction: column-reverse
on the wrapper, so that thelabel
shows up before theinput
. I then setposition: relative
on this wrapper andposition: absolute
on theoutput
. I also make sure that both theinput
and its track have the samewidth
as the wrapper.At this point, setting
left: 0
on theoutput
would make its left edge align with the left edge of the wrapper, which coincides with the left edge of theinput
. Settingleft: 100%
would make its left edge align with the right edge of its parent (the wrapper), which coincides with the right edge of theinput
. If we also settransform: translate(-50%)
, then its middle vertical axis gets aligned with the left edge of theinput
(theleft: 0
case) or with the right edge of theinput
.But if we want it to act like a tooltip for the range thumb, then it needs to move within the same limits as the thumb. The thumb moves within the limits of the track‘s
content-box
in Chrome and within the limits of the input‘scontent-box
in Firefox and Edge. But if we have the samewidth
on the wrapper, actual range input and its range track, nopadding
, noborder
and also nomargin
on the inner ones… then thecontent-box
is the same for all three of them.This means that the central vertical axis of the thumb moves between half a thumb width to the right of the left edge of the input and half a thumb width to the left of the right edge of the input.
So we set the
width
of the wrapper (inherited by both theinput
and its track) to the distance between these two endpoints ($d
) plus the thumb width ($tw
) because we have half a thumb witdth at one end and a second half at the other end. We also set theleft
for theoutput
to half a thumb width (.5*$tw
):We then compute the numeric range of motion (difference between the max and min of the input), the starting position (
--pos
) of theoutput
element using the CSS variables set inline and then we add this position value to in thetranslate()
function:Finally, in the JS, we need to update
--val
and theoutput
‘s value whenever theinput
‘s value changes:This makes the
output
move along with the thumb of the range. No other JS needed for positioning, all done with a bit of CSS variable magic.You can see a fully working demo of this (or if video is your thing, you can see me embarrass myself live coding the thing at CodePenDay Hamburg… apparently, I can’t spell attribute names).
It didn’t work in Edge at the time due to a bug I’ve been told has been fixed in this month’s release.
Nice!
Lovely. Why listen/react to both “change” and “update” events?
It makes SR read the updated value twice.
Here it is in React, just because:
Nice – Operator Mono, great font for coding!
One other nice thing about the element is that it is implicitly a “live region” so whenever the range value changes, SR users hear the updated value.
We can rely on the explicit relationship between
output
/input
and bind all of them dynamically…