jantimon / next-yak

a streamlined CSS-in-JS solution tailor-made for Next.js, seamlessly combining the expressive power of styled-components syntax with efficient build-time extraction and minimal runtime footprint, ensuring optimal performance and easy integration with existing atomic CSS frameworks like Tailwind CSS
https://yak.js.org
118 stars 4 forks source link

redefining css mixins #146

Closed jantimon closed 1 month ago

jantimon commented 3 months ago

Summary

Introducing a new const colorMixin = atom.css`color:red`; API and redefining the behavior of the existing css template literal in next-yak could solve the cross file css mixin complexity

Background

Currently, next-yak treats all css template literals as static, reusable atomic classes. While this approach works well for simple cases and promotes CSS reuse, it falls short in more complex scenarios, particularly when used within nested selectors or media queries

For example:

const borderMixin = css`
  border: 1px solid black;
  border-radius: 999px;
`; // becomes .border-mixin { border: 1px solid black; border-radius: 999px; }

const Button = styled.button`
  &:hover {
    ${borderMixin}
  }
  color: red;
`; // becomes <button class="button border-mixin">...</button?

In this case, the current implementation cannot correctly apply the borderMixin within the :hover selector, as it's treated as a static atomic class

@Mad-Kat pointed out that this is a core problem where a lot of automated atomic utility css extraction get very complex

Proposed Solution

Instead of creating atomic css mixins automatically we make it an optional feature, so developers can explicitly create an atom and are aware of the limitations or just use the ordinary css api with a little bit more bundled css code

So we would:

  1. Introduce a new atom.css API for creating static, atomic CSS mixins
  2. Redefine the behavior of the existing css template literal to always inline its contents when used
1. New atom.css API

The atom.css API will behave similarly to the current css implementation, creating a static, reusable atomic class:

const borderMixin = atom.css`
  border: 1px solid black;
  border-radius: 999px;
`;

This will be extracted as a utility atom with a class (e.g., border-mixin) and can be reused across the codebase, reducing bundled CSS size.

2. Redefined css Behavior

The css template literal will now inline its contents when used, allowing for proper nesting and selector combination:

const borderMixin = css`
  border: 1px solid black;
  border-radius: 999px;
`;

const Button = styled.button`
  &:hover {
    ${borderMixin}
  }
  color: red;
`;

This change allows the borderMixin to be correctly applied within the :hover selector.

Cross File behaviour

This could also work for cross file mixins.
Here is an example how the compilation would look like:

Source Code
import { borderMixin } from "./mixin";
const Button = styled.button`
  &:hover {
    ${borderMixin}
  }
  color: red;
`;
Compiled Output

The above code would be compiled to:

import { borderMixin } from "./mixin";
const Button =
/*YAK Extracted CSS:
.Button {
  &:hover { --yak-css-import: url("./mixin:borderMixin"); }
  color: red;
}*/
/*#__PURE__*/
styled.button(borderMixin, __styleYak.Button);
CSS Loader Processing

The CSS loader would then process the extracted CSS, replacing the special --yak-css-import property with the actual content of the borderMixin. For example, if borderMixin was defined as:

export const borderMixin = css`
  border: 1px solid black;
  border-radius: 999px;
`;

The final CSS output would be:

.Button {
  &:hover {
    border: 1px solid black;
    border-radius: 999px;
  }
  color: red;
}

As we keep the mixin reference also in the typescript code (styled.button(borderMixin, __styleYak.Button);) this approach could also work for dynamic mixins across files

jantimon commented 2 months ago

Here is what we would have to do for static css mixins:

  1. SWC Plugin Modifications:

    a. Identify static mixin definitions:

    • Look for exports of css template literals.

    b. Process mixin content:

    • Extract the content of the css template literal.
    • Use the export name as the mixin identifier.
    • Generate a comment out of the extracted code

    For example:

    Before compilation:

    // mixins.ts
    export const borderMixin = css`
     border: 1px solid black;
     border-radius: 999px;
    `;

    After compilation:

    // mixins.ts
    /*YAK Mixin:
    borderMixin:
    border: 1px solid black;
    border-radius: 999px;
    */
    export const borderMixin = null;

    d. Process mixin usage:

    • Replace mixin usage in css or styled template literals with a special import syntax.

    Before compilation:

    // Button.ts
    import { borderMixin } from './mixins';
    
    const Button = styled.button`
     ${borderMixin};
     color: blue;
    `;

    After compilation:

    // Button.ts
    import { borderMixin } from './mixins';
    
    const Button = 
    /*YAK Extracted CSS:
    .Button {
     --yak-css-import: url("./mixins:borderMixin");
     color: blue;
    }
    */
    /*#__PURE__*/
    styled.button(borderMixin, __styleYak.Button);
  2. CSS Loader Enhancements:

    a. Implement mixin resolution:

    • Resolve file paths and read mixin content from special comments.

    b. Replace mixin placeholders:

    • Substitute import statements with actual mixin content.

    Example:

    Extracted CSS from Button.ts:

    .Button {
     --yak-css-import: url("./mixins:borderMixin");
     color: blue;
    }

    Final processed CSS:

    .Button {
     border: 1px solid black;
     border-radius: 999px;
     color: blue;
    }
  3. Runtime Changes:

    a. We have to modify the runtime styled function:

    • Update to accept mixin arguments (which will be null at runtime).