diff --git a/css/reveal.css b/css/reveal.css
index 7af66ac7df5cf939693b6ea979b38925e325c345..34ace93b6949b1e39e6d629ea4528293ac952fc1 100644
--- a/css/reveal.css
+++ b/css/reveal.css
@@ -1233,6 +1233,9 @@ body {
 /*********************************************
  * AUTO ANIMATE
  *********************************************/
+.reveal [data-auto-animate-target="unmatched"] {
+  will-change: opacity; }
+
 .reveal section[data-auto-animate]:not(.stack):not([data-auto-animate="running"]) [data-auto-animate-target="unmatched"] {
   opacity: 0; }
 
@@ -1515,6 +1518,12 @@ body {
   width: 100%;
   box-sizing: border-box; }
 
+.reveal pre[data-auto-animate-target] {
+  overflow: hidden; }
+
+.reveal pre[data-auto-animate-target] code {
+  height: 100%; }
+
 /*********************************************
  * ROLLING LINKS
  *********************************************/
diff --git a/css/reveal.scss b/css/reveal.scss
index 51c7a9ec5f4b2d9429b6816222b0bedf5d332829..2bb76561664499d8bc9bc8802d7a210c8f208683 100644
--- a/css/reveal.scss
+++ b/css/reveal.scss
@@ -1318,6 +1318,10 @@ $controlsArrowAngleActive: 36deg;
  * AUTO ANIMATE
  *********************************************/
 
+.reveal [data-auto-animate-target="unmatched"] {
+	will-change: opacity;
+}
+
 .reveal section[data-auto-animate]:not(.stack):not([data-auto-animate="running"]) [data-auto-animate-target="unmatched"] {
 	opacity: 0;
 }
@@ -1663,6 +1667,13 @@ $controlsArrowAngleActive: 36deg;
 	box-sizing: border-box;
 }
 
+.reveal pre[data-auto-animate-target] {
+	overflow: hidden;
+}
+.reveal pre[data-auto-animate-target] code {
+	height: 100%;
+}
+
 
 /*********************************************
  * ROLLING LINKS
diff --git a/js/reveal.js b/js/reveal.js
index 6ec56dc9d1d4a3f201f808942b2684ee6075b950..d0e7394d9a6c465e28588ae4a779fd4fd8d21c8b 100644
--- a/js/reveal.js
+++ b/js/reveal.js
@@ -3058,7 +3058,7 @@
 		cueAutoSlide();
 
 		// Auto-animation
-		if( slideChanged && previousSlide && currentSlide ) {
+		if( slideChanged && previousSlide && currentSlide && !isOverview() ) {
 
 			// Skip the slide transition between our two slides
 			// when auto-animating individual elements
@@ -4028,6 +4028,7 @@
 			// Animate towards the 'to' state
 			toProps.styles['transition'] = 'all '+ options.duration +'s '+ options.easing + ' ' + options.delay + 's';
 			toProps.styles['transition-property'] = toStyleProperties.join( ', ' );
+			toProps.styles['will-change'] = toStyleProperties.join( ', ' );
 
 			// Build up our custom CSS. We need to override inline styles
 			// so we need to make our styles vErY IMPORTANT!1!!
@@ -4162,15 +4163,18 @@
 	 */
 	function getAutoAnimatableElements( fromSlide, toSlide ) {
 
-		var matcher = typeof config.autoAnimateMatcher === 'function' ? config.autoAnimateMatcher : findAutoAnimatePairs;
+		var matcher = typeof config.autoAnimateMatcher === 'function' ? config.autoAnimateMatcher : getAutoAnimatePairs;
 
 		var pairs = matcher( fromSlide, toSlide );
 
+		var reserved = [];
+
 		// Remove duplicate pairs
 		return pairs.filter( function( pair, index ) {
-			return index === pairs.findIndex( function( comparePair ) {
-				return pair.from === comparePair.from && pair.to === comparePair.to;
-			} );
+			if( reserved.indexOf( pair.to ) === -1 ) {
+				reserved.push( pair.to );
+				return true;
+			}
 		} );
 
 	}
@@ -4181,64 +4185,67 @@
 	 * You can specify a custom matcher function by using
 	 * the `autoAnimateMatcher` config option.
 	 */
