Theming

Theming with Recast

The theme layer is where the visual presentation of the component is defined. It ensures that our styles are entirely decoupled from the component primitive, maximizing reuse. The following guide will detail the creation of a theme layer for a component primitive and elucidate the individual properties at our disposal to fulfill any design requirements.

All examples below use Tailwind CSS (opens in a new tab). If you are using Tailwind CSS, check out our section on setting up Tailwind CSS with Recast.

Create a Themed Component

Once you have a component primitive, you can create a themed component by importing the primitive and calling the recast method and passing any React component. This is all that is required to start using a Recast component in your project.

components/ui/button.ts
import { recast } from "rpxl/recast";
import ButtonPrimitive from "@/components/primitives/button";
 
export const Button = recast(ButtonPrimitive, {});

Of course this is a very basic example and will only render a button with no styling. Let's build from this foundation and step through the process of creating a beautifully themed button.

Add Base Styles

The base property is where you define the base styles for your component. Base styles will always be applied to your component. Depending on your component primitives theme API you will have access to one or more handles to apply your base styles to (see Advanced Usage). For example, our button component primitive only has one element, so therefore all our styles will be defined directly to the base key.

components/ui/button.ts
import { recast } from "rpxl/recast";
import ButtonPrimitive from "@/components/primitives/button";
 
export const Button = recast(ButtonPrimitive, {
  base: [
    "inline-flex",
    "items-center",
    "justify-center",
    "whitespace-nowrap",
    "rounded-md",
    "text-sm",
    "font-medium",
    "ring-offset-background",
    "transition-colors",
    "focus-visible:outline-none",
    "focus-visible:ring-2",
    "focus-visible:ring-ring",
    "focus-visible:ring-offset-2",
    "disabled:pointer-events-none",
    "disabled:opacity-50",
  ],
  /* ... */
});
ℹ️

Note that the classes can be defined as a string or an array of strings. This is completely optional but intended to improve readability and maintainability of long class lists.

Add Variants

The best way to describe variants is to think of them as distinct variations of a component. Some common examples for a button component might be: "size" and "intent". The "size" would define the size of the button e.g. small, medium, large, and the "intent" would define the intention of the button e.g. success, danger etc. With Recast it is possible to define any number of variants which will start to form your button theme API.

components/ui/button.ts
import { recast } from "rpxl/recast";
import ButtonPrimitive from "@/components/primitives/button";
 
export const Button = recast(ButtonPrimitive, {
  /* ... */
  variants: {
    size: {
      sm: "px-3 py-2 text-sm",
      md: "text-md px-5 py-2.5",
      lg: "px-8 py-3.5 text-lg",
    },
    primary:
      "bg-gradient-to-br from-purple-600 to-blue-500 text-white hover:bg-gradient-to-bl",
    secondary:
      "bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:bg-gradient-to-l",
    outline: "border border-gray-300 bg-white text-gray-900 ",
  },
  /* ... */
});
ℹ️

It's important to note that any variants defined in your theme API will benefit from autocompletion and type checking, leveraging the power of TypeScript, assuming your editor supports these features.

Add Modifiers

Modifiers are a powerful feature of Recast that work like boolean properties. Modifiers are variations of a component that can be "mixed-in" and combined with other modifiers and all variants. Some common examples for a button might include "floating" for an elevated button presentation and "block" for a full-width button.

components/ui/button.ts
import { recast } from "rpxl/recast";
import ButtonPrimitive from "@/components/primitives/button";
 
export const Button = recast(ButtonPrimitive, {
  /* ... */
  modifiers: {
    block: "w-full",
    floating: "shadow-lg",
    pill: "rounded-full px-8",
  },
  /* ... */
});
ℹ️

As a general rule of thumb, a component variation should only be defined as a modifier if it can be combined with other variants and modifiers. For instance, representing button sizes as modifiers would not be advisable, as combining "sm," "md," and "lg" will not yield a deterministic result.

Add Conditional Styles

Conditonals are a way to define conditional styles that will only be applied if certain rules are met. For example, you may want to apply a certain style only if the button size === "lg" and the floating modifier is enabled.

components/ui/button.ts
import { recast } from "rpxl/recast";
import ButtonPrimitive from "@/components/primitives/button";
 
export const Button = recast(ButtonPrimitive, {
  /* ... */
  conditionals: [
    {
      variants: { size: "lg" },
      modifiers: ["floating"],
      className: "border-4 border-blue-500 text-white",
    },
  ],
  /* ... */
});
ℹ️

The conditionals property will take an array of conditions to validate independently. This means that you can define multiple conditions that will be applied in array order.

To take this one step further you can match combinations of variant values and modifiers to meet a condition.

components/ui/button.ts
import { recast } from "rpxl/recast";
import ButtonPrimitive from "@/components/primitives/button";
 
export const Button = recast(ButtonPrimitive, {
  /* ... */
  conditionals: [
    {
      /**
       * The following `variants` condition requires that the `size` is set to `lg`
       * and the `variant` is set to `primary` or `secondary`.
       */
      variants: { size: "lg", variant: ["primary", "secondary"] },
 
      /**
       * The following `modifiers` condition requires
       * that the `floating` and `block` modifiers are enabled.
       */
      modifiers: ["floating", "block"],
      className: "border-4 border-blue-500 text-white",
    },
  ],
  /* ... */
});

Set Some Defaults

Finally, we can set some default values for our component. This is useful to reduce the amount of props that need to be passed to the component for common component configuration. For example, we can set the variant to primary and the size to md by default.

components/ui/button.ts
import { recast } from "rpxl/recast";
import ButtonPrimitive from "@/components/primitives/button";
 
export const Button = recast(ButtonPrimitive, {
  /* ... */
  defaults: {
    variants: { variant: "primary", size: "md" },
  },
  /* ... */
});
🎉

Congratulations! You should now be armed with the essentials to go off and start building your own component library using Recast.