Vanilla Extract with responsive variants

Extending @vanilla-extract/recipes with tailwind-variant inspired `responsiveVariants`.

2025-05-173 min read

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;
  }
}

Check out the example.

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. 😃

Source code

More examples

Thanks