> This page is for Developer.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.bigcommerce.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.bigcommerce.com/_mcp/server.

# Components and Theming

Plan: Composable Developer

Lesson 9 of 19 · 45 min

## Introduction

The default Catalyst theme is built on a design system of modern React components for use in composable websites, built and maintained by BigCommerce.

This component system is designed specifically for ecommerce, providing beautiful, out-of-the-box UI for every part of the commerce experience. The library includes general, low-level components like buttons and accordions, as well as orchestrating these lower level components into more sophisticated presentations like product cards, product lists, and a site header.

The component library leverages modern patterns and the latest features of Next.js, and it is optimized for fast performance. The clean, practical prop interfaces of its components make them flexible and reusable in any context where they're needed within your UI.

The look and feel of your storefront is easily style-able without the need for modifying component code. However, all theme components are also included directly in your project, ready to be customized to suit your own requirements.

## File Structure

The theme components are found in **vibes/soul** within your Catalyst project. This includes several major sub-sections:

* **form:** Contains streamlined implementations of common form controls
* **lib:** Contains general utilities that are used within various components
* **primitives:** Contains unique component implementations for various pieces of UI, from the smallest general building blocks like buttons to more complex UI like accordions and product cards
* **sections:** Components in this area represent larger UI sections built from the more primitive components or other sections, such as a "product detail" section built by orchestrating together a product gallery component, rating component, product form component, etc.

## Tailwind CSS

