diff --git a/packages/docs/docs/pages/features.md b/packages/docs/docs/pages/features.md
index 132c09ae..eb43a19e 100644
--- a/packages/docs/docs/pages/features.md
+++ b/packages/docs/docs/pages/features.md
@@ -1,6 +1,17 @@
# Features
next-yak is a featured packed static CSS-in-JS framework with a minimal runtime aspect.
+## Table of contents
+1. [Static CSS](#static-css)
+2. [Dynamic styles](#dynamic-styles)
+3. [Compatible with utility-first CSS frameworks (e.g. Tailwind)](#compatible-with-utility-first-css-frameworks-eg-tailwind)
+4. [Animations](#animations)
+5. [Mixins](#mixins)
+6. [Automatic CSS variables](#automatic-css-variables)
+7. [Theming](#theming)
+8. [CSS Prop](#css-prop)
+
+
## Static CSS
At the heart of next-yak lies the extraction of static CSS.
@@ -497,3 +508,45 @@ const Button = styled.button`
`}
`;
```
+
+:::
+
+## CSS Prop
+
+We support out of the box the `css` prop which is a shorthand for adding styles to an element. Similiar to inline-styles
+it allows you to write local styles for certain elements on your page. Differently than inline-styles, it allows you to use
+selectors that target wrapped elements.
+
+```jsx
+import { css } from 'next-yak';
+
+const Component = () => {
+ return
props.theme.colors.text};
+ `}
+/>
+
+```
+
+```tsx [next-yak]
+import { css } from 'next-yak';
+
+
+
+```
+
+:::
+
+If you use TypeScript, you can just add the following to your `tsconfig.json` to get type checking for the css prop.
+
+```json
+{
+ "compilerOptions": {
+ "jsxImportSource": "next-yak"
+ }
+}
+```
+
+
### keyframes
The api for keyframes is exactly the same as in styled-components. You can define your keyframes
diff --git a/packages/example/app/page.tsx b/packages/example/app/page.tsx
index dac74616..2f258c2f 100644
--- a/packages/example/app/page.tsx
+++ b/packages/example/app/page.tsx
@@ -1,3 +1,5 @@
+/** @jsxImportSource next-yak */
+
import { YakThemeProvider, css, styled } from "next-yak";
import styles from "./page.module.css";
import { queries, colors } from "@/theme/constants.yak";
@@ -127,6 +129,13 @@ export default function Home() {
view code
+
+ CSS Prop works if this is green
+
diff --git a/packages/next-yak/loaders/__tests__/tsloader.test.ts b/packages/next-yak/loaders/__tests__/tsloader.test.ts
index 385870f4..782f1492 100644
--- a/packages/next-yak/loaders/__tests__/tsloader.test.ts
+++ b/packages/next-yak/loaders/__tests__/tsloader.test.ts
@@ -1329,3 +1329,489 @@ it("should minify css variables for production", async () => {
const variableNameMaxLength = Math.max(...variableNames.map((v) => v.length));
expect(variableNameMaxLength).toBeLessThanOrEqual("var(--hash___1)".length);
});
+
+describe("css prop", () => {
+ it("should work with a css property", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css } from "next-yak";
+ const elem =
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css } from \\"next-yak\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const elem =
;"
+ `);
+ });
+ it("should work with a css property that is not in the root jsx element", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const MyComp = () =>
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const MyComp = () =>
;"
+ `);
+ });
+
+ it("should work with css that is spreaded into another component", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const MyComp = (p) =>
anything
;
+ const MyComp2 = () =>
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const MyComp = p =>
anything
;
+ const MyComp2 = () =>
;"
+ `);
+ });
+
+ it("should work with an object identifier", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const TestObj = {
+ TestMem: (p) =>
anything
,
+ }
+ const MyComp2 = () =>
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const TestObj = {
+ TestMem: p =>
anything
+ };
+ const MyComp2 = () =>
;"
+ `);
+ });
+
+ it("should work with nested object identifier", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const test = {
+ nested: {
+ TestMem: (p) =>
anything
,
+ }
+ }
+ const MyComp2 = () =>
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const test = {
+ nested: {
+ TestMem: p =>
anything
+ }
+ };
+ const MyComp2 = () =>
;"
+ `);
+ });
+
+ it("should work with custom elements", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const MyComp2 = () =>
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const MyComp2 = () =>
;"
+ `);
+ });
+
+ it.skip("shouldn't convert it when css prop is a simple string", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const MyComp = () =>
anything
+ `,
+ ),
+ ).toMatchInlineSnapshot();
+ });
+
+ it("should work with when css property reuses css", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const padding = css\`
+ padding: 10px;
+ \`;
+ const Elem = () =>
+
;
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const padding =
+ /*YAK Extracted CSS:
+ .padding {
+ padding: 10px;
+ }*/
+ /*#__PURE__*/
+ css(__styleYak.padding);
+ const Elem = () =>
;"
+ `);
+ });
+
+ it("should work when css property is conditionally applied", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const MyComp = () =>
anything
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const MyComp = () =>
anything
;"
+ `);
+ });
+
+ it("should work when css property is conditionally not applied", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const MyComp = () =>
anything
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const MyComp = () =>
anything
;"
+ `);
+ });
+
+ it("should allow conditional css properties", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const padding = css\`
+ padding: 10px;
+ \`;
+ const Elem = () =>
+
;
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const padding =
+ /*YAK Extracted CSS:
+ .padding {
+ padding: 10px;
+ }*/
+ /*#__PURE__*/
+ css(__styleYak.padding);
+ const Elem = () =>
;"
+ `);
+ });
+ describe("merge properties", () => {
+ it("when className is set", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const Elem = () =>
;
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import { __yak_mergeCssProp } from \\"next-yak/runtime-internals\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const Elem = () =>
;"
+ `);
+ });
+ it("when style is set", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const Elem = () =>
;
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import { __yak_mergeCssProp } from \\"next-yak/runtime-internals\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const Elem = () =>
;"
+ `);
+ });
+ it("when spreaded property is set", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const Elem = () =>
;
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import { __yak_mergeCssProp } from \\"next-yak/runtime-internals\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const Elem = () =>
;"
+ `);
+ });
+ it("when class name and style is set", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const Elem = () =>
;
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import { __yak_mergeCssProp } from \\"next-yak/runtime-internals\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const Elem = () =>
;"
+ `);
+ });
+ it("when class name, style and spreaded property is set", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const Elem = () =>
;
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import { __yak_mergeCssProp } from \\"next-yak/runtime-internals\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const Elem = () =>
;"
+ `);
+ });
+ it("when props are spreaded", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const Elem = (props) =>
;
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import { __yak_mergeCssProp } from \\"next-yak/runtime-internals\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const Elem = props =>
;"
+ `);
+ });
+ it("throws an error when dynamic properties are used", async () => {
+ expect(
+ await tsloader.call(
+ loaderContext,
+ `
+ import { css, styled } from "next-yak";
+ const Elem = (props) =>
props.dynamicPadding ? '10px' : "0"}px;
+ \${() => props.active && css\`color: orange\`}
+ \`} {...props} />;
+ `,
+ ),
+ ).toMatchInlineSnapshot(`
+ "import { css, styled } from \\"next-yak\\";
+ import { __yak_mergeCssProp } from \\"next-yak/runtime-internals\\";
+ import __styleYak from \\"./page.yak.module.css!=!./page?./page.yak.module.css\\";
+ const Elem = props =>
props.active && /*#__PURE__*/css(__styleYak.Elem__propsActive), {
+ \\"style\\": {
+ \\"--Elem-padding_18fi82j\\": () => (props.dynamicPadding ? '10px' : \\"0\\") + \\"px\\"
+ }
+ })({}))} />;"
+ `);
+ });
+ });
+});
diff --git a/packages/next-yak/loaders/babel-yak-plugin.ts b/packages/next-yak/loaders/babel-yak-plugin.ts
index 41807349..51d99438 100644
--- a/packages/next-yak/loaders/babel-yak-plugin.ts
+++ b/packages/next-yak/loaders/babel-yak-plugin.ts
@@ -10,6 +10,7 @@ import getCssName from "./lib/getCssName.js";
import { Declaration, ParserState, parseCss } from "./lib/parseCss.js";
import { toCss } from "./lib/toCss.js";
import appendCssUnitToExpressionValue from "./lib/appendCssUnitToExpressionValue.js";
+import { transpileCssProp } from "./lib/transpileCssProp.js";
type YakBabelPluginOptions = {
replaces: Record
;
@@ -66,6 +67,7 @@ export default function (
YakTemplateLiteral
>;
yakTemplateExpressionsByName: Map;
+ runtimeInternalHelpers: Set;
}
> {
const { replaces } = options;
@@ -156,6 +158,7 @@ export default function (
this.topLevelConstBindings = new Map();
this.yakTemplateExpressionsByPath = new Map();
this.yakTemplateExpressionsByName = new Map();
+ this.runtimeInternalHelpers = new Set();
},
visitor: {
Program: {
@@ -167,7 +170,6 @@ export default function (
return;
}
const devMode = options.devMode || false;
- const runtimeInternalHelpers = new Set();
// Util to create a unique identifiers per file name
const existingNames = new Set();
const createUniqueName = (name: string, hash?: boolean) => {
@@ -197,7 +199,7 @@ export default function (
cssParserState,
visitChildren,
createUniqueName,
- runtimeInternalHelpers,
+ this.runtimeInternalHelpers,
getComponentTypes(this.yakTemplateExpressionsByPath),
this.topLevelConstBindings,
state.file,
@@ -206,9 +208,9 @@ export default function (
);
// Add used runtime helpers to the import
- if (runtimeInternalHelpers.size && this.yakImportPath) {
+ if (this.runtimeInternalHelpers.size && this.yakImportPath) {
const newImport = t.importDeclaration(
- [...runtimeInternalHelpers].map((helper) =>
+ [...this.runtimeInternalHelpers].map((helper) =>
t.importSpecifier(t.identifier(helper), t.identifier(helper)),
),
t.stringLiteral("next-yak/runtime-internals"),
@@ -217,6 +219,12 @@ export default function (
}
},
},
+ JSXElement(path, state) {
+ if (!this.isImportedInCurrentFile || !this.yakImportPath) {
+ return;
+ }
+ transpileCssProp(t, path, this.runtimeInternalHelpers, this.file);
+ },
/**
* Store the name of the imported 'css' and 'styled' variables e.g.:
* - `import { css, styled } from 'next-yak'` -> { css: 'css', styled: 'styled' }
diff --git a/packages/next-yak/loaders/lib/getStyledComponentName.ts b/packages/next-yak/loaders/lib/getStyledComponentName.ts
index 8a469f44..ab91b18e 100644
--- a/packages/next-yak/loaders/lib/getStyledComponentName.ts
+++ b/packages/next-yak/loaders/lib/getStyledComponentName.ts
@@ -9,20 +9,33 @@ import type { NodePath, types as babelTypes } from "@babel/core";
const getStyledComponentName = (
taggedTemplateExpressionPath: NodePath,
) => {
- const variableDeclaratorPath = taggedTemplateExpressionPath.findParent(
- (path) => path.isVariableDeclarator(),
- );
+ const variableOrFunctionDeclaratorPath =
+ taggedTemplateExpressionPath.findParent(
+ (path) => path.isVariableDeclarator() || path.isFunctionDeclaration(),
+ );
+
+ if (
+ variableOrFunctionDeclaratorPath?.isFunctionDeclaration() &&
+ "id" in variableOrFunctionDeclaratorPath.node &&
+ variableOrFunctionDeclaratorPath.node.id === null
+ ) {
+ const parent = variableOrFunctionDeclaratorPath.parentPath;
+ if (parent.isExportDefaultDeclaration()) {
+ return "defaultExp";
+ }
+ }
+
if (
- !variableDeclaratorPath ||
- !("id" in variableDeclaratorPath.node) ||
- variableDeclaratorPath.node.id?.type !== "Identifier"
+ !variableOrFunctionDeclaratorPath ||
+ !("id" in variableOrFunctionDeclaratorPath.node) ||
+ variableOrFunctionDeclaratorPath.node.id?.type !== "Identifier"
) {
throw new Error(
"Could not find variable declaration for styled component at " +
- taggedTemplateExpressionPath.node.loc,
+ JSON.stringify(taggedTemplateExpressionPath.node.loc),
);
}
- return variableDeclaratorPath.node.id.name;
+ return variableOrFunctionDeclaratorPath.node.id.name;
};
export default getStyledComponentName;
diff --git a/packages/next-yak/loaders/lib/transpileCssProp.ts b/packages/next-yak/loaders/lib/transpileCssProp.ts
new file mode 100644
index 00000000..2bb422ab
--- /dev/null
+++ b/packages/next-yak/loaders/lib/transpileCssProp.ts
@@ -0,0 +1,138 @@
+import type { BabelFile, NodePath, types as babelTypes } from "@babel/core";
+import {
+ objectExpression,
+ type JSXAttribute,
+ type JSXElement,
+} from "@babel/types";
+import { InvalidPositionError } from "../babel-yak-plugin.js";
+
+export const transpileCssProp = (
+ t: typeof babelTypes,
+ path: NodePath,
+ runtimeInternalHelpers: Set,
+ file: BabelFile,
+) => {
+ const openingElement = path.node.openingElement;
+ const cssPropIndex = openingElement.attributes.findIndex(
+ (prop) =>
+ t.isJSXAttribute(prop) &&
+ t.isJSXIdentifier(prop.name) &&
+ prop.name.name === "css",
+ );
+ // if the css prop is not present, we don't need to do anything
+ if (cssPropIndex === -1) {
+ return;
+ }
+
+ // can only be a JSXAttribute, as we checked above
+ const cssPropValue = (openingElement.attributes[cssPropIndex] as JSXAttribute)
+ .value;
+
+ // if the css prop is not an expression, we don't need to do anything
+ // e.g.
instead of
+ if (!t.isJSXExpressionContainer(cssPropValue)) {
+ if (cssPropValue) {
+ throw new InvalidPositionError(
+ `CSS prop must be an expression.`,
+ cssPropValue,
+ file,
+ "Use the css prop like this:
",
+ );
+ } else {
+ throw new Error(
+ `css prop must be an expression but found ${cssPropValue}. Please use the css prop like this:
`,
+ );
+ }
+ }
+
+ // namespaced JSX is not supported (even by React itself)
+ // e.g.
+ if (t.isJSXNamespacedName(openingElement.name)) {
+ throw new Error("Namespaced JSX not supported");
+ }
+
+ const cssExpression = cssPropValue.expression;
+
+ if (t.isJSXEmptyExpression(cssExpression)) {
+ return;
+ }
+
+ // relevant props are the ones that we need to merge with the css prop
+ // like className, style, and other spread props
+ const relevantProps = openingElement.attributes.filter(
+ (prop) =>
+ t.isJSXSpreadAttribute(prop) ||
+ (t.isJSXAttribute(prop) &&
+ t.isJSXIdentifier(prop.name) &&
+ (prop.name.name === "className" || prop.name.name === "style")),
+ );
+
+ // simple case where we don't have any other relevant props
+ // e.g.
+ if (relevantProps.length === 0) {
+ // remove the css prop
+ openingElement.attributes.splice(cssPropIndex, 1);
+
+ // adding a spread attribute with the css prop, in order to be able to use `className` and `style` props that are
+ // returned from the css prop
+ openingElement.attributes.push(
+ t.jsxSpreadAttribute(
+ t.callExpression(cssExpression, [t.objectExpression([])]),
+ ),
+ );
+ return;
+ }
+
+ // map the relevant props to an object
+ // e.g.
gets converted to
+ // { className: "x", style: {y:true}, ...p }
+ const mapped = relevantProps
+ .map((prop) => {
+ if (t.isJSXAttribute(prop)) {
+ if (t.isJSXNamespacedName(prop.name)) {
+ throw new Error("Namespaced JSX not supported");
+ }
+ if (!prop.value) {
+ return null;
+ }
+ if (t.isJSXExpressionContainer(prop.value)) {
+ if (t.isJSXEmptyExpression(prop.value.expression)) {
+ return null;
+ }
+ return t.objectProperty(
+ t.identifier(prop.name.name),
+ prop.value.expression,
+ );
+ }
+ return t.objectProperty(t.identifier(prop.name.name), prop.value);
+ }
+ return t.spreadElement(prop.argument);
+ })
+ .filter(Boolean) as Array<
+ babelTypes.ObjectProperty | babelTypes.SpreadElement
+ >;
+
+ // remove all properties that are in the relevant props
+ openingElement.attributes = openingElement.attributes.filter(
+ (prop, index) => !relevantProps.includes(prop) && index !== cssPropIndex,
+ );
+
+ // add the spread attribute
+ openingElement.attributes.push(
+ t.jsxSpreadAttribute(
+ t.callExpression(t.identifier("__yak_mergeCssProp"), [
+ // shortcircuit if there is only one spread element to avoid creating an unnecessary object
+ // e.g.
gets converted to
+ // __yak_mergeCssProp(p, css`...`) instead of __yak_mergeCssProp({ ...p }, css`...`)
+ mapped.length === 1 && t.isSpreadElement(mapped[0])
+ ? mapped[0].argument
+ : t.objectExpression(mapped),
+ t.callExpression(cssExpression, [t.objectExpression([])]),
+ ]),
+ ),
+ );
+
+ // add import to the custom merge function to the top of the file
+ // import { __yak_mergeCssProp } from "next-yak/runtime-internals";
+ runtimeInternalHelpers.add("__yak_mergeCssProp");
+};
diff --git a/packages/next-yak/package.json b/packages/next-yak/package.json
index 4a7ec0b8..5b638736 100644
--- a/packages/next-yak/package.json
+++ b/packages/next-yak/package.json
@@ -41,6 +41,14 @@
"./cssloader": {
"require": "./dist/loaders/cssloader.cjs",
"import": "./dist/loaders/cssloader.js"
+ },
+ "./jsx-runtime": {
+ "require": "./dist/jsx-runtime.cjs",
+ "import": "./dist/jsx-runtime.js"
+ },
+ "./jsx-dev-runtime": {
+ "require": "./dist/jsx-dev-runtime.cjs",
+ "import": "./dist/jsx-dev-runtime.js"
}
},
"scripts": {
@@ -82,4 +90,4 @@
"runtime",
"withYak"
]
-}
\ No newline at end of file
+}
diff --git a/packages/next-yak/runtime/__tests__/cssProp.test.tsx b/packages/next-yak/runtime/__tests__/cssProp.test.tsx
new file mode 100644
index 00000000..ec1839ea
--- /dev/null
+++ b/packages/next-yak/runtime/__tests__/cssProp.test.tsx
@@ -0,0 +1,52 @@
+import { it, expect } from "vitest";
+import { mergeCssProp } from "../internals/mergeCssProp";
+
+it("merge properties when className is set", async () => {
+ expect(
+ mergeCssProp(
+ { className: "foo" },
+ { className: "cssProp", style: { "--any-var": "any" } },
+ ),
+ ).toMatchObject({ className: "foo cssProp", style: { "--any-var": "any" } });
+});
+it("merge properties when style is set", async () => {
+ expect(
+ mergeCssProp(
+ { style: { padding: "5px" } },
+ { className: "cssProp", style: { "--any-var": "any" } },
+ ),
+ ).toMatchObject({
+ className: "cssProp",
+ style: { padding: "5px", "--any-var": "any" },
+ });
+});
+it("merge properties when spreaded property is set", async () => {
+ expect(
+ mergeCssProp(
+ { className: "foo" },
+ { className: "cssProp", style: { "--any-var": "any" } },
+ ),
+ ).toMatchObject({ className: "foo cssProp", style: { "--any-var": "any" } });
+});
+it("merge properties when class name and style is set", async () => {
+ expect(
+ mergeCssProp(
+ { className: "foo", style: { padding: "5px" } },
+ { className: "cssProp", style: { "--any-var": "any" } },
+ ),
+ ).toMatchObject({
+ className: "foo cssProp",
+ style: { padding: "5px", "--any-var": "any" },
+ });
+});
+it("merge properties when class name, style and spreaded property is set", async () => {
+ expect(
+ mergeCssProp(
+ { className: "foo", style: { padding: "5px" } },
+ { className: "cssProp", style: { "--any-var": "any" } },
+ ),
+ ).toMatchObject({
+ className: "foo cssProp",
+ style: { padding: "5px", "--any-var": "any" },
+ });
+});
diff --git a/packages/next-yak/runtime/__tests__/cssPropTest.tsx b/packages/next-yak/runtime/__tests__/cssPropTest.tsx
new file mode 100644
index 00000000..d9db6434
--- /dev/null
+++ b/packages/next-yak/runtime/__tests__/cssPropTest.tsx
@@ -0,0 +1,112 @@
+/** @jsxImportSource next-yak */
+// this is only a type check file and should not be executed
+
+import { css } from "next-yak";
+import { CSSProperties } from "react";
+
+declare module "next-yak" {
+ export interface YakTheme {
+ primaryColor: "red" | "blue";
+ }
+}
+
+const ComponentWithCssProp = () => {
+ return
;
+};
+
+const NestedComponentWithCssProp = () => (
+
+);
+
+const ComponentThatTakesCssProp = (p: {
+ css: { className: string; style?: CSSProperties };
+}) => anything
;
+
+const ComponentWithCssPropAsProp = () => {
+ return ;
+};
+
+const ObjectWithComponent = {
+ ComponentThatTakesCssProp,
+ nested: {
+ ComponentThatTakesCssProp,
+ },
+};
+const ComponentThatUsesObjectWithComponent = () => (
+
+);
+
+const ComponentThatUsesNestedObjectWithComponent = () => (
+
+);
+
+const ComponentWithCSSPropString = () => (
+
+ anything
+
+);
+
+const padding = css`
+ padding: 10px;
+`;
+const ComponentThatReusesCSS = () =>
;
+
+const ComponentWithConditionalCSSProp = () => (
+
+ anything
+
+);
+
+const ComponentWithConditionalCSSProp2 = () => (
+
+);
+
+const ComponentWithInterpolatedCSS = () => {
+ const x = Math.random() > 0.5;
+ return (
+ css`
+ padding: 20px;
+ `}
+ />
+ );
+};
diff --git a/packages/next-yak/runtime/__tests__/tsconfig.json b/packages/next-yak/runtime/__tests__/tsconfig.json
index e433797f..fd8728fe 100644
--- a/packages/next-yak/runtime/__tests__/tsconfig.json
+++ b/packages/next-yak/runtime/__tests__/tsconfig.json
@@ -18,5 +18,6 @@
},
"include": [
"./typeTest.tsx",
+ "./cssPropTest.tsx"
],
}
\ No newline at end of file
diff --git a/packages/next-yak/runtime/cssLiteral.tsx b/packages/next-yak/runtime/cssLiteral.tsx
index 315d6ec0..ca41f3a2 100644
--- a/packages/next-yak/runtime/cssLiteral.tsx
+++ b/packages/next-yak/runtime/cssLiteral.tsx
@@ -1,3 +1,4 @@
+import { CSSProperties } from "react";
import type { YakTheme } from "./index.d.ts";
type ComponentStyles
= (props: TProps) => {
@@ -7,6 +8,11 @@ type ComponentStyles = (props: TProps) => {
};
};
+export type StaticCSSProp = {
+ className: string;
+ style?: CSSProperties;
+};
+
export type CSSInterpolation =
| string
| number
@@ -14,6 +20,7 @@ export type CSSInterpolation =
| null
| false
| ComponentStyles
+ | StaticCSSProp
| {
// type only identifier to allow targeting components
// e.g. styled.svg`${Button}:hover & { fill: red; }`
@@ -49,13 +56,16 @@ type PropsToClassNameFn = (props: unknown) =>
* Therefore this is only an internal function only and it must be cast to any
* before exported to the user.
*/
-const internalCssFactory = (
- ...args: Array>
-) => {
+export function css(styles: TemplateStringsArray, ...values: []): StaticCSSProp;
+export function css(
+ styles: TemplateStringsArray,
+ ...values: CSSInterpolation[]
+): ComponentStyles;
+export function css(...args: Array): StaticCSSProp | ComponentStyles {
const classNames: string[] = [];
const dynamicCssFunctions: PropsToClassNameFn[] = [];
const style: Record = {};
- for (const arg of args) {
+ for (const arg of args as Array>) {
// A CSS-module class name which got auto generated during build from static css
// e.g. css`color: red;`
// compiled -> css("yak31e4")
@@ -110,7 +120,7 @@ const internalCssFactory = (
style: allStyles,
};
};
-};
+}
// Dynamic CSS with runtime logic
const unwrapProps = (
@@ -161,5 +171,3 @@ const recursivePropExecution = (
}
return result;
};
-
-export const css = internalCssFactory as any as CSSFunction;
diff --git a/packages/next-yak/runtime/internals/index.ts b/packages/next-yak/runtime/internals/index.ts
index c72010a3..8286b4ed 100644
--- a/packages/next-yak/runtime/internals/index.ts
+++ b/packages/next-yak/runtime/internals/index.ts
@@ -1 +1,2 @@
export { unitPostFix as __yak_unitPostFix } from "./unitPostFix.js";
+export { mergeCssProp as __yak_mergeCssProp } from "./mergeCssProp.js";
diff --git a/packages/next-yak/runtime/internals/mergeCssProp.ts b/packages/next-yak/runtime/internals/mergeCssProp.ts
new file mode 100644
index 00000000..af4eed9e
--- /dev/null
+++ b/packages/next-yak/runtime/internals/mergeCssProp.ts
@@ -0,0 +1,27 @@
+/**
+ * This is an internal helper function to merge relevant props of a native element with a css prop.
+ * It's automatically added when using the `css` prop in a JSX element.
+ * e.g.:
+ * ```tsx
+ *
+ */
+export const mergeCssProp = (
+ relevantProps: Record,
+ cssProp: {
+ className: string;
+ style?: Record;
+ },
+) => {
+ return {
+ className: relevantProps.className
+ ? relevantProps.className + " " + cssProp.className
+ : cssProp.className,
+ style: { ...(relevantProps.style ?? {}), ...cssProp.style },
+ };
+};
diff --git a/packages/next-yak/runtime/jsx-dev-runtime.ts b/packages/next-yak/runtime/jsx-dev-runtime.ts
new file mode 100644
index 00000000..dcc4dd1c
--- /dev/null
+++ b/packages/next-yak/runtime/jsx-dev-runtime.ts
@@ -0,0 +1,8 @@
+import ReactJSXRuntimeDev from "react/jsx-dev-runtime";
+
+// @ts-expect-error as the types are not exported
+const Fragment = ReactJSXRuntimeDev.Fragment;
+// @ts-expect-error as the types are not exported
+const jsxDEV = ReactJSXRuntimeDev.jsxDEV;
+
+export { Fragment, jsxDEV };
diff --git a/packages/next-yak/runtime/jsx-runtime.ts b/packages/next-yak/runtime/jsx-runtime.ts
new file mode 100644
index 00000000..a7bda223
--- /dev/null
+++ b/packages/next-yak/runtime/jsx-runtime.ts
@@ -0,0 +1,29 @@
+import ReactJSXRuntime from "react/jsx-runtime";
+import type { StaticCSSProp } from "./cssLiteral.js";
+
+// @ts-expect-error as the types are not exported
+const Fragment = ReactJSXRuntime.Fragment;
+// @ts-expect-error as the types are not exported
+const jsx = ReactJSXRuntime.jsx;
+// @ts-expect-error as the types are not exported
+const jsxs = ReactJSXRuntime.jsxs;
+
+export declare namespace YakJSX {
+ export type Element = React.JSX.Element;
+ export type ElementType = React.JSX.ElementType;
+ export type ElementClass = React.JSX.ElementClass;
+ export type ElementAttributesProperty = React.JSX.ElementAttributesProperty;
+ export type ElementChildrenAttribute = React.JSX.ElementChildrenAttribute;
+ export type LibraryManagedAttributes =
+ React.JSX.LibraryManagedAttributes;
+ export type IntrinsicAttributes = React.JSX.IntrinsicAttributes;
+ export type IntrinsicClassAttributes =
+ React.JSX.IntrinsicClassAttributes;
+ export type IntrinsicElements = {
+ [K in keyof JSX.IntrinsicElements]: React.JSX.IntrinsicElements[K] & {
+ css?: StaticCSSProp;
+ };
+ };
+}
+
+export { type YakJSX as JSX, Fragment, jsx, jsxs };
diff --git a/packages/next-yak/tsup.config.ts b/packages/next-yak/tsup.config.ts
index 00f6838a..0b2182fc 100644
--- a/packages/next-yak/tsup.config.ts
+++ b/packages/next-yak/tsup.config.ts
@@ -105,4 +105,27 @@ export default defineConfig([
target: "es2022",
outDir: "dist/loaders",
},
+ // jsx-runtime
+ {
+ entryPoints: ["runtime/jsx-runtime.ts"],
+ format: ["cjs", "esm"],
+ minify: true,
+ sourcemap: true,
+ clean: true,
+ dts: true,
+ external: ["react"],
+ target: "es2022",
+ outDir: "dist",
+ },
+ // jsx-runtime-dev
+ {
+ entryPoints: ["runtime/jsx-dev-runtime.ts"],
+ format: ["cjs", "esm"],
+ minify: true,
+ sourcemap: true,
+ clean: true,
+ external: ["react"],
+ target: "es2022",
+ outDir: "dist",
+ },
]);