<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ciprian Titire</title>
    <description>The latest articles on DEV Community by Ciprian Titire (@cipriantitire).</description>
    <link>https://dev.to/cipriantitire</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4205741%2F04872679-f705-4ae1-9660-50e905eb7d03.png</url>
      <title>DEV Community: Ciprian Titire</title>
      <link>https://dev.to/cipriantitire</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cipriantitire"/>
    <language>en</language>
    <item>
      <title>I made a web odometer that carries like the real one</title>
      <dc:creator>Ciprian Titire</dc:creator>
      <pubDate>Mon, 26 Oct 2026 16:26:55 +0000</pubDate>
      <link>https://dev.to/cipriantitire/i-made-a-web-odometer-that-carries-like-the-real-one-1i6i</link>
      <guid>https://dev.to/cipriantitire/i-made-a-web-odometer-that-carries-like-the-real-one-1i6i</guid>
      <description>&lt;p&gt;Most animated counters move every changing digit at once. A real odometer doesn't do that. Going from 1,099 to 1,100, the ones drum turns and the tens and hundreds only get pulled along at the end.&lt;/p&gt;

&lt;p&gt;I wanted that on the web so I made one. It's free, MIT, on npm as &lt;code&gt;tactile-odometer&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  how it carries
&lt;/h2&gt;

&lt;p&gt;Each drum is a ten-sided prism in CSS 3D. Only the last drum is driven, by one spring on a single number. Every other drum works out its angle from that number. It stays put while the drum to its right goes 0 to 9, and turns when that drum goes 9 to 0.&lt;/p&gt;

&lt;p&gt;In the 1,099 to 1,100 test the ones drum is most of the way round before the tens and hundreds start (268 ms in), and all three land together at 417 ms. I timed odometer.js and NumberFlow the same way in Chromium, and both start every changing column on the same frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  the small stuff
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;the faces are real text, not images&lt;/li&gt;
&lt;li&gt;past about 24 digits a second a drum draws as a blur instead of strobing&lt;/li&gt;
&lt;li&gt;screen readers hear the number once when it lands&lt;/li&gt;
&lt;li&gt;with reduced motion on, the number just changes&lt;/li&gt;
&lt;li&gt;Intl.NumberFormat, up to 6 decimals, a prefix and a suffix, and a tally mode that counts up or down&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  using it
&lt;/h2&gt;

&lt;p&gt;The quickest way in is the shadcn registry, no account needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx shadcn add @tactile/odometer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or from npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Odometer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tactile-odometer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tactile-odometer/odometer.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;odo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Odometer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;counter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1099&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;odo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// rolls&lt;/span&gt;
&lt;span class="nx"&gt;odo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;instant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// jumps&lt;/span&gt;
&lt;span class="nx"&gt;odo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// live tally, odo.count(0) stops it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a custom element if you'd rather drop it in as a tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  what it costs
&lt;/h2&gt;

&lt;p&gt;About 8 KB gzipped of script, 4.2 KB of CSS and a 20.7 KB numerals font, so it's heavier than the others. There's no React or Vue package yet (in React, create it in an effect and destroy it on unmount), and the bundled numerals are Latin only.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/cipriantitire/tactile-ui-free" rel="noopener noreferrer"&gt;github.com/cipriantitire/tactile-ui-free&lt;/a&gt;&lt;br&gt;
Live, to play with: &lt;a href="https://tactileui.dev/objects/odometer" rel="noopener noreferrer"&gt;tactileui.dev/objects/odometer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's one piece of Tactile UI, the library I'm launching on Tuesday. If this looks wrong next to a real odometer, tell me where.&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>ui</category>
    </item>
  </channel>
</rss>
