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. This guide will detail the creation of a theme layer for a component primitive and explain the individual properties available to fulfill any design requirements.
While the examples below use Tailwind CSS (opens in a new tab), Recast works with any CSS solution, including CSS modules, CSS-in-JS libraries, or plain CSS classes.
Creating a Themed Component
To create a themed component, import your component primitive and use the
recast
function:
import { recast } from "@rpxl/recast";
import ButtonPrimitive from "@/components/primitives/button";
export const Button = recast(ButtonPrimitive, {
// Theme object
});
The recast
function takes two arguments:
- The component primitive
- A theme object that defines the styling
Let's explore the structure of the theme object:
Theme Object Structure
Base Styles
Base styles are always applied to the component:
export const Button = recast(ButtonPrimitive, {
base: "inline-flex items-center justify-center rounded-md font-medium transition-colors",
// ...
});
Variants
Variants define different variations of your component:
export const Button = recast(ButtonPrimitive, {
// ...
variants: {
variant: {
primary: "bg-blue-500 text-white hover:bg-blue-600",
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
outline: "border border-gray-300 text-gray-700 hover:bg-gray-100",
},
size: {
sm: "px-3 py-2 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
},
},
// ...
});
Modifiers
Modifiers are boolean properties that can be combined with variants:
export const Button = recast(ButtonPrimitive, {
// ...
modifiers: {
fullWidth: "w-full",
rounded: "rounded-full",
},
// ...
});
Conditionals
Conditionals allow you to apply styles based on combinations of variants and modifiers:
export const Button = recast(ButtonPrimitive, {
// ...
conditionals: [
{
variants: { size: "lg", variant: "primary" },
modifiers: ["fullWidth"],
className: "font-bold uppercase",
},
],
// ...
});
Defaults
Set default values for variants and modifiers:
export const Button = recast(ButtonPrimitive, {
// ...
defaults: {
variants: { variant: "primary", size: "md" },
modifiers: ["rounded"],
},
});
Best Practices for Theming with Recast
- Keep your component primitives simple and focused on functionality.
- Use descriptive names for variants and modifiers.
- Leverage conditionals for complex style combinations.
- Set sensible defaults to reduce prop clutter in usage.
- Use TypeScript for better type checking and developer experience.
By following these guidelines and utilizing the full power of Recast's theming capabilities, you can create flexible, reusable components that can easily adapt to different design requirements across projects.
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.
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.
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.
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.
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.
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.
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.