[{"data":1,"prerenderedAt":1182},["ShallowReactive",2],{"blog-\u002Fblog\u002Fvue3-autocounter":3,"blog-surround-\u002Fblog\u002Fvue3-autocounter":1171},{"id":4,"title":5,"body":6,"date":1126,"description":1127,"extension":1128,"faq":1129,"image":1144,"imageAlt":1145,"links":1146,"meta":1158,"navigation":230,"path":1159,"rawbody":1160,"readingTime":243,"seo":1161,"sitemap":1162,"stem":1163,"tags":1164,"tldr":1168,"updated":1169,"__hash__":1170},"blog\u002Fblog\u002Fvue3-autocounter.md","Vue 3 Autocounter: an animated number counter component for Vue 3",{"type":7,"value":8,"toc":1114},"minimark",[9,14,22,35,38,113,128,132,135,160,165,176,275,279,395,399,402,553,560,564,763,774,778,781,977,1041,1075,1079,1090,1097,1103,1107,1110],[10,11,13],"h2",{"id":12},"what-is-vue3-autocounter","What is vue3-autocounter?",[15,16,17,21],"p",{},[18,19,20],"strong",{},"vue3-autocounter"," is an open-source Vue 3 component, published as an npm package, that animates a number from a start value to an end value over a set duration. I built it as a small, reusable way to show statistics, metrics and prices that \"count up\" when a page loads, without pulling in a large animation library.",[15,23,24,25,30,31,34],{},"It has been downloaded more than ",[26,27,29],"live-counter",{":end":28},"176000","176,000"," times from npm and has ",[26,32,33],{":end":33},"82"," stars on GitHub. Both of those numbers are animated by the component itself.",[15,36,37],{},"Key features:",[39,40,41,58,65,75,95,106],"ul",{},[42,43,44,45,48,49,53,54,57],"li",{},"Counts ",[18,46,47],{},"up or down",". The direction comes from comparing ",[50,51,52],"code",{},"startAmount"," and ",[50,55,56],{},"endAmount",".",[42,59,60,61,64],{},"Configurable ",[18,62,63],{},"duration"," in seconds.",[42,66,67,70,71,74],{},[18,68,69],{},"Formatting",": decimals, thousands separator, decimal separator, prefix and suffix (for example ",[50,72,73],{},"$1,250.00 USD",").",[42,76,77,80,81,84,85,84,88,53,91,94],{},[18,78,79],{},"Control methods",": ",[50,82,83],{},"start",", ",[50,86,87],{},"pause",[50,89,90],{},"resume",[50,92,93],{},"reset",", available through a template ref.",[42,96,97,80,100,53,103,57],{},[18,98,99],{},"Lifecycle events",[50,101,102],{},"mounted",[50,104,105],{},"finished",[42,107,108,109,112],{},"Written in ",[18,110,111],{},"TypeScript",", with an SSR-compatible build.",[114,115,118],"project-embed",{"src":116,"title":117},"https:\u002F\u002Fvue3autocounter.cristopherps.dev","vue3-autocounter live demo",[15,119,120,121,127],{},"The ",[122,123,126],"a",{"href":116,"rel":124},[125],"nofollow","official vue3-autocounter website",", with live examples of every option.",[10,129,131],{"id":130},"how-do-you-install-vue3-autocounter","How do you install vue3-autocounter?",[15,133,134],{},"Install it from npm:",[136,137,142],"pre",{"className":138,"code":139,"language":140,"meta":141,"style":141},"language-sh shiki shiki-themes github-dark","npm install vue3-autocounter\n","sh","",[50,143,144],{"__ignoreMap":141},[145,146,149,153,157],"span",{"class":147,"line":148},"line",1,[145,150,152],{"class":151},"svObZ","npm",[145,154,156],{"class":155},"sU2Wk"," install",[145,158,159],{"class":155}," vue3-autocounter\n",[161,162,164],"h3",{"id":163},"register-it-globally","Register it globally",[15,166,167,168,171,172,175],{},"Add it to your ",[50,169,170],{},"main.js"," (or ",[50,173,174],{},"main.ts",") to make it available in every component:",[136,177,181],{"className":178,"code":179,"language":180,"meta":141,"style":141},"language-js shiki shiki-themes github-dark","import { createApp } from 'vue'\nimport Vue3Autocounter from 'vue3-autocounter'\nimport App from '.\u002FApp.vue'\n\ncreateApp(App)\n  .component('vue3-autocounter', Vue3Autocounter)\n  .mount('#app')\n","js",[50,182,183,199,212,225,232,241,259],{"__ignoreMap":141},[145,184,185,189,193,196],{"class":147,"line":148},[145,186,188],{"class":187},"snl16","import",[145,190,192],{"class":191},"s95oV"," { createApp } ",[145,194,195],{"class":187},"from",[145,197,198],{"class":155}," 'vue'\n",[145,200,202,204,207,209],{"class":147,"line":201},2,[145,203,188],{"class":187},[145,205,206],{"class":191}," Vue3Autocounter ",[145,208,195],{"class":187},[145,210,211],{"class":155}," 'vue3-autocounter'\n",[145,213,215,217,220,222],{"class":147,"line":214},3,[145,216,188],{"class":187},[145,218,219],{"class":191}," App ",[145,221,195],{"class":187},[145,223,224],{"class":155}," '.\u002FApp.vue'\n",[145,226,228],{"class":147,"line":227},4,[145,229,231],{"emptyLinePlaceholder":230},true,"\n",[145,233,235,238],{"class":147,"line":234},5,[145,236,237],{"class":151},"createApp",[145,239,240],{"class":191},"(App)\n",[145,242,244,247,250,253,256],{"class":147,"line":243},6,[145,245,246],{"class":191},"  .",[145,248,249],{"class":151},"component",[145,251,252],{"class":191},"(",[145,254,255],{"class":155},"'vue3-autocounter'",[145,257,258],{"class":191},", Vue3Autocounter)\n",[145,260,262,264,267,269,272],{"class":147,"line":261},7,[145,263,246],{"class":191},[145,265,266],{"class":151},"mount",[145,268,252],{"class":191},[145,270,271],{"class":155},"'#app'",[145,273,274],{"class":191},")\n",[161,276,278],{"id":277},"or-import-it-in-a-single-file-component","Or import it in a single file component",[136,280,284],{"className":281,"code":282,"language":283,"meta":141,"style":141},"language-vue shiki shiki-themes github-dark","\u003Cscript setup>\nimport Vue3Autocounter from 'vue3-autocounter'\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CVue3Autocounter :startAmount=\"0\" :endAmount=\"2021\" :duration=\"3\" \u002F>\n\u003C\u002Ftemplate>\n","vue",[50,285,286,301,311,320,324,333,387],{"__ignoreMap":141},[145,287,288,291,295,298],{"class":147,"line":148},[145,289,290],{"class":191},"\u003C",[145,292,294],{"class":293},"s4JwU","script",[145,296,297],{"class":151}," setup",[145,299,300],{"class":191},">\n",[145,302,303,305,307,309],{"class":147,"line":201},[145,304,188],{"class":187},[145,306,206],{"class":191},[145,308,195],{"class":187},[145,310,211],{"class":155},[145,312,313,316,318],{"class":147,"line":214},[145,314,315],{"class":191},"\u003C\u002F",[145,317,294],{"class":293},[145,319,300],{"class":191},[145,321,322],{"class":147,"line":227},[145,323,231],{"emptyLinePlaceholder":230},[145,325,326,328,331],{"class":147,"line":234},[145,327,290],{"class":191},[145,329,330],{"class":293},"template",[145,332,300],{"class":191},[145,334,335,338,341,344,346,349,352,356,358,360,362,364,366,369,371,373,375,377,379,382,384],{"class":147,"line":243},[145,336,337],{"class":191},"  \u003C",[145,339,340],{"class":293},"Vue3Autocounter",[145,342,343],{"class":191}," :",[145,345,52],{"class":151},[145,347,348],{"class":191},"=",[145,350,351],{"class":155},"\"",[145,353,355],{"class":354},"sDLfK","0",[145,357,351],{"class":155},[145,359,343],{"class":191},[145,361,56],{"class":151},[145,363,348],{"class":191},[145,365,351],{"class":155},[145,367,368],{"class":354},"2021",[145,370,351],{"class":155},[145,372,343],{"class":191},[145,374,63],{"class":151},[145,376,348],{"class":191},[145,378,351],{"class":155},[145,380,381],{"class":354},"3",[145,383,351],{"class":155},[145,385,386],{"class":191}," \u002F>\n",[145,388,389,391,393],{"class":147,"line":261},[145,390,315],{"class":191},[145,392,330],{"class":293},[145,394,300],{"class":191},[10,396,398],{"id":397},"how-do-you-use-it","How do you use it?",[15,400,401],{},"A complete example with formatting and an event listener:",[136,403,405],{"className":281,"code":404,"language":283,"meta":141,"style":141},"\u003Ctemplate>\n  \u003Cvue3-autocounter\n    ref=\"counter\"\n    :startAmount=\"0\"\n    :endAmount=\"2021\"\n    :duration=\"3\"\n    prefix=\"$\"\n    suffix=\" USD\"\n    separator=\",\"\n    decimalSeparator=\".\"\n    :decimals=\"2\"\n    :autoinit=\"true\"\n    @finished=\"onFinished\"\n  \u002F>\n\u003C\u002Ftemplate>\n",[50,406,407,415,422,432,442,452,462,472,483,494,505,516,527,538,544],{"__ignoreMap":141},[145,408,409,411,413],{"class":147,"line":148},[145,410,290],{"class":191},[145,412,330],{"class":293},[145,414,300],{"class":191},[145,416,417,419],{"class":147,"line":201},[145,418,337],{"class":191},[145,420,421],{"class":293},"vue3-autocounter\n",[145,423,424,427,429],{"class":147,"line":214},[145,425,426],{"class":151},"    ref",[145,428,348],{"class":191},[145,430,431],{"class":155},"\"counter\"\n",[145,433,434,437,439],{"class":147,"line":227},[145,435,436],{"class":151},"    :startAmount",[145,438,348],{"class":191},[145,440,441],{"class":155},"\"0\"\n",[145,443,444,447,449],{"class":147,"line":234},[145,445,446],{"class":151},"    :endAmount",[145,448,348],{"class":191},[145,450,451],{"class":155},"\"2021\"\n",[145,453,454,457,459],{"class":147,"line":243},[145,455,456],{"class":151},"    :duration",[145,458,348],{"class":191},[145,460,461],{"class":155},"\"3\"\n",[145,463,464,467,469],{"class":147,"line":261},[145,465,466],{"class":151},"    prefix",[145,468,348],{"class":191},[145,470,471],{"class":155},"\"$\"\n",[145,473,475,478,480],{"class":147,"line":474},8,[145,476,477],{"class":151},"    suffix",[145,479,348],{"class":191},[145,481,482],{"class":155},"\" USD\"\n",[145,484,486,489,491],{"class":147,"line":485},9,[145,487,488],{"class":151},"    separator",[145,490,348],{"class":191},[145,492,493],{"class":155},"\",\"\n",[145,495,497,500,502],{"class":147,"line":496},10,[145,498,499],{"class":151},"    decimalSeparator",[145,501,348],{"class":191},[145,503,504],{"class":155},"\".\"\n",[145,506,508,511,513],{"class":147,"line":507},11,[145,509,510],{"class":151},"    :decimals",[145,512,348],{"class":191},[145,514,515],{"class":155},"\"2\"\n",[145,517,519,522,524],{"class":147,"line":518},12,[145,520,521],{"class":151},"    :autoinit",[145,523,348],{"class":191},[145,525,526],{"class":155},"\"true\"\n",[145,528,530,533,535],{"class":147,"line":529},13,[145,531,532],{"class":151},"    @finished",[145,534,348],{"class":191},[145,536,537],{"class":155},"\"onFinished\"\n",[145,539,541],{"class":147,"line":540},14,[145,542,543],{"class":191},"  \u002F>\n",[145,545,547,549,551],{"class":147,"line":546},15,[145,548,315],{"class":191},[145,550,330],{"class":293},[145,552,300],{"class":191},[15,554,555,556,559],{},"This renders ",[50,557,558],{},"$2,021.00 USD"," once the 3-second animation finishes.",[10,561,563],{"id":562},"which-props-does-vue3-autocounter-support","Which props does vue3-autocounter support?",[565,566,567,586],"table",{},[568,569,570],"thead",{},[571,572,573,577,580,583],"tr",{},[574,575,576],"th",{},"Prop",[574,578,579],{},"Type",[574,581,582],{},"Default",[574,584,585],{},"Description",[587,588,589,608,626,643,663,686,707,726,745],"tbody",{},[571,590,591,596,601,605],{},[592,593,594],"td",{},[50,595,52],{},[592,597,598],{},[50,599,600],{},"Number",[592,602,603],{},[50,604,355],{},[592,606,607],{},"Value the animation starts from.",[571,609,610,614,618,623],{},[592,611,612],{},[50,613,56],{},[592,615,616],{},[50,617,600],{},[592,619,620,622],{},[50,621,355],{}," (required)",[592,624,625],{},"Value the animation ends at.",[571,627,628,632,636,640],{},[592,629,630],{},[50,631,63],{},[592,633,634],{},[50,635,600],{},[592,637,638],{},[50,639,381],{},[592,641,642],{},"Animation length in seconds. Must be greater than 0.",[571,644,645,650,655,660],{},[592,646,647],{},[50,648,649],{},"autoinit",[592,651,652],{},[50,653,654],{},"Boolean",[592,656,657],{},[50,658,659],{},"true",[592,661,662],{},"Start counting automatically on mount.",[571,664,665,670,675,680],{},[592,666,667],{},[50,668,669],{},"prefix",[592,671,672],{},[50,673,674],{},"String",[592,676,677],{},[50,678,679],{},"''",[592,681,682,683,57],{},"Text shown before the number, e.g. ",[50,684,685],{},"$",[571,687,688,693,697,701],{},[592,689,690],{},[50,691,692],{},"suffix",[592,694,695],{},[50,696,674],{},[592,698,699],{},[50,700,679],{},[592,702,703,704,57],{},"Text shown after the number, e.g. ",[50,705,706],{},"USD",[571,708,709,714,718,723],{},[592,710,711],{},[50,712,713],{},"separator",[592,715,716],{},[50,717,674],{},[592,719,720],{},[50,721,722],{},"','",[592,724,725],{},"Thousands separator.",[571,727,728,733,737,742],{},[592,729,730],{},[50,731,732],{},"decimalSeparator",[592,734,735],{},[50,736,674],{},[592,738,739],{},[50,740,741],{},"'.'",[592,743,744],{},"Decimal separator.",[571,746,747,752,756,760],{},[592,748,749],{},[50,750,751],{},"decimals",[592,753,754],{},[50,755,600],{},[592,757,758],{},[50,759,355],{},[592,761,762],{},"Number of decimal places. Must be 0 or greater.",[15,764,765,766,84,768,770,771,773],{},"Changing ",[50,767,52],{},[50,769,56],{}," or ",[50,772,63],{}," after mount resets the animation and runs it again with the new values. This makes it easy to animate numbers that come from an API.",[10,775,777],{"id":776},"how-do-you-control-the-animation","How do you control the animation?",[15,779,780],{},"Use a template ref to call the component's methods:",[136,782,784],{"className":281,"code":783,"language":283,"meta":141,"style":141},"\u003Cscript setup>\nimport { ref } from 'vue'\n\nconst counter = ref(null)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cvue3-autocounter ref=\"counter\" :endAmount=\"1000\" :autoinit=\"false\" \u002F>\n\n  \u003Cbutton @click=\"counter.start()\">Start\u003C\u002Fbutton>\n  \u003Cbutton @click=\"counter.pause()\">Pause\u003C\u002Fbutton>\n  \u003Cbutton @click=\"counter.resume()\">Resume\u003C\u002Fbutton>\n  \u003Cbutton @click=\"counter.reset()\">Reset\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n",[50,785,786,796,807,811,832,840,844,852,883,887,909,929,949,969],{"__ignoreMap":141},[145,787,788,790,792,794],{"class":147,"line":148},[145,789,290],{"class":191},[145,791,294],{"class":293},[145,793,297],{"class":151},[145,795,300],{"class":191},[145,797,798,800,803,805],{"class":147,"line":201},[145,799,188],{"class":187},[145,801,802],{"class":191}," { ref } ",[145,804,195],{"class":187},[145,806,198],{"class":155},[145,808,809],{"class":147,"line":214},[145,810,231],{"emptyLinePlaceholder":230},[145,812,813,816,819,822,825,827,830],{"class":147,"line":227},[145,814,815],{"class":187},"const",[145,817,818],{"class":354}," counter",[145,820,821],{"class":187}," =",[145,823,824],{"class":151}," ref",[145,826,252],{"class":191},[145,828,829],{"class":354},"null",[145,831,274],{"class":191},[145,833,834,836,838],{"class":147,"line":234},[145,835,315],{"class":191},[145,837,294],{"class":293},[145,839,300],{"class":191},[145,841,842],{"class":147,"line":243},[145,843,231],{"emptyLinePlaceholder":230},[145,845,846,848,850],{"class":147,"line":261},[145,847,290],{"class":191},[145,849,330],{"class":293},[145,851,300],{"class":191},[145,853,854,856,858,860,862,865,868,870,873,876,878,881],{"class":147,"line":474},[145,855,337],{"class":191},[145,857,20],{"class":293},[145,859,824],{"class":151},[145,861,348],{"class":191},[145,863,864],{"class":155},"\"counter\"",[145,866,867],{"class":151}," :endAmount",[145,869,348],{"class":191},[145,871,872],{"class":155},"\"1000\"",[145,874,875],{"class":151}," :autoinit",[145,877,348],{"class":191},[145,879,880],{"class":155},"\"false\"",[145,882,386],{"class":191},[145,884,885],{"class":147,"line":485},[145,886,231],{"emptyLinePlaceholder":230},[145,888,889,891,894,897,899,902,905,907],{"class":147,"line":496},[145,890,337],{"class":191},[145,892,893],{"class":293},"button",[145,895,896],{"class":151}," @click",[145,898,348],{"class":191},[145,900,901],{"class":155},"\"counter.start()\"",[145,903,904],{"class":191},">Start\u003C\u002F",[145,906,893],{"class":293},[145,908,300],{"class":191},[145,910,911,913,915,917,919,922,925,927],{"class":147,"line":507},[145,912,337],{"class":191},[145,914,893],{"class":293},[145,916,896],{"class":151},[145,918,348],{"class":191},[145,920,921],{"class":155},"\"counter.pause()\"",[145,923,924],{"class":191},">Pause\u003C\u002F",[145,926,893],{"class":293},[145,928,300],{"class":191},[145,930,931,933,935,937,939,942,945,947],{"class":147,"line":518},[145,932,337],{"class":191},[145,934,893],{"class":293},[145,936,896],{"class":151},[145,938,348],{"class":191},[145,940,941],{"class":155},"\"counter.resume()\"",[145,943,944],{"class":191},">Resume\u003C\u002F",[145,946,893],{"class":293},[145,948,300],{"class":191},[145,950,951,953,955,957,959,962,965,967],{"class":147,"line":529},[145,952,337],{"class":191},[145,954,893],{"class":293},[145,956,896],{"class":151},[145,958,348],{"class":191},[145,960,961],{"class":155},"\"counter.reset()\"",[145,963,964],{"class":191},">Reset\u003C\u002F",[145,966,893],{"class":293},[145,968,300],{"class":191},[145,970,971,973,975],{"class":147,"line":540},[145,972,315],{"class":191},[145,974,330],{"class":293},[145,976,300],{"class":191},[565,978,979,989],{},[568,980,981],{},[571,982,983,986],{},[574,984,985],{},"Method",[574,987,988],{},"What it does",[587,990,991,1003,1013,1023],{},[571,992,993,998],{},[592,994,995],{},[50,996,997],{},"start()",[592,999,1000,1001,57],{},"Starts the animation from ",[50,1002,52],{},[571,1004,1005,1010],{},[592,1006,1007],{},[50,1008,1009],{},"pause()",[592,1011,1012],{},"Pauses the animation and keeps the current value.",[571,1014,1015,1020],{},[592,1016,1017],{},[50,1018,1019],{},"resume()",[592,1021,1022],{},"Continues from the current value for the remaining time.",[571,1024,1025,1030],{},[592,1026,1027],{},[50,1028,1029],{},"reset()",[592,1031,1032,1033,1035,1036,1038,1039,57],{},"Goes back to ",[50,1034,52],{},". It restarts if ",[50,1037,649],{}," is ",[50,1040,659],{},[565,1042,1043,1053],{},[568,1044,1045],{},[571,1046,1047,1050],{},[574,1048,1049],{},"Event",[574,1051,1052],{},"When it fires",[587,1054,1055,1064],{},[571,1056,1057,1061],{},[592,1058,1059],{},[50,1060,102],{},[592,1062,1063],{},"The component has mounted and is ready.",[571,1065,1066,1070],{},[592,1067,1068],{},[50,1069,105],{},[592,1071,1072,1073,57],{},"The counter has reached ",[50,1074,56],{},[10,1076,1078],{"id":1077},"how-does-it-work-under-the-hood","How does it work under the hood?",[15,1080,1081,1082,1085,1086,1089],{},"The component uses ",[50,1083,1084],{},"window.requestAnimationFrame"," instead of ",[50,1087,1088],{},"setInterval",". Each frame, it calculates how much of the duration has passed and moves the current value forward by that proportion. This keeps the animation smooth and in sync with the browser's repaint cycle, and the browser pauses it automatically in background tabs.",[15,1091,1092,1093,1096],{},"Formatting runs in a computed property. The number goes through ",[50,1094,1095],{},"toFixed(decimals)",", gets split into integer and decimal parts, and the thousands separator is inserted with a regular expression. Because it is computed, the displayed string only recalculates when the value changes.",[15,1098,1099,1100,1102],{},"Pausing stores the remaining time and the current value. ",[50,1101,1019],{}," then starts a new animation from that point, so pause and resume never make the counter jump.",[10,1104,1106],{"id":1105},"using-it-with-nuxt","Using it with Nuxt",[15,1108,1109],{},"The package ships an SSR build, so it works in Nuxt out of the box. Import it in a page or component and the server renders it; the counting starts once the page hydrates in the browser. This site, built with Nuxt, uses vue3-autocounter for the download and star counts above.",[1111,1112,1113],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":141,"searchDepth":201,"depth":201,"links":1115},[1116,1117,1121,1122,1123,1124,1125],{"id":12,"depth":201,"text":13},{"id":130,"depth":201,"text":131,"children":1118},[1119,1120],{"id":163,"depth":214,"text":164},{"id":277,"depth":214,"text":278},{"id":397,"depth":201,"text":398},{"id":562,"depth":201,"text":563},{"id":776,"depth":201,"text":777},{"id":1077,"depth":201,"text":1078},{"id":1105,"depth":201,"text":1106},"2026-10-17","How vue3-autocounter works, how to install it, and how to use it to animate statistics, metrics and prices in Vue 3 and Nuxt apps, with every prop, event and method documented.","md",[1130,1132,1135,1138,1141],{"question":13,"answer":1131},"vue3-autocounter is an open-source Vue 3 component, published on npm, that animates a number from a start amount to an end amount over a configurable duration. It is written in TypeScript and can count up or down.",{"question":1133,"answer":1134},"How do I install vue3-autocounter?","Run npm install vue3-autocounter, then register the component globally with app.component('vue3-autocounter', Vue3Autocounter) or import it directly in a single file component.",{"question":1136,"answer":1137},"Can vue3-autocounter count down?","Yes. If endAmount is lower than startAmount the component counts down automatically. No extra configuration is needed.",{"question":1139,"answer":1140},"Does vue3-autocounter work with Nuxt and server-side rendering?","Yes. The package ships an SSR build, and the counting animation starts on the client after the component mounts. This website is built with Nuxt and uses it to animate its own download and star counts.",{"question":1142,"answer":1143},"How do I know when the animation has finished?","Listen to the finished event, for example @finished=\"onFinished\". The component also emits a mounted event when it is ready.","\u002Fimages\u002Fprojects\u002Fvue3-autocounter-demo.png","The vue3-autocounter demo website showing animated counters",[1147,1151,1155],{"label":1148,"to":1149,"icon":1150},"View on npm","https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fvue3-autocounter","pi pi-code",{"label":1152,"to":1153,"icon":1154},"View on GitHub","https:\u002F\u002Fgithub.com\u002Fps-cristopher\u002Fvue3-autocounter","pi pi-github",{"label":1156,"to":116,"icon":1157},"Live demo and docs","pi pi-globe",{},"\u002Fblog\u002Fvue3-autocounter","---\ntitle: \"Vue 3 Autocounter: an animated number counter component for Vue 3\"\ndescription: \"How vue3-autocounter works, how to install it, and how to use it to animate statistics, metrics and prices in Vue 3 and Nuxt apps, with every prop, event and method documented.\"\ndate: \"2026-10-17\"\ntags: [\"Vue 3\", \"TypeScript\", \"npm\", \"Open source\", \"Components\"]\ntldr: \"vue3-autocounter is a lightweight, open-source Vue 3 component written in TypeScript that animates a number from a start value to an end value over a set duration. It counts up or down and supports decimals, thousands separators, prefixes, suffixes, pause\u002Fresume and a finished event. Install it with npm install vue3-autocounter.\"\nimage: \"\u002Fimages\u002Fprojects\u002Fvue3-autocounter-demo.png\"\nimageAlt: \"The vue3-autocounter demo website showing animated counters\"\nreadingTime: 6\nlinks:\n  - label: \"View on npm\"\n    to: \"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fvue3-autocounter\"\n    icon: \"pi pi-code\"\n  - label: \"View on GitHub\"\n    to: \"https:\u002F\u002Fgithub.com\u002Fps-cristopher\u002Fvue3-autocounter\"\n    icon: \"pi pi-github\"\n  - label: \"Live demo and docs\"\n    to: \"https:\u002F\u002Fvue3autocounter.cristopherps.dev\"\n    icon: \"pi pi-globe\"\nfaq:\n  - question: \"What is vue3-autocounter?\"\n    answer: \"vue3-autocounter is an open-source Vue 3 component, published on npm, that animates a number from a start amount to an end amount over a configurable duration. It is written in TypeScript and can count up or down.\"\n  - question: \"How do I install vue3-autocounter?\"\n    answer: \"Run npm install vue3-autocounter, then register the component globally with app.component('vue3-autocounter', Vue3Autocounter) or import it directly in a single file component.\"\n  - question: \"Can vue3-autocounter count down?\"\n    answer: \"Yes. If endAmount is lower than startAmount the component counts down automatically. No extra configuration is needed.\"\n  - question: \"Does vue3-autocounter work with Nuxt and server-side rendering?\"\n    answer: \"Yes. The package ships an SSR build, and the counting animation starts on the client after the component mounts. This website is built with Nuxt and uses it to animate its own download and star counts.\"\n  - question: \"How do I know when the animation has finished?\"\n    answer: \"Listen to the finished event, for example @finished=\\\"onFinished\\\". The component also emits a mounted event when it is ready.\"\n---\n\n## What is vue3-autocounter?\n\n**vue3-autocounter** is an open-source Vue 3 component, published as an npm package, that animates a number from a start value to an end value over a set duration. I built it as a small, reusable way to show statistics, metrics and prices that \"count up\" when a page loads, without pulling in a large animation library.\n\nIt has been downloaded more than :live-counter[176,000]{:end=\"176000\"} times from npm and has :live-counter[82]{:end=\"82\"} stars on GitHub. Both of those numbers are animated by the component itself.\n\nKey features:\n\n- Counts **up or down**. The direction comes from comparing `startAmount` and `endAmount`.\n- Configurable **duration** in seconds.\n- **Formatting**: decimals, thousands separator, decimal separator, prefix and suffix (for example `$1,250.00 USD`).\n- **Control methods**: `start`, `pause`, `resume` and `reset`, available through a template ref.\n- **Lifecycle events**: `mounted` and `finished`.\n- Written in **TypeScript**, with an SSR-compatible build.\n\n::project-embed{src=\"https:\u002F\u002Fvue3autocounter.cristopherps.dev\" title=\"vue3-autocounter live demo\"}\nThe [official vue3-autocounter website](https:\u002F\u002Fvue3autocounter.cristopherps.dev), with live examples of every option.\n::\n\n## How do you install vue3-autocounter?\n\nInstall it from npm:\n\n```sh\nnpm install vue3-autocounter\n```\n\n### Register it globally\n\nAdd it to your `main.js` (or `main.ts`) to make it available in every component:\n\n```js\nimport { createApp } from 'vue'\nimport Vue3Autocounter from 'vue3-autocounter'\nimport App from '.\u002FApp.vue'\n\ncreateApp(App)\n  .component('vue3-autocounter', Vue3Autocounter)\n  .mount('#app')\n```\n\n### Or import it in a single file component\n\n```vue\n\u003Cscript setup>\nimport Vue3Autocounter from 'vue3-autocounter'\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CVue3Autocounter :startAmount=\"0\" :endAmount=\"2021\" :duration=\"3\" \u002F>\n\u003C\u002Ftemplate>\n```\n\n## How do you use it?\n\nA complete example with formatting and an event listener:\n\n```vue\n\u003Ctemplate>\n  \u003Cvue3-autocounter\n    ref=\"counter\"\n    :startAmount=\"0\"\n    :endAmount=\"2021\"\n    :duration=\"3\"\n    prefix=\"$\"\n    suffix=\" USD\"\n    separator=\",\"\n    decimalSeparator=\".\"\n    :decimals=\"2\"\n    :autoinit=\"true\"\n    @finished=\"onFinished\"\n  \u002F>\n\u003C\u002Ftemplate>\n```\n\nThis renders `$2,021.00 USD` once the 3-second animation finishes.\n\n## Which props does vue3-autocounter support?\n\n| Prop | Type | Default | Description |\n| --- | --- | --- | --- |\n| `startAmount` | `Number` | `0` | Value the animation starts from. |\n| `endAmount` | `Number` | `0` (required) | Value the animation ends at. |\n| `duration` | `Number` | `3` | Animation length in seconds. Must be greater than 0. |\n| `autoinit` | `Boolean` | `true` | Start counting automatically on mount. |\n| `prefix` | `String` | `''` | Text shown before the number, e.g. `$`. |\n| `suffix` | `String` | `''` | Text shown after the number, e.g. `USD`. |\n| `separator` | `String` | `','` | Thousands separator. |\n| `decimalSeparator` | `String` | `'.'` | Decimal separator. |\n| `decimals` | `Number` | `0` | Number of decimal places. Must be 0 or greater. |\n\nChanging `startAmount`, `endAmount` or `duration` after mount resets the animation and runs it again with the new values. This makes it easy to animate numbers that come from an API.\n\n## How do you control the animation?\n\nUse a template ref to call the component's methods:\n\n```vue\n\u003Cscript setup>\nimport { ref } from 'vue'\n\nconst counter = ref(null)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cvue3-autocounter ref=\"counter\" :endAmount=\"1000\" :autoinit=\"false\" \u002F>\n\n  \u003Cbutton @click=\"counter.start()\">Start\u003C\u002Fbutton>\n  \u003Cbutton @click=\"counter.pause()\">Pause\u003C\u002Fbutton>\n  \u003Cbutton @click=\"counter.resume()\">Resume\u003C\u002Fbutton>\n  \u003Cbutton @click=\"counter.reset()\">Reset\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n```\n\n| Method | What it does |\n| --- | --- |\n| `start()` | Starts the animation from `startAmount`. |\n| `pause()` | Pauses the animation and keeps the current value. |\n| `resume()` | Continues from the current value for the remaining time. |\n| `reset()` | Goes back to `startAmount`. It restarts if `autoinit` is `true`. |\n\n| Event | When it fires |\n| --- | --- |\n| `mounted` | The component has mounted and is ready. |\n| `finished` | The counter has reached `endAmount`. |\n\n## How does it work under the hood?\n\nThe component uses `window.requestAnimationFrame` instead of `setInterval`. Each frame, it calculates how much of the duration has passed and moves the current value forward by that proportion. This keeps the animation smooth and in sync with the browser's repaint cycle, and the browser pauses it automatically in background tabs.\n\nFormatting runs in a computed property. The number goes through `toFixed(decimals)`, gets split into integer and decimal parts, and the thousands separator is inserted with a regular expression. Because it is computed, the displayed string only recalculates when the value changes.\n\nPausing stores the remaining time and the current value. `resume()` then starts a new animation from that point, so pause and resume never make the counter jump.\n\n## Using it with Nuxt\n\nThe package ships an SSR build, so it works in Nuxt out of the box. Import it in a page or component and the server renders it; the counting starts once the page hydrates in the browser. This site, built with Nuxt, uses vue3-autocounter for the download and star counts above.\n",{"title":5,"description":1127},{"loc":1159},"blog\u002Fvue3-autocounter",[1165,111,152,1166,1167],"Vue 3","Open source","Components","vue3-autocounter is a lightweight, open-source Vue 3 component written in TypeScript that animates a number from a start value to an end value over a set duration. It counts up or down and supports decimals, thousands separators, prefixes, suffixes, pause\u002Fresume and a finished event. Install it with npm install vue3-autocounter.",null,"h0mybk_JlIC0sSofElePJek_b5Zjc0RpiYtRVaLvfAE",[1172,1177],{"title":1173,"path":1174,"stem":1175,"description":1176,"children":-1},"Pokémon shopping cart with Vue 3, Axios and the PokéAPI","\u002Fblog\u002Fpokemon-shopping-cart-vue3","blog\u002Fpokemon-shopping-cart-vue3","How a Vue 3 shopping cart demo consumes the PokéAPI REST API: batched lazy loading with Promise.all, client-side pagination, a reactive cart with computed totals, loading and error states, and a simulated checkout.",{"title":1178,"path":1179,"stem":1180,"description":1181,"children":-1},"Building a TODO app with the Vue 3 Composition API and localStorage","\u002Fblog\u002Fvue3-todo-app-composition-api","blog\u002Fvue3-todo-app-composition-api","A walkthrough of a Vue 3 TODO app built with ref, computed and onMounted: reactive task state, To-Do and Done tabs, UUID task IDs, confirmation flows and localStorage persistence.",1792306174062]