Styling

How to style and customize Apsara components in your application.

Apsara uses vanilla CSS with CSS custom properties (tokens) for all styling. There is no runtime CSS-in-JS — styles are static, scoped, and predictable. This guide covers the recommended ways to customize components and build your own styled elements.

Using Design Tokens

Design tokens are CSS custom properties that adapt automatically to the active theme. Always use tokens instead of hard-coded values — this ensures your UI stays consistent and responds to light/dark mode changes.

1.card {
2 color: var(--rs-color-foreground-base-primary);
3 background: var(--rs-color-background-base-secondary);
4 border: 1px solid var(--rs-color-border-base-primary);
5 padding: var(--rs-space-5);
6 border-radius: var(--rs-radius-3);
7 box-shadow: var(--rs-shadow-feather);
8 font-size: var(--rs-font-size-small);
9}

See the Theme section for the complete token reference.

Customizing Components

With className

Every Apsara component accepts a className prop. Use it alongside tokens to add custom styles:

1import { Button } from "@raystack/apsara";
2
3<Button className="my-button" variant="outline">
4 Custom styled
5</Button>
1.my-button {
2 min-width: 200px;
3 border-radius: var(--rs-radius-5);
4}

With style Prop

For one-off adjustments, use the inline style prop:

1<Flex gap="4" style={{ maxWidth: 600, margin: "0 auto" }}>
2 <Button style={{ flex: 1 }}>Full width</Button>
3</Flex>

With Data Attributes

Apsara components built on Base UI expose data attributes that reflect component state. Use these for state-driven styling without JavaScript:

1/* Style a sidebar based on open/closed state */
2.my-sidebar[data-open] {
3 width: 240px;
4}
5
6.my-sidebar[data-closed] {
7 width: 57px;
8}
9
10/* Style based on active state */
11.my-nav-item[data-active="true"] {
12 background: var(--rs-color-background-neutral-secondary);
13}
14
15/* Animate entry and exit */
16.my-overlay[data-starting-style],
17.my-overlay[data-ending-style] {
18 opacity: 0;
19}

Common data attributes include data-open, data-closed, data-active, data-disabled, data-state, data-starting-style, and data-ending-style.

Theming with Data Attributes

The ThemeProvider sets data attributes on the root <html> element. Use these to conditionally style elements based on the active theme:

1/* Dark mode specific styles */
2[data-theme="dark"] .custom-card {
3 border-color: var(--rs-color-border-base-tertiary);
4}
5
6/* Traditional style variant */
7[data-style="traditional"] .custom-heading {
8 font-family: var(--rs-font-family-serif);
9}

Available theme attributes:

AttributeValues
data-themelight, dark
data-stylemodern, traditional
data-accent-colorindigo, orange, mint
data-gray-colorgray, mauve, slate, sage

Writing Component Styles

When building custom components in your application, follow these patterns used internally by Apsara:

Use CSS Modules for Scoping

CSS Modules prevent class name collisions and keep styles co-located with components:

1// status-card.tsx
2import styles from "./status-card.module.css";
3
4export function StatusCard({ status, children }) {
5 return (
6 <div className={`${styles.card} ${styles[`card-${status}`]}`}>
7 {children}
8 </div>
9 );
10}
1/* status-card.module.css */
2.card {
3 padding: var(--rs-space-4);
4 border-radius: var(--rs-radius-3);
5 border: 1px solid var(--rs-color-border-base-primary);
6}
7
8.card-success {
9 border-color: var(--rs-color-border-success-emphasis);
10 background: var(--rs-color-background-success-primary);
11}
12
13.card-danger {
14 border-color: var(--rs-color-border-danger-emphasis);
15 background: var(--rs-color-background-danger-primary);
16}

Use CVA for Variant Management

Apsara uses class-variance-authority (CVA) to manage component variants with type safety. This is recommended for components with multiple visual variations:

1import { cva, type VariantProps } from "class-variance-authority";
2import styles from "./tag.module.css";
3
4const tag = cva(styles.tag, {
5 variants: {
6 size: {
7 small: styles["tag-small"],
8 medium: styles["tag-medium"],
9 },
10 color: {
11 neutral: styles["tag-neutral"],
12 accent: styles["tag-accent"],
13 },
14 },
15 defaultVariants: {
16 size: "medium",
17 color: "neutral",
18 },
19});
20
21type TagProps = VariantProps<typeof tag> & { className?: string };
22
23export function Tag({ size, color, className, children }: TagProps) {
24 return <span className={tag({ size, color, className })}>{children}</span>;
25}

CVA handles merging the base class, variant classes, and any custom className passed by consumers.

Best Practices

Use semantic tokens, not primitives. Prefer --rs-color-foreground-base-primary over --rs-neutral-12. Semantic tokens automatically adapt across themes; primitives do not carry semantic meaning.

Avoid hard-coded colors. Every color value should come from a token. This guarantees proper light/dark mode support and visual consistency.

Use spacing tokens for layout. Consistent spacing creates visual rhythm. Use --rs-space-* tokens for padding, margin, and gap values.

Keep specificity low. Use single class selectors. Avoid !important and deeply nested selectors that are hard to override.

Co-locate styles with components. Place .module.css files next to their component files. This makes it easy to find and maintain styles.