Advanced Usage

Advanced Usage

Nested Component Structure

Recast is great for applying component styles to a single html element within your components and offers a very comparable experience to other utilities lic cva (opens in a new tab), tv (opens in a new tab) and stitches (opens in a new tab). However, when it comes to managing more complex React component structures, Recast really shines.

Take for example this slider component taken from shad/cn (opens in a new tab) and based on the Radix UI (opens in a new tab) library. You will see that the slider component is composed of multiple elements which could each have styles applied independently. Recast provides a mechanism to achieve this with minimal footprint on your component primitives.

components/primitives/slider.tsx
import { cn } from "../../utils/cn.js";
import * as RadixSliderPrimitive from "@radix-ui/react-slider";
import { RecastWithClassNameProps } from "@rpxl/recast"; /** Import the Recast type utility */
import React, { forwardRef } from "react";
 
type Props = React.ComponentPropsWithoutRef<typeof RadixSliderPrimitive.Root> &
  /** Define your component className props here*/
  RecastWithClassNameProps<{
    root: string;
    track: string;
    range: string;
    thumb: string;
  }>;
 
const Slider = forwardRef<
  React.ElementRef<typeof RadixSliderPrimitive.Root>,
  Props
  /**
   * Your component can now receive styles on any
   * Recast className "anchor" via the `rcx` component prop. */
>(({ className, rcx, ...props }, ref) => {
  return (
    <RadixSliderPrimitive.Root
      ref={ref}
      className={cn(rcx?.root, className)}
      {...props}
    >
      <RadixSliderPrimitive.Track className={rcx?.track}>
        <RadixSliderPrimitive.Range className={rcx?.range} />
      </RadixSliderPrimitive.Track>
      <RadixSliderPrimitive.Thumb className={rcx?.thumb} />
    </RadixSliderPrimitive.Root>
  );
});
 
/* ... */

Now when you wrap your component with Recast, you can apply styles to the nested elements. All Recast theme props are available to the nested elements as well.

components/ui/slider.tsx
export const Slider = recast(SliderPrimitive, {
  base: {
    root: "relative flex w-full touch-none select-none items-center",
    track: "relative h-1.5 w-full grow overflow-hidden rounded-full bg-black",
    range: "bg-primary absolute h-full",
    thumb: "block h-4 w-4 rounded-full border border-black/50 bg-white shadow",
  },
  /* ... Other recast theme props */
});

Note: Even though this component uses nested className props it will still work used outside of Recast as a standalone component.

Wrap a Recast Component

It is foreseeable that you may want to "wrap" a Recast component for certain cases. For instance, in the example below, we are wrapping the ScrollAreaPrimitive component to eliminate the variant prop from the consumer. Instead, we conditionally apply the variant styles based on the component's built-in orientation prop (horizontal | vertical). In this scenario, we aim to spare the consumer from having to add both the orientation and the variant prop, as the latter can be inferred simply by the orientation prop.

components/ui/scroll-area.tsx
"use client";
import { recast } from "@rpxl/recast";
import { ScrollAreaPrimitive } from "@/components/primitives/scroll-area.tsx";
import { forwardRef } from "react";
 
const BaseScrollArea = recast(ScrollAreaPrimitive, {
  // This matches the default for the primitives built in `orientation` prop
  defaults: { variants: { variant: "vertical" } },
  base: {
    root: "relative overflow-hidden",
    viewport: "h-full w-full rounded-[inherit]",
    thumb: "bg-border relative flex-1 rounded-full",
    scrollbar: "flex touch-none select-none transition-colors",
  },
  variants: {
    variant: {
      vertical: {
        scrollbar: "h-full w-2.5 border-l border-l-transparent p-[1px]",
      },
      horizontal: {
        scrollbar: "h-2.5 flex-col border-t border-t-transparent p-[1px]",
      },
    },
  },
});
 
// Remove the variant prop from the consumer as it will be conditionally
// applied based on the components built in `orientation` prop
type Props = Omit<
  React.ComponentPropsWithoutRef<typeof BaseScrollArea>,
  "variant"
>;
 
export const ScrollArea = forwardRef<
  React.ElementRef<typeof BaseScrollArea>,
  Props
>(({ ...props }, ref) => {
  // Here we use the components built in `orientation`
  // prop to configure the variant styles
  return <BaseScrollArea ref={ref} variant={props.orientation} {...props} />;
});
 
ScrollArea.displayName = "ScrollArea";