Vanilla Extract offers a powerful styling solution, essentially it's “CSS Modules in TypeScript” with scoped CSS variables and a lot more functionality. However, it does not have built in support for responsive variants, a feature available in Stitches. After migrating my UI library from Stitches to Vanilla Extract, I wanted to retain this functionality. That became the main motivation behind creating Homemade Recipes.
Homemade Recipes
A package which is an extension of @vanilla-extract/recipes
that adds responsive variants. It is built for projects that need responsive styling with recipes while keeping Vanilla Extract's type safety approach.
How it works
Setup
createHomemadeRecipe
accepts key/value pairs where the keys become your responsive modifiers, and the values are the min-width where that breakpoint should start.
// homemade-recipe.css.ts
import { createHomemadeRecipe } from "homemade-recipes";
export const homemadeRecipe = createHomemadeRecipe({
/** Phones (landscape) */
xs: "520px",
/** Tablets (portrait) */
sm: "768px",
/** Tablets (landscape) */
md: "1024px",
/** Laptops */
lg: "1280px",
/** Desktops */
xl: "1640px",
});
homemadeRecipe
extends recipe that accepts an optional responsiveVariants
.
// button-recipe.css.ts
import { HomemadeRecipeVariants } from "homemade-recipes";
import { homemadeRecipe } from "./homemade-recipe.css";
export const buttonRecipe = homemadeRecipe({
base: {
borderRadius: 6,
},
variants: {
fullWidth: {
true: {
width: "100%",
},
false: {
width: "unset",
},
},
variant: {
bright: "bright",
},
},
responsiveVariants: ["sm"],
});
export type ButtonVariants = NonNullable<
HomemadeRecipeVariants<typeof buttonRecipe>
>;
responsiveVariants: ["sm"]
will generate an additional set of classes at build time.
.button-recipe__4lwr860 {
border-radius: 6px;
}
.button-recipe_fullWidth_true__4lwr861 {
width: 100%;
}
.button-recipe_fullWidth_false__4lwr862 {
width: unset;
}
+ @media screen and (min-width: 768px) {
+ .button-recipe_fullWidth_sm_true__4lwr863 {
+ width: 100%;
+ }
+ .button-recipe_fullWidth_sm_false__4lwr864 {
+ width: unset;
+ }
+ }
appendCss will generate (at runtime).
<style
data-package="homemade-recipes"
data-identifier="homemade-recipe__1qtsqlc0"
type="text/css"
>
@media screen and (min-width: 768px) {
.bright_sm {
color: orange;
font-family: Arial;
}
}
</style>
By passing in an existing class (i.e. bright),
bright_sm
can now be created.
With this homemade recipe, you can now use it for your button component.
// button.tsx
import { ButtonVariants, buttonRecipe } from "./button-recipe.css";
type ButtonProps = ButtonVariants;
export const Button = ({ fullWidth, variant }: ButtonProps) => {
return (
<button className={buttonRecipe({ fullWidth, variant })}>
Hello world
</button>
);
};
Usage
// App.tsx
import "./App.css";
import { Button } from "./button";
function App() {
return (
<Button
// fullWidth?: boolean | { initial?: boolean; sm?: boolean; }
fullWidth={{ initial: true, sm: false }}
// variant?: "bright" | { initial?: "bright"; sm?: "bright"; }
variant={{ sm: "bright" }}
/>
);
}
export default App;
The following CSS classes will be applied to your Button
component.
button-recipe__1d33wle0 button-recipe_fullWidth_true__1d33wle1 button-recipe_fullWidth_sm_false__1d33wle5 bright_sm
The classes
.button-recipe__4lwr860 {
border-radius: 6px;
}
.button-recipe_fullWidth_true__4lwr861 {
width: 100%;
}
@media screen and (min-width: 768px) {
.button-recipe_fullWidth_sm_false__4lwr864 {
width: unset;
}
}
@media screen and (min-width: 768px) {
.bright_sm {
color: orange;
font-family: Arial;
}
}
Final thoughts
Feel free to fork, use, or contribute by raising discussions or issues. Since I use this package in my personal projects, I'm committed to continually improving it. 😃
More examples
Thanks
- Vanilla Extract - For creating an awesome library
- Tailwind Variants - For the inspiration behind
responsiveVariants
functionality (discontinued) - Rainbow Sprinkles & Dessert Box - For inspiration on extending Vanilla Extract