-	function findAutoAnimatePairs( fromSlide, toSlide ) {
+	function getAutoAnimatePairs( fromSlide, toSlide ) {
 
 		var pairs = [];
 
-		var findMatches = function( selector, serializer, transformer ) {
-
-			var fromHash = {};
-
-			toArray( fromSlide.querySelectorAll( selector ) ).forEach( function( element ) {
-				if( typeof transformer === 'function' ) element = transformer( element );
-				fromHash[ serializer( element ) ] = element;
-			} );
-
-			toArray( toSlide.querySelectorAll( selector ) ).forEach( function( element ) {
-				if( typeof transformer === 'function' ) element = transformer( element );
-				var fromElement = fromHash[ serializer( element ) ];
-				if( fromElement ) {
-					pairs.push({ from: fromElement, to: element });
-				}
-			} );
-
-		};
-
-		var textNodes = 'h1, h2, h3, h4, h5, h6, p, li, span';
+		var codeNodes = 'pre';
+		var textNodes = 'h1, h2, h3, h4, h5, h6, p, li';
 		var mediaNodes = 'img, video, iframe';
 
 		// Eplicit matches via data-id
-		findMatches( '[data-id]', function( node ) {
+		findAutoAnimateMatches( pairs, fromSlide, toSlide, '[data-id]', function( node ) {
 			return node.nodeName + ':::' + node.getAttribute( 'data-id' );
 		} );
 
 		// Text
-		findMatches( textNodes, function( node ) {
+		findAutoAnimateMatches( pairs, fromSlide, toSlide, textNodes, function( node ) {
 			return node.nodeName + ':::' + node.innerText;
-		}, null );
+		} );
 
 		// Media
-		findMatches( mediaNodes, function( node ) {
+		findAutoAnimateMatches( pairs, fromSlide, toSlide, mediaNodes, function( node ) {
 			return node.nodeName + ':::' + ( node.getAttribute( 'src' ) || node.getAttribute( 'data-src' ) );
 		} );
 
 		// Code
-		findMatches( 'pre>code', function( node ) {
+		findAutoAnimateMatches( pairs, fromSlide, toSlide, codeNodes, function( node ) {
 			return node.nodeName + ':::' + node.innerText;
-		}, function( element ) {
-			return element.parentNode;
 		} );
 
 		pairs.forEach( function( pair ) {
 
-			var fromElement = pair.from;
-			var matchesMethod = fromElement.matches || fromElement.matchesSelector || fromElement.msMatchesSelector;
-
 			// Disable scale transformations on text nodes, we transiition
 			// each individual text property instead
-			if( matchesMethod.call( fromElement, textNodes ) ) {
+			if( pair.from.matches( textNodes ) ) {
 				pair.options = { scale: false };
 			}
+			// Animate individual lines of code
+			else if( pair.from.matches( codeNodes ) ) {
+
+				// Transition the code block's width and height instead of scaling
+				// to prevent its content from being squished
+				pair.options = { scale: false, styles: [ 'width', 'height' ] };
+
+				// Lines of code
+				findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-code', function( node ) {
+					return node.textContent;
+				}, {
+					scale: false,
+					styles: [],
+					measure: getLocalBoundingBox
+				} );
+
+				// Line numbers
+				findAutoAnimateMatches( pairs, pair.from, pair.to, '.hljs .hljs-ln-line[data-line-number]', function( node ) {
+					return node.getAttribute( 'data-line-number' );
+				}, {
+					scale: false,
+					styles: [ 'width' ],
+					measure: getLocalBoundingBox
+				} );
+
+			}
 
 		} );
 
@@ -4246,6 +4253,88 @@
 
 	}
 
+	/**
+	 * Helper method which returns a bounding box based on
+	 * the given elements offset coordinates.
+	 *
+	 * @param {HTMLElement} element
+	 * @return {Object} x, y, width, height
+	 */
+	function getLocalBoundingBox( element ) {
+
+		var presentationScale = Reveal.getScale();
+
+		return {
+			x: Math.round( ( element.offsetLeft * presentationScale ) * 100 ) / 100,
+			y: Math.round( ( element.offsetTop * presentationScale ) * 100 ) / 100,
+			width: Math.round( ( element.offsetWidth * presentationScale ) * 100 ) / 100,
+			height: Math.round( ( element.offsetHeight * presentationScale ) * 100 ) / 100
+		};
+
+	}
+
+	/**
+	 * Finds matching elements between two slides.
+	 *
+	 * @param {Array} pairs            	List of pairs to push matches to
+	 * @param {HTMLElement} fromScope   Scope within the from element exists
+	 * @param {HTMLElement} toScope     Scope within the to element exists
+	 * @param {String} selector         CSS selector of the element to match
+	 * @param {Function} serializer     A function that accepts an element and returns
+	 *                                  a stringified ID based on its contents
+	 * @param {Object} animationOptions Optional config options for this pair
+	 */
+	function findAutoAnimateMatches( pairs, fromScope, toScope, selector, serializer, animationOptions ) {
+
+		var fromMatches = {};
+		var toMatches = {};
+
+		[].slice.call( fromScope.querySelectorAll( selector ) ).forEach( function( element, i ) {
+			var key = serializer( element );
+			if( typeof key === 'string' && key.length ) {
+				fromMatches[key] = fromMatches[key] || [];
+				fromMatches[key].push( element );
+			}
+		} );
+
+		[].slice.call( toScope.querySelectorAll( selector ) ).forEach( function( element, i ) {
+			var key = serializer( element );
+			toMatches[key] = toMatches[key] || [];
+			toMatches[key].push( element );
+
+			var fromElement;
+
+			// Retrieve the 'from' element
+			if( fromMatches[key] ) {
+				var pimaryIndex = toMatches[key].length - 1;
+				var secondaryIndex = fromMatches[key].length - 1;
+
+				// If there are multiple identical from elements, retrieve
+				// the one at the same index as our to-element.
+				if( fromMatches[key][ pimaryIndex ] ) {
+					fromElement = fromMatches[key][ pimaryIndex ];
+					fromMatches[key][ pimaryIndex ] = null;
+				}
+				// If there are no matching from-elements at the same index,
+				// use the last one.
+				else if( fromMatches[key][ secondaryIndex ] ) {
+					fromElement = fromMatches[key][ secondaryIndex ];
+					fromMatches[key][ secondaryIndex ] = null;
+				}
+			}
+
+			// If we've got a matching pair, push it to the list of pairs
+			if( fromElement ) {
+				pairs.push({
+					from: fromElement,
+					to: element,
+					options: animationOptions
+				});
+			}
+		} );
+
+	}
+
 	/**
 	 * Returns a all elements within the given scope that should
 	 * be considered unmatched in an auto-animate transition. If
diff --git a/test/examples/auto-animate.html b/test/examples/auto-animate.html
index c81b949ec57f110c01dedcec94fd008570be8ba9..c353748b5839ec0ab9e2dbc411f7fdd0f9948a7d 100644
--- a/test/examples/auto-animate.html
+++ b/test/examples/auto-animate.html
@@ -23,7 +23,7 @@
 					<h3>Auto-Animate Example</h3>
 					<p>This will fade out</p>
 					<img src="assets/image1.png" style="height: 100px;">
-					<pre><code class="hljs" data-trim>
+					<pre data-id="code"><code data-line-numbers class="hljs" data-trim>
 						function Example() {
 						  const [count, setCount] = useState(0);
 						}
@@ -34,8 +34,9 @@
 					<p style="opacity: 0.2; margin-top: 200px;">This will fade out</p>
 					<p>This element is unmatched</p>
 					<img src="assets/image1.png" style="height: 100px;">
-					<pre><code class="hljs" data-trim>
+					<pre data-id="code"><code data-line-numbers class="hljs" data-trim>
 						function Example() {
+						  New line!
 						  const [count, setCount] = useState(0);
 						}
 					</code></pre>
@@ -48,6 +49,36 @@
 					<p data-id="text-props" style="background: #555; line-height: 3em; letter-spacing: 0.2em;">Line Height & Letter Spacing</p>
 				</section>
 
+				<section data-auto-animate>
+					<pre data-id="code"><code data-line-numbers class="hljs" data-trim>
+						import React, { useState } from 'react';
+
+						function Example() {
+						  const [count, setCount] = useState(0);
+
+						  return (
+						    ...
+						  );
+						}
+					</code></pre>
+				</section>
+				<section data-auto-animate>
+					<pre data-id="code"><code data-line-numbers class="hljs" data-trim>
+						function Example() {
+						  const [count, setCount] = useState(0);
+
+						  return (
+						    &lt;div&gt;
+						      &lt;p&gt;You clicked {count} times&lt;/p&gt;
+						      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;
+						        Click me
+						      &lt;/button&gt;
+						    &lt;/div&gt;
+						  );
+						}
+					</code></pre>
+				</section>
+
 				<section>
 					<section data-auto-animate>
 						<h3>Swapping list items</h3>