Overview

To better understard how BaseLayer works, lets take a look at some examples.


Base

Slots

Let’s dive with an example. Take a look at this ComboBox component.

The Base Layer for the component looks like this.

import type {
  ComboBoxProps as AriaComboBoxProps,
  ItemProps,
} from "react-aria-components";

import {
  ComboBox as AriaComboBox,
  Button,
  Input,
  Item,
  Label,
  ListBox,
  Popover,
  Text,
} from "react-aria-components";

import { ChevronDown } from "lucide-react";
import { tv } from "tailwind-variants";

const combobox = tv({
  slots: {
    input: "m-0 w-64 rounded-md border bg-surface p-2 align-middle text-fg",
    root: "max-h-inherit overflow-auto p-1 outline-none",
    item: "relative m-1 flex cursor-default flex-col rounded-md p-2 outline-none hover:bg-surface-2 aria-selected:bg-secondary  aria-selected:text-secondary-fg",
    popover:
      "w-64 rounded-xl border bg-surface p-2 text-fg shadow-xl outline-none",
    button:
      "absolute right-2 flex appearance-none items-center justify-center rounded-md border-0 outline-none hover:bg-surface-2",
  },
});

const { input, button, item, popover, root } = combobox();

interface ComboBoxProps<T extends object>
  extends Omit<AriaComboBoxProps<T>, "children"> {
  className?: string;
  label?: string;
  description?: string | null;
  errorMessage?: string | null;
  children: React.ReactNode | ((item: T) => React.ReactNode);
}

const ComboBox = <T extends object>({
  label,
  className,
  description,
  errorMessage,
  children,
  ...props
}: ComboBoxProps<T>) => (
  <AriaComboBox className={root({ className })} {...props}>
    <Label className="text-fg">{label}</Label>
    <div className="relative flex w-fit items-center rounded-2xl bg-surface">
      <Input className={input()} />
      <Button className={button()}>
        <ChevronDown className="text-fg"/>
      </Button>
    </div>
    {description && <Text slot="description">{description}</Text>}
    {errorMessage && <Text slot="errorMessage">{errorMessage}</Text>}
    <Popover className={popover()}>
      <ListBox>{children}</ListBox>
    </Popover>
  </AriaComboBox>
);

ComboBox.displayName = "ComboBox";

const ComboBoxItem = (props: ItemProps) => (
  <Item {...props} className={item()} />
);

ComboBoxItem.displayName = "ComboBoxItem";

export { ComboBox, ComboBoxItem };

and the using the component itself like this.

import { ComboBox, ComboBoxItem } from "@/components/base/combobox";

export const ComboBoxExample = () => {
  return (
    <ComboBox label="Project">
      <ComboBoxItem>Health Dashboard</ComboBoxItem>
      <ComboBoxItem>To-Do App</ComboBoxItem>
      <ComboBoxItem>UI Kit</ComboBoxItem>
      <ComboBoxItem>Portfolio Site</ComboBoxItem>
    </ComboBox>
  );
};

As you can see, BaseLayer leverages the Tailwind Variants library to create reusable styles. The TV library provides usefull slots, which are perfect for handling the styles of complex multi part Ark Components.

Variants

Tailwind Variants (as the name implies) also provies support for creating style variants as well as compound and even responsive variants.

Let’s take a look at how variants work in BaseLayer.

import { tv, type VariantProps } from "tailwind-variants";

export const button = tv({
  base: "inline-flex items-center justify-center rounded-md font-medium ring-offset-background transition-transform duration-100 active:scale-[.97] data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
  variants: {
    intent: {
      primary: "bg-primary text-primary-content hover:bg-primary-400",
      secondary: "bg-secondary text-secondary-content hover:bg-secondary-400",
    },
    state: {
      filled: "",
      ghost: "",
      soft: "",
      outline: "",
    },
    size: {
      sm: "h-8 px-2 text-sm",
      md: "text-md h-10 px-4",
      lg: "h-12 px-6 text-lg",
    },
  },
  compoundVariants: [
    {
      intent: "primary",
      state: "ghost",
      className: "border-2 border-primary bg-primary/30 hover:bg-primary",
    },
    {
      intent: "primary",
      state: "soft",
      className:
        "bg-primary-200 text-primary-600 hover:bg-primary-400 hover:text-black",
    },
    {
      intent: "primary",
      state: "outline",
      className:
        "border-2 border-primary bg-transparent text-primary-content hover:bg-primary-500 hover:text-black",
    },
    {
      intent: "secondary",
      state: "ghost",
      className: "border-2 border-primary bg-primary/30 hover:bg-primary",
    },
    {
      intent: "secondary",
      state: "soft",
      className:
        "bg-primary-200 text-primary-600 hover:bg-primary-400 hover:text-black",
    },
    {
      intent: "secondary",
      state: "outline",
      className:
        "border-2 border-primary bg-transparent text-primary-content hover:bg-primary-500 hover:text-black",
    },
  ],
  defaultVariants: {
    intent: "primary",
    state: "filled",
    size: "md",
  },
});

export type buttonProps = VariantProps<typeof button>;

Here is a Button component using the button with several variants.

import { button } from "@/components/base/button";

<Button intent="secondary" state="outline">Button</Button>;

You can use the variants inline like shown above, or pass them as props to your component itself.

Overrides

Tailwind Variants provides excellent support for overriding styles and handling conflicts. The library uses tw-merge under the hood. All Tailwind Variants components provide an optional class / className for overriding classes on any component. Be sure to check out the Tailwind Variants docs for more info info.

Examples

Assembled components built from the Base components, such as the ComboBox above are distributed via VScode snippets. This allows you to rapidly build complex systems with just a few keystrokes. Have a look at the VsCode Docs for how to use.