Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
Custom animations with more than 1 animation separeted by commas dont work
### What version of Tailwind CSS are you using?

v2.2.4

### What build tool (or framework if it abstracts the build tool) are you using?

Next.js 11.0.1

### What version of Node.js are you using?

v16.4.2

### What browser are you using?

Chrome

### What operating system are you using?

Windows

### Reproduction repository

https://play.tailwindcss.com/si3enE3TvA

### Describe your issue

On `tailwind.config.js`
```js
theme: {
  extend: {
    animation: {
      issue: 'animation1, animation2',
    }
  }
}
```
adding multiple animations makes the keyframes disappear from the styled component which makes the animation not work


Hints:
Relevant codepen with expected result
https://codepen.io/timber1900/pen/rNmwEvq?editors=1100

@Timber1900 Sounds like same problem as mine #4711. Combined animations are not being processed ( intentionally ? ) due to which keyframes are ignored. I tried #4802 but closed due to inaction. Defining animation along with keyframes in CSS should work for now.
I had a mistake in my reproduction repository and while I could have sworn it wasn't working when I created the issue as I was testing in another Tailwind Play it appears to now be working https://play.tailwindcss.com/5t0tNwzics

I'll be closing this issue as the problem appears to have fixed itself.
I'm reopening this issue because I found managed to reproduce the issue, 

- Removing the animations of the other divs com the `tailwind.config.js` removes the keyframes from the css causing the animation to not work
https://play.tailwindcss.com/mFR8PkYYKO?file=config

- But if we include them it now works
https://play.tailwindcss.com/gso8V7sBOc?file=config

Therefore the issue is with tailwind not including the keyframes in the compiled css when the only animation using said keyframes includes multiple animations
I'm seeing this issue too, keyframes are ignored when a `,` is used in the animation. Temporary workaround is to create `animation-1` and `animation-2` for `keyframes-1` and `keyframes-2` respectively (as opposed to `animation: keyframes-1, keyframes-2`), and then add those animation classes to the safelist.