[Tailwind CSS](https://tailwindcss.com/) is a popular CSS framework that enables rapid and reliable styling and visual layouts with a system of utility classes.

With Tailwind, instead of writing raw CSS, you chain together utility classes to accomplish the styling you need, as in this example from `vibes/soul/sections/cart/index.tsx`:

```jsx showLineNumbers={false}
<div>
  {/* Utility classes dealing with margins and text formatting */}
  <h1 className="mb-10 ... text-4xl font-medium leading-none @xl:text-5xl">
     {title}
     ...
  </h1>

   ...

  {/* Utility classes dealing with flexbox layout */}
  <ul className="flex flex-col gap-5 ...">
    ...
```

Tailwind is used by Next.js by default, and Catalyst components fully embrace Tailwind styling.

## Styling Components

The Tailwind styles used throughout your theme components apply a consistent look and feel through a set of standardized values for text styles, font sizes, and color palette. It's easy to customize the theme of your storefront without changing the basic structure and behavior of its components, using Tailwind and CSS configuration.

* **tailwind.config.js:** This config file contains Tailwind's basic configuration, including the definition of common styles in *theme.extend*. These include named styles for things like colors and fonts. Most styles in this config are tied to CSS variables that are defined elsewhere, but you expand your available styles by adding new key names.
* **globals.css:** The CSS variables in this file provide the values for the styles defined in `tailwind.config.js`. You have a great deal of flexibility for adjusting your storefront's theme simply by modifying these vars.
* **app/fonts.ts:** Font family definitions are loaded here. By default, available fonts are loaded via the package `next/font/google`. The CSS variables applied to each font family here are utilized for standard font styles in the Tailwind config.

> **Info**
>
> **Makeswift Style Customization**
>
> In your own codebase, you may notice that `globals.css` doesn't define all the CSS variables used in `tailwind.config.js`. Color values, for example, are not defined here.
>
> With Makeswift integrated, many hard-coded CSS variables are replaced with common color and font styles defined in the builder. These can then be applied to the properties of specific component types, allowing the storefront-wide look and feel to be modified without touching a line of code.
>
> Explore more details about managing global styles in Makeswift Core.

### Try It Out

Try practicing the use of global styles and configuration to modify your Catalyst theme.

1. **Browse** to a product detail page on your local site.
2. **Modify** `globals.css` to change the value of `--font-size-base` to a larger or smaller value.
3. **Observe** the effect on the page's "Add to cart" button.

Note in `tailwind.config.js` where the CSS variable is used: for the `fontSize.base` configuration. A Tailwind text size class expressed as `text-base` should reflect this config. You can observe where this very class is used in the definition of the `Button` component (in the theme directory, in `primitives/button/index.tsx`).

4. Try **adding** your own `fontSize` value in `tailwind.config.js`, which can have an arbitrary key:

```js showLineNumbers={false}
const config = {
  ...
  theme: {
    extend: {
      ...
      fontSize: {
        '2xs': '0.5rem',
        ...
      },
    },
  },
  ...
};
```

5. **Open** the file where the `Button` component is defined and replace occurrences of `text-base` with `text-2xs`.
6. **Observe** the effect on the page's "Add to cart" button.

> **Info**
>
> To follow the typical Catalyst pattern, you can take this further by establishing a global CSS variable for this size in `app/globals.css`, and reference this in your Tailwind config.

## Component Variation

### Component CSS Variables

Theme components implement a number of specialized CSS variables that can be used to control the general look and feel of those components, globally or in an isolated context.

This example can be seen in `vibes/soul/primitives/card/index.tsx`:

````text showLineNumbers={false}
/**
 * This component supports various CSS variables for theming. Here's a comprehensive list, along
 * with their default values:
 *
 * ```css
 * :root {
 *   --card-focus: hsl(var(--primary));
 *   --card-light-offset: hsl(var(--background));
 *   --card-light-text: hsl(var(--foreground));
 *   --card-light-icon: hsl(var(--foreground));
 *   --card-light-background: hsl(var(--contrast-100));
 *   --card-dark-offset: hsl(var(--foreground));
 *   --card-dark-text: hsl(var(--background));
 *   --card-dark-icon: hsl(var(--background));
 *   --card-dark-background: hsl(var(--contrast-500));
 *   --card-font-family: var(--font-family-body);
 *   --card-border-radius: 1rem;
 * }
 * ```
 */
export function Card({
  ...
}: CardProps) {
````

Taking `--card-border-radius` as an example, the `Card` component will fall back to a default if this CSS variable is not set but will use its value otherwise. You can take advantage of this in two ways:

* Set a value for `--card-border-radius` globally.
* In a project *not* integrated with Makeswift, this can be done in `app/globals.css`.
* With the Makeswift integration, global values for these component CSS vars are set by properties in the Makeswift editor, in the Site Theme element. Adding these variables to `app/globals.css` will have no effect.
* Set a unique value for `--card-border-radius` directly on an ancestor element in specific context, affecting only a `Card` in that context.

> **Info**
>
> Explore more details about the process of theming components with Makeswift in Makeswift Core.

### Variants

Many components include multiple variants.

See an example in the simple `Button` component (`vibes/soul/primitives/button/index.tsx`). This component defines "primary", "secondary" and "tertiary" variants, controlled by a prop, each of which is associated with its own unique styles.

Including a specific component variant is then as simple as passing a `variant` prop:

```jsx showLineNumbers={false}
import { Button } from '@/vibes/soul/primitives/button';

export default function MyComponent() {
  return (
    <Button variant="secondary">Click Me</Button>
  );
}
```

### Color Scheme

Similarly to the concept of a variant, some components accept a prop called `colorScheme` (or more specific props like `textColorScheme`).

```jsx showLineNumbers={false}
import { ProductCard } from '@/vibes/soul/primitives/product-card';

export default function MyComponent() {
  return (
    <ProductCard colorScheme="dark" ... />
  );
}
```

The use of this prop will result in the component applying default styles suitable for "light" or "dark" mode. Note that, for example, `ProductCard` makes use of CSS vars `--product-card-light-background` and `--product-card-dark-background`, selectively applying the appropriate one for the current color scheme.

### Try It Out

Practice the use of targeted CSS variables to control the appearance of a component in a specific context.

The `Button` component supports the CSS variable `--button-primary-background` to set the background color when the "primary" variant is rendered. A global value for this var (set either in `app/globals.css` or via the Site Theme configuration in Makeswift) will apply to all buttons in the storefront. However, we can set it in a narrower context if required.

1. **Browse** to a product detail page on your local site.
2. **Open** the file `app/[locale]/(default)/product/[slug]/page.tsx` and **find** `<ProductDetail>`.
3. **Wrap** `<ProductDetail>` in an element with an appropriate `style` prop to set the button background CSS var.

```html showLineNumbers={false}
<div
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  style={{
    '--button-primary-background': '#f00',
  } as React.CSSProperties}
>
  <ProductDetail
    ...
  />
</div>
```

4. **Observe** the effect on your page's "Add to cart" button.

This is a bit of a contrived example, as you will likely want to keep a consistent look and feel for buttons throughout the theme. If you do need additional appearance options beyond the existing variants, it would be advisable to modify your `Button` component definition to add new variants.

## The Toaster Component

The `Toaster` component enables simple and unobtrusive UI notifications displayed to users in response to their interactions, such as a success or error message after an "add to cart" action completes, is an important UI concern but can be cumbersome to build out from scratch.

The necessary layout config is built into Catalyst by default, and notifications can be initiated from anywhere:

```js showLineNumbers={false}
import { toast } from '@/vibes/soul/primitives/toaster';

toast.success('This is a success message');

toast.error('This is an error message');
```

## Asynchronous Rendering with *Stream*

As we've seen, `Suspense` boundaries can be used to render components asynchronously, allowing their content to be streamed to the browser when ready.

Catalyst enhances this capability with the component `Stream` (see `vibes/soul/lib/streamable.tsx`). `Stream`, which uses `Suspense` internally, accomplishes several design goals while supporting an excellent developer experience with data components:

* **Components that handle their own fallbacks** - The page route or parent component shouldn't need to concern itself with whether a fallback is needed.  This is a concern of the component itself.
* **Components that are data agnostic** - Components should be concerned only with presentation, not how the data is loaded. This makes for more reusable components that can support a variety of data loading strategies.
* **Support for both synchronous and asynchronous data** - Components shouldn't be concerned with how data is fetched, seamlessly handling either loaded data for a Promise for that data.
* **Server-side and client-side compatibility** - It should be possible to drop a component into a React server component or a client-side context, with any Promise being resolved appropriately in either context.

In this simple example, the `TopCategories` component expects a "streamable" list of categories, meaning that the prop may be a Promise yet to be resolved or may be a simple array. The `<Stream>` component and its callback function take care of displaying a fallback if necessary until the streamable value is available, and the wrapped JSX can deal with categories in typical fashion.

```jsx showLineNumbers={false}
import { Stream, Streamable } from '@/vibes/soul/lib/streamable';

interface Category {
  ...
}

export async function TopCategories({
  categories: streamableCategories,
}: {
  // Accept a Streamable
  categories: Streamable<Category[]>;
}) {
  return <>
    // Wrap UI in <Stream> with a streamable value
    <Stream value={streamableCategories} fallback={<FallbackComponent />}>
      {(categories) => (

        <div>...</div>

      )}
    </Stream>
  </>;
}
```

This pattern keeps the concern of fallback behavior within the component while also avoiding assumptions about receiving a Promise. The data fetching is a concern outside the component.

In the context where the data fetch is set up, the recommended pattern is to use the `Streamable.from` callback to create a Promise that will not be executed until the component ultimately awaits it.

```jsx showLineNumbers={false}
import { Stream, Streamable } from '@/vibes/soul/lib/streamable';

export async function PageRoute() {
  // Wrapping the fetch in Streamable.from
  const streamableCategories = Streamable.from(async () => {
    return await getTopCategories();
  });

  return <>
    <TopCategories categories={streamableCategories} />
  </>;
}
```