Repository: tailwindlabs/tailwindcss

Problem statement:
Wrong keyframe keys when generationg CSS classes with JS
<!-- Please provide all of the information requested below. We're a small team and without all of this information it's not possible for us to help and your bug report will be closed. -->

**What version of Tailwind CSS are you using?**

For example: v3.0.15

**What build tool (or framework if it abstracts the build tool) are you using?**

postcss-cli 9.1.0, vite 2.7.13

**What version of Node.js are you using?**

16.13.2

**What browser are you using?**

Firefox

**What operating system are you using?**

Ubuntu 20.04

**Reproduction URL**

https://github.com/Mastercuber/tailwindcss-minimal-example

**Describe your issue**

In the config file `tailwind.config.js` I'm generating some animations with floating point numbers in the class name like [this](https://github.com/Mastercuber/tailwindcss-minimal-example/blob/a7c9450525434893077c03cb91754295f4cbea83/tailwind.config.js#L14).  
The generated animation classes looks good, but the `@keyframe` directives have a different value than the animation classes itself, though the exact same key is used for both. 

The used class in the App is `animate-zoom-2.5` to actually see it in the output and the generated css is [here](https://github.com/Mastercuber/tailwindcss-minimal-example/blob/a7c9450525434893077c03cb91754295f4cbea83/test.css#L427)

Now I just use another key for the keyframe name.


Hints:
Hey, @Mastercuber. What you're seeing there isn't the "wrong key" per se. Tailwind is mistakenly trying to prefix the "." in your animation value and keyframe name when it should instead be escaping them and then prefixing.
I will send the Tailwind team a PR to address this.

Patch:
diff --git a/src/corePlugins.js b/src/corePlugins.js
--- a/src/corePlugins.js
+++ b/src/corePlugins.js
@@ -3,6 +3,7 @@ import * as path from 'path'
 import postcss from 'postcss'
 import createUtilityPlugin from './util/createUtilityPlugin'
 import buildMediaQuery from './util/buildMediaQuery'
+import escapeClassName from './util/escapeClassName'
 import parseAnimationValue from './util/parseAnimationValue'
 import flattenColorPalette from './util/flattenColorPalette'
 import withAlphaVariable, { withAlphaValue } from './util/withAlphaVariable'
@@ -612,8 +613,8 @@ export let corePlugins = {
     })
   },
 
-  animation: ({ matchUtilities, theme, prefix }) => {
-    let prefixName = (name) => prefix(`.${name}`).slice(1)
+  animation: ({ matchUtilities, theme, config }) => {
+    let prefixName = (name) => `${config('prefix')}${escapeClassName(name)}`
     let keyframes = Object.fromEntries(
       Object.entries(theme('keyframes') ?? {}).map(([key, value]) => {
         return [key, { [`@keyframes ${prefixName(key)}`]: value }]


Test patch:
diff --git a/tests/animations.test.js b/tests/animations.test.js
--- a/tests/animations.test.js
+++ b/tests/animations.test.js
@@ -181,3 +181,96 @@ test('multiple custom', () => {
     `)
   })
 })
+
+test('with dots in the name', () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div class="animate-zoom-.5"></div>
+          <div class="animate-zoom-1.5"></div>
+        `,
+      },
+    ],
+    theme: {
+      extend: {
+        keyframes: {
+          'zoom-.5': { to: { transform: 'scale(0.5)' } },
+          'zoom-1.5': { to: { transform: 'scale(1.5)' } },
+        },
+        animation: {
+          'zoom-.5': 'zoom-.5 2s',
+          'zoom-1.5': 'zoom-1.5 2s',
+        },
+      },
+    },
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      @keyframes zoom-\.5 {
+        to {
+          transform: scale(0.5);
+        }
+      }
+      .animate-zoom-\.5 {
+        animation: zoom-\.5 2s;
+      }
+      @keyframes zoom-1\.5 {
+        to {
+          transform: scale(1.5);
+        }
+      }
+      .animate-zoom-1\.5 {
+        animation: zoom-1\.5 2s;
+      }
+    `)
+  })
+})
+
+test('with dots in the name and prefix', () => {
+  let config = {
+    prefix: 'tw-',
+    content: [
+      {
+        raw: html`
+          <div class="tw-animate-zoom-.5"></div>
+          <div class="tw-animate-zoom-1.5"></div>
+        `,
+      },
+    ],
+    theme: {
+      extend: {
+        keyframes: {
+          'zoom-.5': { to: { transform: 'scale(0.5)' } },
+          'zoom-1.5': { to: { transform: 'scale(1.5)' } },
+        },
+        animation: {
+          'zoom-.5': 'zoom-.5 2s',
+          'zoom-1.5': 'zoom-1.5 2s',
+        },
+      },
+    },
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      @keyframes tw-zoom-\.5 {
+        to {
+          transform: scale(0.5);
+        }
+      }
+      .tw-animate-zoom-\.5 {
+        animation: tw-zoom-\.5 2s;
+      }
+      @keyframes tw-zoom-1\.5 {
+        to {
+          transform: scale(1.5);
+        }
+      }
+      .tw-animate-zoom-1\.5 {
+        animation: tw-zoom-1\.5 2s;
+      }
+    `)
+  })
+})