Problem
Recently, I began working on a product that required creating a handful of components. I wanted my UI library to have zero runtime and to be compatible with any front-end stack. I came up with the idea of incorporating a base (unstyled) version of Daisy UI and styling it to my own preferences.
However, the challenge was that Tailwind expects me to style my components using CSS-in-JS, whereas I strongly prefer writing plain CSS.
So, here is how I solved it.
Solution
Let's take one of the components and see how I created it. Here is the source of the Card component.
.card-hero {
@apply bg-paper rounded-[2.5rem] sm:rounded-[4rem] p-[2.5rem] sm:p-[4rem]
max-w-md mx-auto grid gap-4;
figure {
@apply mt-[-4rem] sm:mt-[-5.75rem];
img {
@apply rounded-xl sm:rounded-3xl w-full flex items-center justify-center
text-xs text-opacity-40 border border-paper border-opacity-20 bg-neutral
dark:bg-opacity-80 dark:brightness-[.64];
}
}
.card-body {
@apply text-center;
}
.card-title {
@apply text-2xl font-light;
}
.card-subtitle {
@apply text-sm font-light;
}
}
Now, I need to parse it as a JavaScript object and make Tailwind happy:
import plugin from "tailwindcss/plugin";
import { loadStyles } from "../_tw/loadStyles.ts";
export const cardPlugin = plugin(async ({ addComponents }) => {
const styles = await loadStyles("/ui/Card/Card.components.css");
addComponents(styles);
});
What’s the magic of loadStyles
?
import { parse } from "postcss";
import { objectify } from "postcss-js";
import type { Root } from "postcss-js";
import { getAbsPath, readCss } from "./fs.ts";
/**
* Represents a CSS-in-JS object where each selector maps to its CSS properties.
*/
type type CSSInJS = {
[selector: string]: null | string | string[] | CSSInJS;
};
/**
* Transforms a CSS file with @apply directives into a CSS-in-JS object.
*
* @param {string} cssFilePath - The pathname to the CSS file.
* @returns {Promise<CSSInJS>} - The resulting CSS-in-JS object.
*/
export const loadStyles = async (
cssFilePath: string,
): Promise<CSSInJS> => {
const absPath = getAbsPath(cssFilePath);
const cssContent = await readCss(absPath);
const processed = parse(cssContent) as Root;
const jssObject: CSSInJS = objectify(processed);
return jssObject;
};