diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e1d9a479..f3ef1455a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## Not released
 - Fix CategoryWidgetUI displaying no data while loading [#26](https://github.com/CartoDB/carto-react-lib/pull/26)
+- Animate CategoryWidget values [#30](https://github.com/CartoDB/carto-react-lib/pull/30)
 - Make OAuthLogin component responsive [#28](https://github.com/CartoDB/carto-react-lib/pull/28)
 
 ## 1.0.0-beta5 (2020-11-25)
diff --git a/src/ui/widgets/CategoryWidgetUI.js b/src/ui/widgets/CategoryWidgetUI.js
index e78d29311..5c8df3885 100644
--- a/src/ui/widgets/CategoryWidgetUI.js
+++ b/src/ui/widgets/CategoryWidgetUI.js
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
 import PropTypes from 'prop-types';
 import {
   Button,
@@ -102,6 +102,14 @@ const useStyles = makeStyles((theme) => ({
   },
 }));
 
+function usePrevious(value) {
+  const ref = useRef();
+  useEffect(() => {
+    ref.current = value;
+  });
+  return ref.current;
+}
+
 const REST_CATEGORY = '__rest__';
 
 const SearchIcon = () => (
@@ -122,6 +130,8 @@ function CategoryWidgetUI(props) {
   const [searchValue, setSearchValue] = useState('');
   const [blockedCategories, setBlockedCategories] = useState([]);
   const [tempBlockedCategories, setTempBlockedCategories] = useState(false);
+  const [animValues, setAnimValues] = useState([]);
+  const prevAnimValues = usePrevious(animValues);
   const classes = useStyles();
 
   const handleCategorySelected = (category) => {
@@ -294,6 +304,43 @@ function CategoryWidgetUI(props) {
     showAll,
   ]);
 
+  useEffect(() => {
+    animateValues(prevAnimValues || [], sortedData, 500, (val) => setAnimValues(val));
+  }, [sortedData]);
+
+  const animateValues = (start, end, duration, drawFrame) => {
+    const isEqual = start.length === end.length && start.every((val, i) => val.value === end[i].value);
+    if (isEqual) return;
+
+    let currentValues = end.map((elem, i) => start[i] && start[i].category === elem.category
+      ? { ...elem, value: start[i].value }
+      : elem
+    );
+    let currentFrame = 0;
+    
+    const ranges = currentValues.map((elem, i) => end[i].value - elem.value);
+    const noChanges = ranges.every((val) => val === 0);
+    if (noChanges) {
+      drawFrame(end);
+      return;
+    }
+
+    const frames = (duration / 1000) * 60;
+    const steps = ranges.map((val) => val / frames);
+
+    const animate = () => {
+      if (currentFrame < frames) {
+        currentValues = currentValues.map((elem, i) => ({...elem, value: elem.value + steps[i]}) );
+        drawFrame(currentValues);
+        currentFrame++;
+        requestAnimationFrame(animate);
+      } else {
+        drawFrame(end);
+      }
+    };
+    requestAnimationFrame(animate);
+  }
+
   // Separated to simplify the widget layout but inside the main component to avoid passing all dependencies
   const CategoryItem = (props) => {
     const { data, onCategoryClick } = props;
@@ -438,8 +485,8 @@ function CategoryWidgetUI(props) {
             </Grid>
           )}
           <Grid container item className={classes.categoriesWrapper}>
-            {sortedData.length
-              ? sortedData.map((d, i) => (
+            {animValues.length
+              ? animValues.map((d, i) => (
                   <CategoryItem
                     key={i}
                     data={d}