{"id":3569,"date":"2024-08-21T09:20:29","date_gmt":"2024-08-21T14:20:29","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=3569"},"modified":"2026-02-23T22:20:58","modified_gmt":"2026-02-24T03:20:58","slug":"custom-range-slider-using-anchor-positioning-scroll-driven-animations","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/custom-range-slider-using-anchor-positioning-scroll-driven-animations\/","title":{"rendered":"Custom Range Slider Using Anchor Positioning &amp; Scroll-Driven Animations"},"content":{"rendered":"\n<p><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_anchor_positioning\">Anchor positioning<\/a>&nbsp;and&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_scroll-driven_animations\">s<\/a><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_scroll-driven_animations\">croll-<\/a><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_scroll-driven_animations\">d<\/a><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_scroll-driven_animations\">riven animations<\/a>&nbsp;are among of the most popular and exciting CSS features of 2024. They unlock a lot of possibilities, and will continue to do so as browser support improves and developers get to know them.<\/p>\n\n\n\n<p>Here is a demo of a custom range slider where I am relying on such features.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_MWdmZPL\" src=\"\/\/codepen.io\/anon\/embed\/MWdmZPL?height=450&amp;theme-id=1&amp;slug-hash=MWdmZPL&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed MWdmZPL\" title=\"CodePen Embed MWdmZPL\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>This whole UI is a semantic HTML&nbsp;<code>&lt;input type=\"range\"&gt;<\/code>, with another semantic&nbsp;<code>&lt;output&gt;<\/code>&nbsp;element showing off the current value, along with quite fancy CSS.<\/p>\n\n\n\n<p>Intuitively, you may think there is a JavaScript code somewhere gathering the value of the input \u201con change\u201d and updating the position\/content of the tooltip. As for the motion, it\u2019s probably a kind of JavaScript library that calculates the speed of the mouse movement to apply a rotation and create that traction illusion.<\/p>\n\n\n\n<p>Actually, there is no JavaScript at all.<\/p>\n\n\n\n<p>It\u2019s hard to believe but CSS has evolved in a way that we can achieve such magic without any scripts or library. You will also see that the code is not that complex. It\u2019s a combination of small CSS tricks that we will dissect together so follow along!<\/p>\n\n\n\n<p class=\"learn-more\">At the time of writing, only Chrome (and Edge) have the full support of the features we will be using.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequisites\">Prerequisites<\/h2>\n\n\n\n<p>First, let\u2019s start with the HTML structure:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n  Label\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"range\"<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"one\"<\/span> <span class=\"hljs-attr\">min<\/span>=<span class=\"hljs-string\">\"0\"<\/span> <span class=\"hljs-attr\">max<\/span>=<span class=\"hljs-string\">\"120\"<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">\"20\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">output<\/span> <span class=\"hljs-attr\">for<\/span>=<span class=\"hljs-string\">\"one\"<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">\"--min: 0;--max: 120\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">output<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>  <\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>An&nbsp;<code>input<\/code>&nbsp;element and an&nbsp;<code>output<\/code>&nbsp;element are all that we need here. The label part is not mandatory for the functionality, but form elements should always be labelled and you need a wrapper element anyway.<\/p>\n\n\n\n<p>I won\u2019t detail the attributes of the&nbsp;<code>input<\/code>&nbsp;element but note the use of two CSS variables on the&nbsp;<code>output<\/code>&nbsp;element that should have the same values as the <code>min<\/code> and <code>max<\/code> attributes.<\/p>\n\n\n\n<p>In addition to the HTML code, I am going to consider the styling of the range slider and the tooltip as prerequisites as well. I will mainly focus on the new features and skip most of the aesthetic parts, although I have covered some of those aspects in other articles, <a href=\"https:\/\/www.sitepoint.com\/css-custom-range-slider\/\">like here<\/a> where I detail the styling of the range slider.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_KKGpmGE\" src=\"\/\/codepen.io\/anon\/embed\/KKGpmGE?height=450&amp;theme-id=47434&amp;slug-hash=KKGpmGE&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed KKGpmGE\" title=\"CodePen Embed KKGpmGE\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>As for the tooltip, I have a&nbsp;<a href=\"https:\/\/css-generators.com\/tooltip-speech-bubble\/\">big collection of 100 different tooltip shapes<\/a>&nbsp;and I am going to use the #41 and #42. I also have a&nbsp;<a href=\"https:\/\/www.smashingmagazine.com\/2024\/03\/modern-css-tooltips-speech-bubbles-part1\/\">two-part article<\/a>&nbsp;detailing the creation of most of the tooltips.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"989\" height=\"307\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/08\/s_3FEC3DE430682F2526F00121B6B9A21346D04C3B20F98BA491C64590EEBAAF65_1723504367208_image.png?resize=989%2C307&#038;ssl=1\" alt=\"\" class=\"wp-image-3571\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/08\/s_3FEC3DE430682F2526F00121B6B9A21346D04C3B20F98BA491C64590EEBAAF65_1723504367208_image.png?w=989&amp;ssl=1 989w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/08\/s_3FEC3DE430682F2526F00121B6B9A21346D04C3B20F98BA491C64590EEBAAF65_1723504367208_image.png?resize=300%2C93&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/08\/s_3FEC3DE430682F2526F00121B6B9A21346D04C3B20F98BA491C64590EEBAAF65_1723504367208_image.png?resize=768%2C238&amp;ssl=1 768w\" sizes=\"auto, (max-width: 989px) 100vw, 989px\" \/><\/figure>\n\n\n\n<p>You don&#8217;t <em>need<\/em> the fancy styled tooltip output, nor do you need the custom styling of the range slider itself, it&#8217;s just fun and offers some visual control you might want. Here&#8217;s a &#8220;naked&#8221; demo without all that:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_oNrojEJ\/9074b7f5dd43d82d5bae3f99b46c5d3d\" src=\"\/\/codepen.io\/anon\/embed\/oNrojEJ\/9074b7f5dd43d82d5bae3f99b46c5d3d?height=450&amp;theme-id=47434&amp;slug-hash=oNrojEJ\/9074b7f5dd43d82d5bae3f99b46c5d3d&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed oNrojEJ\/9074b7f5dd43d82d5bae3f99b46c5d3d\" title=\"CodePen Embed oNrojEJ\/9074b7f5dd43d82d5bae3f99b46c5d3d\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-tooltip-position\">The Tooltip Position<\/h2>\n\n\n\n<p>The first thing we are going to do is to correctly place the tooltip above (or below) the thumb element of the slider. This will be the job of Anchor positioning and here is the code:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">input<\/span><span class=\"hljs-selector-attr\">&#91;type=<span class=\"hljs-string\">\"range\"<\/span> i]<\/span><span class=\"hljs-selector-pseudo\">::-webkit-slider-thumb<\/span> {\n  <span class=\"hljs-attribute\">anchor-name<\/span>: --thumb;\n}\n<span class=\"hljs-selector-tag\">output<\/span> {\n  <span class=\"hljs-attribute\">position-anchor<\/span>: --thumb;\n  <span class=\"hljs-attribute\">position<\/span>: absolute;\n  <span class=\"hljs-attribute\">position-area<\/span>: top; <span class=\"hljs-comment\">\/* or bottom *\/<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>That\u2019s all! No more than four CSS declarations and our tooltip is correctly placed and will follow the movement of the slider thumb.<\/p>\n\n\n\n<p>Anchor positioning is an upgrade of&nbsp;<code>position: absolute<\/code> here. Instead of positioning the element relatively to an ancestor having&nbsp;<code>position: relative<\/code>&nbsp;we can consider any element on the page called an \u201canchor\u201d. To define an anchor we use&nbsp;<code>anchor-name<\/code>&nbsp;with whatever value you want. It\u2019s mandatory to use the dashed indent notation like with custom properties. That same value can later be used within the absolute element to link it with the \u201canchor\u201d using&nbsp;<code>position-anchor<\/code>.<\/p>\n\n\n\n<p>Defining the anchor is not enough, we also need to correctly position the element. For this, we have the&nbsp;<code>position-area<\/code>.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>The&nbsp;<code>position-area<\/code>&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\">CSS<\/a>&nbsp;property enables an anchor-positioned element to be positioned relative to the edges of its associated anchor element by placing the positioned element on one or more tiles of an implicit 3&#215;3 grid, where the anchoring element is the center cell.<\/p>\n<cite><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/position-area\">ref<\/a><\/cite><\/blockquote>\n\n\n\n<p><a href=\"https:\/\/css-tip.com\/position-area\/\">Here is an online tool<\/a> to visualize the different values.<\/p>\n\n\n\n<p>We&#8217;re using <code>position-area: top<\/code> on the <code>&lt;output&gt;<\/code>, and a <code>bottom<\/code> class flips that to <code>position-area: bottom<\/code> to re-position it and make the design work below.<\/p>\n\n\n\n<p>Here is the demo so far:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ZEdaxpL\/4a9c01600f429727a43ee8be80a01e33\" src=\"\/\/codepen.io\/anon\/embed\/ZEdaxpL\/4a9c01600f429727a43ee8be80a01e33?height=450&amp;theme-id=1&amp;slug-hash=ZEdaxpL\/4a9c01600f429727a43ee8be80a01e33&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ZEdaxpL\/4a9c01600f429727a43ee8be80a01e33\" title=\"CodePen Embed ZEdaxpL\/4a9c01600f429727a43ee8be80a01e33\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Hmmmm, there is an issue! Both tooltips are linked to the same thumb. This is understandable, because I used the same anchor name so the first one will get ignored.<\/p>\n\n\n\n<p><em>Use a different name<\/em>, you say, and that\u2019s correct, but it\u2019s not the optimal solution. We can still keep the same name, and instead, limit the scope using <code><a href=\"https:\/\/www.w3.org\/TR\/css-anchor-position-1\/#anchor-scope\">anchor-scope<\/a><\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">label<\/span> {\n  <span class=\"hljs-attribute\">anchor-scope<\/span>: all;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The above code should limit the scope of any anchor defined inside the <code>label<\/code> element to the <code>label<\/code> element and its descendants. In other words, the anchors cannot be seen outside the <code>label<\/code> element.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_WbGeqeE\" src=\"\/\/codepen.io\/anon\/embed\/WbGeqeE?height=450&amp;theme-id=1&amp;slug-hash=WbGeqeE&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed WbGeqeE\" title=\"CodePen Embed WbGeqeE\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>We can also limit the scoping by adding <code>position: relative<\/code> to <code>label<\/code>.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ZEdaxwa\" src=\"\/\/codepen.io\/anon\/embed\/ZEdaxwa?height=450&amp;theme-id=1&amp;slug-hash=ZEdaxwa&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ZEdaxwa\" title=\"CodePen Embed ZEdaxwa\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>This fixes the scoping problem, but the position of the tooltip is not good. It\u2019s kind of strange, but it\u2019s by design. Even if <code>anchor-scope<\/code> is doing a better job, I wanted to highlight the use of <code>position: relative<\/code> because you will for sure face it one day, so you have to understand why it does this.<\/p>\n\n\n\n<p>By adding <code>position: relative<\/code>, we create a containing block for the tooltip and we trigger two behaviors. The first one is related to the scoping feature. An element cannot reference an anchor outside its containing block. You can find more details in this post: <a href=\"https:\/\/css-tip.com\/anchor-issues\/\">Why is Anchor Positioning not working?<\/a><\/p>\n\n\n\n<p>The second behavior is related to overflow, as described in <a href=\"https:\/\/www.w3.org\/TR\/css-anchor-position-1\/#anchor-scope\">the specification<\/a>:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>If the box overflows its inset-modified containing block, but would still fit within its original containing block, by default it will \u201cshift\u201d to stay within its original containing block, even if that violates its normal alignment. This behavior makes it more likely that positioned boxes remain visible and within their intended bounds, even when their containing block ends up smaller than anticipated.<\/p>\n<\/blockquote>\n\n\n\n<p>We have a default \u201csafe\u201d alignment that prevents the tooltip from overflowing its containing block, which can be good in some cases but not in ours. We want the tooltip to overflow the <code>label<\/code> element<\/p>\n\n\n\n<p>To allow this, we can use <code>justify-self: unsafe anchor-center;<\/code> and <code>align-self: unsafe end<\/code> (or <code>align-self: unsafe start<\/code> for the bottom tooltip).<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_BaXmbNL\" src=\"\/\/codepen.io\/anon\/embed\/BaXmbNL?height=450&amp;theme-id=1&amp;slug-hash=BaXmbNL&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed BaXmbNL\" title=\"CodePen Embed BaXmbNL\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>When using <code>position-area: top<\/code>, the browser applies a default alignment equivalent to <code>justify-self: anchor-center<\/code> and <code>align-self: end<\/code>. By adding the <code>unsafe<\/code> keyword, we allow it to overflow the containing block. Same logic for <code>position: area: bottom<\/code>, except that <code>align-self<\/code> is equal to <code>start<\/code> instead.<\/p>\n\n\n\n<p>If you want to dig deeper into alignment in the context of anchor positioning, check out <a href=\"https:\/\/frontendmasters.com\/blog\/perfectly-pointed-tooltips-a-foundation\/\">my series of articles about tooltips<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-tooltip-content\">The Tooltip Content<\/h2>\n\n\n\n<p>Now that our tooltip is correctly positioned, let\u2019s move to the content. This is where scroll-driven animations enter the story. I know what you are thinking: <em>\u201cWe have nothing to scroll, so how are we going to use scroll-driven animations?\u201d<\/em><\/p>\n\n\n\n<p>If you read&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_scroll-driven_animations\">the MDN page<\/a>&nbsp;you will find something called a \u201c<em>view progress timeline<\/em>\u201d:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>You progress this timeline based on the change in visibility of an element (known as the subject) inside a scroller. The visibility of the subject inside the scroller is tracked as a percentage of progress \u2014 by default, the timeline is at 0% when the subject is first visible at one edge of the scroller, and 100% when it reaches the opposite edge.<\/p>\n<\/blockquote>\n\n\n\n<p>This is perfect for us since we have a thumb (the subject) that moves inside the input (the scroller) so we don\u2019t really need to have anything else to scroll.<\/p>\n\n\n\n<p>We start by defining the timeline as follows:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">input<\/span> {\n  <span class=\"hljs-attribute\">overflow<\/span>: hidden; <span class=\"hljs-comment\">\/* or `auto` *\/<\/span>\n}\n<span class=\"hljs-selector-tag\">input<\/span><span class=\"hljs-selector-attr\">&#91;type=<span class=\"hljs-string\">\"range\"<\/span> i]<\/span><span class=\"hljs-selector-pseudo\">::-webkit-slider-thumb<\/span> {\n  <span class=\"hljs-attribute\">view-timeline<\/span>: --thumb-view inline;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Similar to what we did with the anchor, we give a name and the axis (<code>inline<\/code>) which is the horizontal one in our default writing mode. Then, we define&nbsp;<code>overflow: hidden<\/code>&nbsp;on the input element. This will make the input our scroller while the thumb is the subject.<\/p>\n\n\n\n<p>If you forget about the overflow (so easy to forget!), another element will get used as the scroller, and won&#8217;t really know which one, and nothing will work as expected. Always remember that you need to define the subject using&nbsp;<code>view-timeline<\/code>&nbsp;and the scroller using&nbsp;<code>overflow<\/code>. I will repeat it again: <strong>don\u2019t forget to define overflow on the scroller element!<\/strong><\/p>\n\n\n\n<p>Next, we define the animation:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-keyword\">@property<\/span> --val {\n  <span class=\"hljs-selector-tag\">syntax<\/span>: '&lt;<span class=\"hljs-selector-tag\">integer<\/span>&gt;';\n  <span class=\"hljs-selector-tag\">inherits<\/span>: <span class=\"hljs-selector-tag\">true<\/span>;\n  <span class=\"hljs-selector-tag\">initial-value<\/span>: 0; \n}\n<span class=\"hljs-selector-tag\">label<\/span> {\n  <span class=\"hljs-attribute\">timeline-scope<\/span>: --thumb-view;\n}\n<span class=\"hljs-selector-tag\">output<\/span> {\n  <span class=\"hljs-attribute\">animation<\/span>: range linear both;\n  <span class=\"hljs-attribute\">animation-timeline<\/span>: --thumb-view;\n}\n<span class=\"hljs-keyword\">@keyframes<\/span> range {\n  0%   { <span class=\"hljs-attribute\">--val<\/span>: <span class=\"hljs-built_in\">var<\/span>(--max) }\n  100% { <span class=\"hljs-attribute\">--val<\/span>: <span class=\"hljs-built_in\">var<\/span>(--min) }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Let\u2019s start with&nbsp;<code>timeline-scope<\/code>. This is yet another scoping issue that will give you a lot of headaches. With anchor positioning, we saw that an anchor is by default available everywhere on the page and we have to limit its scope. With scroll-driven animations, the scope is limited to the element where it\u2019s defined (the subject) and its descendant so we have to increase the scope to make it available to other elements. Two different implementations but the same issue.<\/p>\n\n\n\n<p>Never&nbsp;<strong>ever<\/strong>&nbsp;forget about scoping when working with both features. Sometimes, everything is correctly defined and you are only missing&nbsp;<code>timeline-scope<\/code>&nbsp;or&nbsp;<code>position: relative<\/code>&nbsp;somewhere.<\/p>\n\n\n\n<p>Next we define an animation that animates an integer between the <code>min<\/code> and <code>max<\/code> variables, then link that animation with the timeline we previously defined using&nbsp;<code>animation-timeline<\/code>.<\/p>\n\n\n\n<p>Why the <code>max<\/code> is at 0% and the <code>min<\/code> at 100%? Isn&#8217;t that backwards, you ask?<\/p>\n\n\n\n<p>Intuitively, we tend to think \u201cfrom left to right\u201d but this looks like it\u2019s \u201cfrom right to left\u201d. To understand this, we need to consider the \u201cscroll\u201d part of the feature.<\/p>\n\n\n\n<p>I know that we don\u2019t have scrolling in our case but consider the following example to better understand.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_xxoPQWj\/ecaef63ef66fd19cc51cc3607af5bd33\" src=\"\/\/codepen.io\/anon\/embed\/xxoPQWj\/ecaef63ef66fd19cc51cc3607af5bd33?height=250&amp;theme-id=47434&amp;slug-hash=xxoPQWj\/ecaef63ef66fd19cc51cc3607af5bd33&amp;default-tab=result\" height=\"250\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed xxoPQWj\/ecaef63ef66fd19cc51cc3607af5bd33\" title=\"CodePen Embed xxoPQWj\/ecaef63ef66fd19cc51cc3607af5bd33\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>When you scroll the container \u201cfrom left to right\u201d you have a red circle that moves \u201cfrom right to left\u201d. We still have the \u201cfrom left to right\u201d behavior but it\u2019s linked to the scroll. As for the content, it will logically move in the opposite direction \u201cfrom right to left\u201d.<\/p>\n\n\n\n<p>When the scroll is at the left, the element is at the right and when the scroll is at the right, the element is at the left. The same logic applies to our thumb even if there is nothing to scroll. When the thumb is at the right edge, this is our&nbsp;<code>0%<\/code>&nbsp;state and we need to have the <code>max<\/code> value there. The left edge will be the&nbsp;<code>100%<\/code>&nbsp;state and it\u2019s the <code>min<\/code> value.<\/p>\n\n\n\n<p>The last step is to show the value using a pseudo-element and&nbsp;<code>counter()<\/code><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">output<\/span><span class=\"hljs-selector-pseudo\">::before<\/span> {\n  <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-built_in\">counter<\/span>(num);\n  <span class=\"hljs-attribute\">counter-reset<\/span>: num <span class=\"hljs-built_in\">var<\/span>(--val);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And we are done!<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_BagmGPy\/23701926fdd2c6b8adaec743754d5fb4\" src=\"\/\/codepen.io\/anon\/embed\/BagmGPy\/23701926fdd2c6b8adaec743754d5fb4?height=450&amp;theme-id=47434&amp;slug-hash=BagmGPy\/23701926fdd2c6b8adaec743754d5fb4&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed BagmGPy\/23701926fdd2c6b8adaec743754d5fb4\" title=\"CodePen Embed BagmGPy\/23701926fdd2c6b8adaec743754d5fb4\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Wait a minute, the values aren\u2019t good! We are not reaching the min and max values. For the first slider, we are supposed to go from&nbsp;<code>0<\/code>&nbsp;to&nbsp;<code>120<\/code>&nbsp;but instead, we have&nbsp;<code>9<\/code>&nbsp;and&nbsp;<code>111<\/code>.<\/p>\n\n\n\n<p>Another trick related to the scroll part of the feature and here is a figure to illustrate what is happening:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"977\" height=\"254\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/08\/DLxrh0pl.png?resize=977%2C254&#038;ssl=1\" alt=\"\" class=\"wp-image-3586\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/08\/DLxrh0pl.png?w=977&amp;ssl=1 977w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/08\/DLxrh0pl.png?resize=300%2C78&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/08\/DLxrh0pl.png?resize=768%2C200&amp;ssl=1 768w\" sizes=\"auto, (max-width: 977px) 100vw, 977px\" \/><\/figure>\n\n\n\n<p>The movement of the thumb is limited to the input container (the scroller) but the&nbsp;<code>0%<\/code>&nbsp;and&nbsp;<code>100%<\/code>&nbsp;state are defined to be outside the scroller. In our case, the subject cannot reach the&nbsp;<code>0%<\/code>&nbsp;and&nbsp;<code>100%<\/code>&nbsp;since it cannot go outside but luckily we can update the&nbsp;<code>0%<\/code>&nbsp;and&nbsp;<code>100%<\/code>&nbsp;state:<\/p>\n\n\n\n<p>We can either use&nbsp;<code>animation-range<\/code>&nbsp;to make both states inside the container:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">output<\/span> {\n  <span class=\"hljs-attribute\">animation<\/span>: range linear both;\n  <span class=\"hljs-attribute\">animation-timeline<\/span>: --thumb-view;\n  <span class=\"hljs-attribute\">animation-range<\/span>: entry <span class=\"hljs-number\">100%<\/span> exit <span class=\"hljs-number\">0%<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<figure class=\"wp-block-image\"><img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/paper-attachments.dropboxusercontent.com\/s_3FEC3DE430682F2526F00121B6B9A21346D04C3B20F98BA491C64590EEBAAF65_1723632516189_image.png?ssl=1\" alt=\"\"\/><\/figure>\n\n\n\n<p>Or we consider&nbsp;<code>view-timeline-inset<\/code>&nbsp;with a value equal to the width of the thumb.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">input<\/span><span class=\"hljs-selector-attr\">&#91;type=<span class=\"hljs-string\">\"range\"<\/span> i]<\/span><span class=\"hljs-selector-pseudo\">::-webkit-slider-thumb<\/span>{\n  <span class=\"hljs-attribute\">anchor-name<\/span>: --thumb;\n  <span class=\"hljs-attribute\">view-timeline<\/span>: --thumb-view inline;\n  <span class=\"hljs-attribute\">view-timeline-inset<\/span>: <span class=\"hljs-built_in\">var<\/span>(--s); <span class=\"hljs-comment\">\/* --s is defined on an upper element and is used to define the size of the thumb *\/<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The first method seems better as we don\u2019t have to know the size of the thumb (the subject) but keep in mind both methods.&nbsp;The <code>view-timeline-inset<\/code>&nbsp;property may be more suitable in some situations.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_JjqNEbZ\" src=\"\/\/codepen.io\/anon\/embed\/JjqNEbZ?height=450&amp;theme-id=47434&amp;slug-hash=JjqNEbZ&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed JjqNEbZ\" title=\"CodePen Embed JjqNEbZ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Now our slider is perfect!<\/p>\n\n\n\n<p>A lot of stuff to remember, right? Between the scoping issues, the range we have to correct, the overflow we should not forget, the <code>min<\/code> that should be at&nbsp;<code>100%<\/code>&nbsp;and <code>max<\/code> that should be at&nbsp;<code>0%<\/code>, etc. Don\u2019t worry, I feel the same. They are new features with new mechanisms so it requires a lot of practice to get used to them and build a clear mental model. If you are a bit lost, that\u2019s fine! No need to understand everything at once. Take the time to play with the different demos, read the doc of each property, and try things on your own.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"adding-some-motion\">Adding Motion<\/h2>\n\n\n\n<p>Now let\u2019s move to the fun part, those silly wobbly animations. A tooltip that follows the thumb with dynamic content is good but it\u2019s even better if we add some motion to it.<\/p>\n\n\n\n<p>You may think this is gonna be the hardest part but actually it\u2019s the easiest one, and here is the relevant code:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-keyword\">@property<\/span> --val {\n  <span class=\"hljs-selector-tag\">syntax<\/span>: '&lt;<span class=\"hljs-selector-tag\">integer<\/span>&gt;';\n  <span class=\"hljs-selector-tag\">inherits<\/span>: <span class=\"hljs-selector-tag\">true<\/span>;\n  <span class=\"hljs-selector-tag\">initial-value<\/span>: 0; \n}\n<span class=\"hljs-keyword\">@property<\/span> --e {\n  <span class=\"hljs-selector-tag\">syntax<\/span>: '&lt;<span class=\"hljs-selector-tag\">number<\/span>&gt;';\n  <span class=\"hljs-selector-tag\">inherits<\/span>: <span class=\"hljs-selector-tag\">true<\/span>;\n  <span class=\"hljs-selector-tag\">initial-value<\/span>: 0; \n}\n<span class=\"hljs-selector-tag\">output<\/span> {\n  <span class=\"hljs-attribute\">animation<\/span>: range linear both;\n  <span class=\"hljs-attribute\">animation-timeline<\/span>: --thumb-view;\n  <span class=\"hljs-attribute\">animation-range<\/span>: entry <span class=\"hljs-number\">100%<\/span> exit <span class=\"hljs-number\">0%<\/span>;\n}\n<span class=\"hljs-selector-tag\">output<\/span><span class=\"hljs-selector-pseudo\">:before<\/span> {\n  <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-built_in\">counter<\/span>(num);\n  <span class=\"hljs-attribute\">counter-reset<\/span>: num <span class=\"hljs-built_in\">var<\/span>(--val);\n  <span class=\"hljs-attribute\">--e<\/span>: <span class=\"hljs-built_in\">var<\/span>(--val);\n  <span class=\"hljs-attribute\">transition<\/span>: --e .<span class=\"hljs-number\">1s<\/span> ease-out;\n  <span class=\"hljs-attribute\">rotate<\/span>: <span class=\"hljs-built_in\">calc<\/span>((var(--e) - <span class=\"hljs-built_in\">var<\/span>(--val))*<span class=\"hljs-number\">2deg<\/span>);\n}\n<span class=\"hljs-keyword\">@keyframes<\/span> range {\n  0%   { <span class=\"hljs-attribute\">--val<\/span>: <span class=\"hljs-built_in\">var<\/span>(--max) }\n  100% { <span class=\"hljs-attribute\">--val<\/span>: <span class=\"hljs-built_in\">var<\/span>(--min) }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We add a new CSS variable&nbsp;<code>--e<\/code>&nbsp;with a number type. This variable will be equal to the&nbsp;<code>--val<\/code>&nbsp;variable. Until now, nothing fancy. We have two variables having the same value but one of them has a <code>transition<\/code>. Here comes the magic.<\/p>\n\n\n\n<p>When you move the thumb, the animation will update the&nbsp;<code>--val<\/code>&nbsp;variable inside the output element. The pseudo-element will then inherit that value to update the content and also update&nbsp;<code>--e<\/code>. But since we are applying a transition to&nbsp;<code>--e<\/code>, it will not have an instant update but a smooth one (well, you know how transitions work!). This means that for a brief moment, both&nbsp;<code>--e<\/code>&nbsp;and&nbsp;<code>--val<\/code>&nbsp;will not be equal thus their difference is different from 0. We use that difference inside the rotation!<\/p>\n\n\n\n<p>In addition to this, the difference can get bigger if you move the thumb fast or slow. Let\u2019s suppose the current value is equal to&nbsp;<code>5<\/code>. If you move the thumb rapidly to the value&nbsp;<code>50<\/code>, the difference will be equal to&nbsp;<code>45<\/code>&nbsp;hence we get a big rotation. If you move to the value&nbsp;<code>7<\/code>, the difference will be equal to&nbsp;<code>2<\/code>&nbsp;and the rotation won\u2019t be that big.<\/p>\n\n\n\n<p>Here is the full demo again so you can play with it. Try different speeds of movement and see how the rotation is different each time.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_MWdmZPL\" src=\"\/\/codepen.io\/anon\/embed\/MWdmZPL?height=450&amp;theme-id=47434&amp;slug-hash=MWdmZPL&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed MWdmZPL\" title=\"CodePen Embed MWdmZPL\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>If you want to dig more into this technique and see more examples I advise you to <a href=\"https:\/\/www.bram.us\/2023\/10\/23\/css-scroll-detection\/\">read this article by Bramus<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"another-example\">Another Example<\/h2>\n\n\n\n<p>Let\u2019s try a different idea.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_vYweZQa\" src=\"\/\/codepen.io\/anon\/embed\/vYweZQa?height=450&amp;theme-id=47434&amp;slug-hash=vYweZQa&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed vYweZQa\" title=\"CodePen Embed vYweZQa\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>This time, I am adjusting the tooltip position (and its tail) to remain within the horizontal boundary of the input element. Can you figure out how it\u2019s done? This will be your homework!<\/p>\n\n\n\n<p>For the tooltip part, I already did the job for you. I will redirect you again to&nbsp;<a href=\"https:\/\/css-generators.com\/tooltip-speech-bubble\/\">my online collection<\/a>&nbsp;where you can get the code of the tooltip shape. Within that code, I am already defining one variable that controls the tail position.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"conclusion\">Conclusion<\/h2>\n\n\n\n<p>CSS is cool. A few years ago, doing such stuff with CSS would have been impossible. You would probably need one or two JavaScript libraries to handle the position of the tooltip, the dynamic content, the motion, etc. Now, all it takes is a few lines of CSS.<\/p>\n\n\n\n<p>It\u2019s still early to adopt those features and include them in real projects but I think it\u2019s a good time to explore them and get an overview of what could be done in the near future. If you want more \u201cfuturistic\u201d experimentation make sure to check&nbsp;<a href=\"https:\/\/css-tip.com\/\">my CSS Tip website<\/a>&nbsp;where I regularly share cool demos!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>We&#8217;ll make some extremely stylized range sliders with simple semantic HTML and some very fresh CSS. You might be surprised how custom these things can get these days.<\/p>\n","protected":false},"author":12,"featured_media":3579,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[121,7,226,57],"class_list":["post-3569","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-anchor","tag-css","tag-range","tag-scroll-driven-animations"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/08\/wobble-thumb.png?fit=1408%2C670&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/3569","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/12"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=3569"}],"version-history":[{"count":12,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/3569\/revisions"}],"predecessor-version":[{"id":8764,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/3569\/revisions\/8764"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/3579"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=3569"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=3569"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=3569"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}