Repository: tailwindlabs/tailwindcss

Problem statement:
v4 alpha 0.28 breaks plugin (tailwindcss-animate) & weird behavior with new p3 color opacity
<!-- 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?**

v4.0.0-alpha.28

**What build tool (or framework if it abstracts the build tool) are you using?**

postcss 8.4.47, Next.js 15.0.0-rc.1

**What version of Node.js are you using?**

v20.16.0

**What browser are you using?**

Chrome

**What operating system are you using?**

For example: macOS

**Reproduction URL**

https://github.com/ajayvignesh01/tailwind-v4.0.0-alpha.28-plugins

**Describe your issue**

The `tailwindcss-animate` plugin was working since plugin support landed a few weeks ago. But when I upgrade to alpha 0.28, it stops working. No errors are thrown, but animations that rendered with alpha 0.21 - 0.27 are now not rendering.

Another bug is from the introduction of p3 colors. If you hover over the primary button which has `bg-primary hover:bg-primary/90`, the hover color shows a hue of green for some reason.

Preview with alpha 0.27 - https://tailwind-v4-0-0-alpha-28-plugins-gjaplla02.vercel.app/
Preview with alpha 0.28 - https://tailwind-v4-0-0-alpha-28-plugins-4pzuz8jxf.vercel.app/


Hints:
Dropping some notes for me to look at tomorrow:

Regarding the colors:
- This is only an issue in Chrome, FF and Safari are fine
- The trigger requires an oklch/okblab color with a `none` component. Replacing it with a `0` fixes the issue.
- Changing the color space of the color-mix _also_ fixes the issue (but should not be necessary)

```css
button {
  /* broken */
  background-color: color-mix(in srgb, oklch(0.985 0 none) 95%, transparent);

  /* works */
  background-color: color-mix(in srgb, oklch(0.985 0 0) 95%, transparent);
  background-color: color-mix(in oklch, oklch(0.985 0 none) 95%, transparent);
  background-color: color-mix(in oklab, oklch(0.985 0 none) 95%, transparent);
}
```

I think what's happening is that it gets converted into `color(srgb …)` internally, carries over the none component, and replaces the `none` at the end with `0` — instead of doing it before the conversion. Based on the following observations:

```css
button {
  /* this is yellow */
  background-color: color-mix(in srgb, color(srgb 1.0 1.0 none) 95%, transparent);

  /* as is this */
  background-color: color-mix(in srgb, color(srgb 1.0 1.0 0) 95%, transparent);
  
  /* window.getComputedStyle($0)["background-color"] produces a color(srgb …) value */
  background-color: color-mix(in srgb, oklch(0.985 0 none) 95%, transparent);
}
```

Patch:
diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts
--- a/packages/tailwindcss/src/compat/plugin-api.ts
+++ b/packages/tailwindcss/src/compat/plugin-api.ts
@@ -204,7 +204,7 @@ export function buildPluginApi(
 
       for (let [name, css] of Object.entries(utils)) {
         if (name.startsWith('@keyframes ')) {
-          designSystem.theme.addKeyframes(rule(name, objectToAst(css)))
+          ast.push(rule(name, objectToAst(css)))
           continue
         }
 


Test patch:
diff --git a/integrations/cli/plugins.test.ts b/integrations/cli/plugins.test.ts
--- a/integrations/cli/plugins.test.ts
+++ b/integrations/cli/plugins.test.ts
@@ -154,6 +154,7 @@ test(
       candidate`duration-350`,
       'transition-duration: 350ms',
       'animation-duration: 350ms',
+      '@keyframes enter {',
     ])
   },
 )
diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts
--- a/packages/tailwindcss/src/compat/plugin-api.test.ts
+++ b/packages/tailwindcss/src/compat/plugin-api.test.ts
@@ -71,6 +71,39 @@ describe('theme', async () => {
     `)
   })
 
+  test('keyframes added via addUtilities are appended to the AST', async () => {
+    let input = css`
+      @tailwind utilities;
+      @plugin "my-plugin";
+    `
+
+    let compiler = await compile(input, {
+      loadModule: async (id, base) => {
+        return {
+          base,
+          module: plugin(function ({ addUtilities, theme }) {
+            addUtilities({
+              '@keyframes enter': {
+                from: {
+                  opacity: 'var(--tw-enter-opacity, 1)',
+                },
+              },
+            })
+          }),
+        }
+      },
+    })
+
+    expect(compiler.build([])).toMatchInlineSnapshot(`
+      "@keyframes enter {
+        from {
+          opacity: var(--tw-enter-opacity, 1);
+        }
+      }
+      "
+    `)
+  })
+
   test('plugin theme can extend colors', async () => {
     let input = css`
       @theme reference {