Patch:
diff --git a/src/plugins/animation.js b/src/plugins/animation.js
--- a/src/plugins/animation.js
+++ b/src/plugins/animation.js
@@ -19,18 +19,23 @@ export default function () {
     matchUtilities(
       {
         animate: (value, { includeRules }) => {
-          let { name: animationName } = parseAnimationValue(value)
+          let animations = parseAnimationValue(value)
 
-          if (keyframes[animationName] !== undefined) {
-            includeRules(keyframes[animationName], { respectImportant: false })
-          }
-
-          if (animationName === undefined || keyframes[animationName] === undefined) {
-            return { animation: value }
+          for (let { name } of animations) {
+            if (keyframes[name] !== undefined) {
+              includeRules(keyframes[name], { respectImportant: false })
+            }
           }
 
           return {
-            animation: value.replace(animationName, prefixName(animationName)),
+            animation: animations
+              .map(({ name, value }) => {
+                if (name === undefined || keyframes[name] === undefined) {
+                  return value
+                }
+                return value.replace(name, prefixName(name))
+              })
+              .join(', '),
           }
         },
       },
diff --git a/src/util/parseAnimationValue.js b/src/util/parseAnimationValue.js
--- a/src/util/parseAnimationValue.js
+++ b/src/util/parseAnimationValue.js
@@ -20,9 +20,10 @@ const DIGIT = /^(\d+)$/
 
 export default function parseAnimationValue(input) {
   let animations = input.split(COMMA)
-  let result = animations.map((animation) => {
-    let result = {}
-    let parts = animation.trim().split(SPACE)
+  return animations.map((animation) => {
+    let value = animation.trim()
+    let result = { value }
+    let parts = value.split(SPACE)
     let seen = new Set()
 
     for (let part of parts) {
@@ -58,6 +59,4 @@ export default function parseAnimationValue(input) {
 
     return result
   })
-
-  return animations.length > 1 ? result : result[0]
 }


Test patch:
diff --git a/tests/jit/animations.test.css b/tests/jit/animations.test.css
deleted file mode 100644
--- a/tests/jit/animations.test.css
+++ /dev/null
@@ -1,32 +0,0 @@
-@keyframes spin {
-  to {
-    transform: rotate(360deg);
-  }
-}
-.animate-spin {
-  animation: spin 1s linear infinite;
-}
-@keyframes ping {
-  75%,
-  100% {
-    transform: scale(2);
-    opacity: 0;
-  }
-}
-.hover\:animate-ping:hover {
-  animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
-}
-@keyframes bounce {
-  0%,
-  100% {
-    transform: translateY(-25%);
-    animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
-  }
-  50% {
-    transform: none;
-    animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
-  }
-}
-.group:hover .group-hover\:animate-bounce {
-  animation: bounce 1s infinite;
-}
diff --git a/tests/jit/animations.test.html b/tests/jit/animations.test.html
deleted file mode 100644
--- a/tests/jit/animations.test.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="animate-spin"></div>
-<div class="hover:animate-ping"></div>
-<div class="group-hover:animate-bounce"></div>
diff --git a/tests/jit/animations.test.js b/tests/jit/animations.test.js
--- a/tests/jit/animations.test.js
+++ b/tests/jit/animations.test.js
@@ -1,5 +1,4 @@
 import postcss from 'postcss'
-import fs from 'fs'
 import path from 'path'
 import tailwind from '../../src/jit/index.js'
 
@@ -9,22 +8,199 @@ function run(input, config = {}) {
   })
 }
 
-test('animations', () => {
+test('basic', () => {
   let config = {
-    darkMode: 'class',
     mode: 'jit',
-    purge: [path.resolve(__dirname, './animations.test.html')],
-    corePlugins: {},
-    theme: {},
-    plugins: [],
+    purge: [
+      {
+        raw: `
+          <div class="animate-spin"></div>
+          <div class="hover:animate-ping"></div>
+          <div class="group-hover:animate-bounce"></div>
+        `,
+      },
+    ],
   }
 
   let css = `@tailwind utilities`
 
   return run(css, config).then((result) => {
-    let expectedPath = path.resolve(__dirname, './animations.test.css')
-    let expected = fs.readFileSync(expectedPath, 'utf8')
+    expect(result.css).toMatchFormattedCss(`
+      @keyframes spin {
+        to {
+          transform: rotate(360deg);
+        }
+      }
+      .animate-spin {
+        animation: spin 1s linear infinite;
+      }
+      @keyframes ping {
+        75%,
+        100% {
+          transform: scale(2);
+          opacity: 0;
+        }
+      }
+      .hover\\:animate-ping:hover {
+        animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
+      }
+      @keyframes bounce {
+        0%,
+        100% {
+          transform: translateY(-25%);
+          animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
+        }
+        50% {
+          transform: none;
+          animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
+        }
+      }
+      .group:hover .group-hover\\:animate-bounce {
+        animation: bounce 1s infinite;
+      }
+    `)
+  })
+})
+
+test('custom', () => {
+  let config = {
+    mode: 'jit',
+    purge: [{ raw: `<div class="animate-one"></div>` }],
+    theme: {
+      extend: {
+        keyframes: {
+          one: { to: { transform: 'rotate(360deg)' } },
+        },
+        animation: {
+          one: 'one 2s',
+        },
+      },
+    },
+  }
+
+  let css = `@tailwind utilities`
+
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      @keyframes one {
+        to {
+          transform: rotate(360deg);
+        }
+      }
+      .animate-one {
+        animation: one 2s;
+      }
+    `)
+  })
+})
+
+test('custom prefixed', () => {
+  let config = {
+    mode: 'jit',
+    prefix: 'tw-',
+    purge: [{ raw: `<div class="tw-animate-one"></div>` }],
+    theme: {
+      extend: {
+        keyframes: {
+          one: { to: { transform: 'rotate(360deg)' } },
+        },
+        animation: {
+          one: 'one 2s',
+        },
+      },
+    },
+  }
+
+  let css = `@tailwind utilities`
 
-    expect(result.css).toMatchFormattedCss(expected)
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      @keyframes tw-one {
+        to {
+          transform: rotate(360deg);
+        }
+      }
+      .tw-animate-one {
+        animation: tw-one 2s;
+      }
+    `)
+  })
+})
+
+test('multiple', () => {
+  let config = {
+    mode: 'jit',
+    purge: [{ raw: `<div class="animate-multiple"></div>` }],
+    theme: {
+      extend: {
+        animation: {
+          multiple: 'bounce 2s linear, pulse 3s ease-in',
+        },
+      },
+    },
+  }
+
+  let css = `@tailwind utilities`
+
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      @keyframes bounce {
+        0%,
+        100% {
+          transform: translateY(-25%);
+          animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
+        }
+        50% {
+          transform: none;
+          animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
+        }
+      }
+      @keyframes pulse {
+        50% {
+          opacity: 0.5;
+        }
+      }
+      .animate-multiple {
+        animation: bounce 2s linear, pulse 3s ease-in;
+      }
+    `)
+  })
+})
+
+test('multiple custom', () => {
+  let config = {
+    mode: 'jit',
+    purge: [{ raw: `<div class="animate-multiple"></div>` }],
+    theme: {
+      extend: {
+        keyframes: {
+          one: { to: { transform: 'rotate(360deg)' } },
+          two: { to: { transform: 'scale(1.23)' } },
+        },
+        animation: {
+          multiple: 'one 2s, two 3s',
+        },
+      },
+    },
+  }
+
+  let css = `@tailwind utilities`
+
+  return run(css, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(`
+      @keyframes one {
+        to {
+          transform: rotate(360deg);
+        }
+      }
+      @keyframes two {
+        to {
+          transform: scale(1.23);
+        }
+      }
+      .animate-multiple {
+        animation: one 2s, two 3s;
+      }
+    `)
   })
 })
diff --git a/tests/parseAnimationValue.test.js b/tests/parseAnimationValue.test.js
--- a/tests/parseAnimationValue.test.js
+++ b/tests/parseAnimationValue.test.js
@@ -5,6 +5,7 @@ describe('Tailwind Defaults', () => {
     [
       'spin 1s linear infinite',
       {
+        value: 'spin 1s linear infinite',
         name: 'spin',
         duration: '1s',
         timingFunction: 'linear',
@@ -14,15 +15,26 @@ describe('Tailwind Defaults', () => {
     [
       'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
       {
+        value: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
         name: 'ping',
         duration: '1s',
         timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
         iterationCount: 'infinite',
       },
     ],
-    ['bounce 1s infinite', { name: 'bounce', duration: '1s', iterationCount: 'infinite' }],
+    [
+      'bounce 1s infinite',
+      {
+        value: 'bounce 1s infinite',
+        name: 'bounce',
+        duration: '1s',
+        iterationCount: 'infinite',
+      },
+    ],
   ])('should be possible to parse: "%s"', (input, expected) => {
-    expect(parseAnimationValue(input)).toEqual(expected)
+    const parsed = parseAnimationValue(input)
+    expect(parsed).toHaveLength(1)
+    expect(parsed[0]).toEqual(expected)
   })
 })
 
@@ -31,6 +43,7 @@ describe('MDN Examples', () => {
     [
       '3s ease-in 1s 2 reverse both paused slidein',
       {
+        value: '3s ease-in 1s 2 reverse both paused slidein',
         delay: '1s',
         direction: 'reverse',
         duration: '3s',
@@ -44,15 +57,18 @@ describe('MDN Examples', () => {
     [
       'slidein 3s linear 1s',
       {
+        value: 'slidein 3s linear 1s',
         delay: '1s',
         duration: '3s',
         name: 'slidein',
         timingFunction: 'linear',
       },
     ],
-    ['slidein 3s', { duration: '3s', name: 'slidein' }],
+    ['slidein 3s', { value: 'slidein 3s', duration: '3s', name: 'slidein' }],
   ])('should be possible to parse: "%s"', (input, expected) => {
-    expect(parseAnimationValue(input)).toEqual(expected)
+    const parsed = parseAnimationValue(input)
+    expect(parsed).toHaveLength(1)
+    expect(parsed[0]).toEqual(expected)
   })
 })
 
@@ -83,8 +99,9 @@ describe('duration & delay', () => {
     ['spin -200.321ms -100.321ms linear', { duration: '-200.321ms', delay: '-100.321ms' }],
   ])('should be possible to parse "%s" into %o', (input, { duration, delay }) => {
     const parsed = parseAnimationValue(input)
-    expect(parsed.duration).toEqual(duration)
-    expect(parsed.delay).toEqual(delay)
+    expect(parsed).toHaveLength(1)
+    expect(parsed[0].duration).toEqual(duration)
+    expect(parsed[0].delay).toEqual(delay)
   })
 })
 
@@ -106,7 +123,9 @@ describe('iteration count', () => {
   ])(
     'should be possible to parse "%s" with an iteraction count of "%s"',
     (input, iterationCount) => {
-      expect(parseAnimationValue(input).iterationCount).toEqual(iterationCount)
+      const parsed = parseAnimationValue(input)
+      expect(parsed).toHaveLength(1)
+      expect(parsed[0].iterationCount).toEqual(iterationCount)
     }
   )
 })
@@ -123,18 +142,21 @@ describe('multiple animations', () => {
     expect(parsed).toHaveLength(3)
     expect(parsed).toEqual([
       {
+        value: 'spin 1s linear infinite',
         name: 'spin',
         duration: '1s',
         timingFunction: 'linear',
         iterationCount: 'infinite',
       },
       {
+        value: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
         name: 'ping',
         duration: '1s',
         timingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
         iterationCount: 'infinite',
       },
       {
+        value: 'pulse 2s cubic-bezier(0.4, 0, 0.6) infinite',
         name: 'pulse',
         duration: '2s',
         timingFunction: 'cubic-bezier(0.4, 0, 0.6)',
@@ -145,20 +167,21 @@ describe('multiple animations', () => {
 })
 
 it.each`
-  input                                                           | direction    | playState    | fillMode       | iterationCount | timingFunction | duration | delay   | name
-  ${'1s spin 1s infinite'}                                        | ${undefined} | ${undefined} | ${undefined}   | ${'infinite'}  | ${undefined}   | ${'1s'}  | ${'1s'} | ${'spin'}
-  ${'infinite infinite 1s 1s'}                                    | ${undefined} | ${undefined} | ${undefined}   | ${'infinite'}  | ${undefined}   | ${'1s'}  | ${'1s'} | ${'infinite'}
-  ${'ease 1s ease 1s'}                                            | ${undefined} | ${undefined} | ${undefined}   | ${undefined}   | ${'ease'}      | ${'1s'}  | ${'1s'} | ${'ease'}
-  ${'normal paused backwards infinite ease-in 1s 2s name'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'paused backwards infinite ease-in 1s 2s name normal'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'backwards infinite ease-in 1s 2s name normal paused'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'infinite ease-in 1s 2s name normal paused backwards'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'ease-in 1s 2s name normal paused backwards infinite'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'1s 2s name normal paused backwards infinite ease-in'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'2s name normal paused backwards infinite ease-in 1s'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'2s'}  | ${'1s'} | ${'name'}
-  ${'name normal paused backwards infinite ease-in 1s 2s'}        | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
-  ${'  name   normal  paused backwards infinite ease-in 1s 2s  '} | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  input                                                           | value                                                       | direction    | playState    | fillMode       | iterationCount | timingFunction | duration | delay   | name
+  ${'1s spin 1s infinite'}                                        | ${'1s spin 1s infinite'}                                    | ${undefined} | ${undefined} | ${undefined}   | ${'infinite'}  | ${undefined}   | ${'1s'}  | ${'1s'} | ${'spin'}
+  ${'infinite infinite 1s 1s'}                                    | ${'infinite infinite 1s 1s'}                                | ${undefined} | ${undefined} | ${undefined}   | ${'infinite'}  | ${undefined}   | ${'1s'}  | ${'1s'} | ${'infinite'}
+  ${'ease 1s ease 1s'}                                            | ${'ease 1s ease 1s'}                                        | ${undefined} | ${undefined} | ${undefined}   | ${undefined}   | ${'ease'}      | ${'1s'}  | ${'1s'} | ${'ease'}
+  ${'normal paused backwards infinite ease-in 1s 2s name'}        | ${'normal paused backwards infinite ease-in 1s 2s name'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'paused backwards infinite ease-in 1s 2s name normal'}        | ${'paused backwards infinite ease-in 1s 2s name normal'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'backwards infinite ease-in 1s 2s name normal paused'}        | ${'backwards infinite ease-in 1s 2s name normal paused'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'infinite ease-in 1s 2s name normal paused backwards'}        | ${'infinite ease-in 1s 2s name normal paused backwards'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'ease-in 1s 2s name normal paused backwards infinite'}        | ${'ease-in 1s 2s name normal paused backwards infinite'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'1s 2s name normal paused backwards infinite ease-in'}        | ${'1s 2s name normal paused backwards infinite ease-in'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'2s name normal paused backwards infinite ease-in 1s'}        | ${'2s name normal paused backwards infinite ease-in 1s'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'2s'}  | ${'1s'} | ${'name'}
+  ${'name normal paused backwards infinite ease-in 1s 2s'}        | ${'name normal paused backwards infinite ease-in 1s 2s'}    | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
+  ${'  name   normal  paused backwards infinite ease-in 1s 2s  '} | ${'name   normal  paused backwards infinite ease-in 1s 2s'} | ${'normal'}  | ${'paused'}  | ${'backwards'} | ${'infinite'}  | ${'ease-in'}   | ${'1s'}  | ${'2s'} | ${'name'}
 `('should parse "$input" correctly', ({ input, ...expected }) => {
   let parsed = parseAnimationValue(input)
-  expect(parsed).toEqual(expected)
+  expect(parsed).toHaveLength(1)
+  expect(parsed[0]).toEqual(expected)
 })