🌴Ocean UI

SolidJS Installation

Prerequisites

Before you begin, make sure you have the following:
  • Node.js (v18 or higher)
  • pnpm package manager (or npm/yarn)
  • Basic knowledge of SolidJS, Vite, and TypeScript
  • A text editor or IDE

This guide uses Vite with SolidJS. The steps may vary slightly for other build tools or SolidJS meta-frameworks like SolidStart.

Step 1: Create a SolidJS Project

Start by creating a new SolidJS project using the official SolidJS template:

pnpm create solid
cd solid-project
pnpm dev

This creates a new SolidJS project with TypeScript and Vite enabled. Once the project is created, navigate to the project directory and start the development server to verify everything is working.

Step 2: Install Utility Dependencies

Ocean UI components use utility functions for merging Tailwind CSS classes. Install the required dependencies:

pnpm i tailwind-merge clsx

These packages help merge and resolve Tailwind CSS class names, preventing conflicts and ensuring proper styling.

Step 3: Create the CN Utility Function

Create a utility function that combines clsx and tailwind-merge for optimal class name handling. This function is used throughout Ocean UI components.

Create a new file src/lib/utils.ts:

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

The cn function intelligently merges Tailwind classes, ensuring that conflicting classes (like p-4 and p-2) resolve correctly, with the last one taking precedence.

Step 4: Install Core Dependencies

Ocean UI components are built on top of Ark UI primitives and use Lucide Solid for icons. Install these core dependencies:

pnpm install @ark-ui/solid lucide-solid
  • @ark-ui/solid: Provides accessible, headless UI primitives for SolidJS - lucide-solid: Icon library used throughout Ocean UI components

Step 5: Copy Base Components

Base components are the core, reusable components that wrap Ark UI primitives. These are the building blocks for your UI.

Copy the base component you want to use from Ocean UI's component library. For example, to use the Accordion component:

Create src/components/ui/accordion.tsx:

import {
  AccordionRoot,
  AccordionItem as AccordionItemPrimitive,
  AccordionItemTrigger as AccordionItemTriggerPrimitive,
  AccordionItemContent as AccordionItemContentPrimitive,
  type AccordionRootProps,
  type AccordionItemProps,
  type AccordionItemTriggerProps,
  type AccordionItemContentProps,
} from "@ark-ui/solid/accordion";
import { ChevronDownIcon } from "lucide-solid";
import type { JSX, ParentComponent } from "solid-js";
import { splitProps } from "solid-js";

import { cn } from "@/lib/utils";

export interface AccordionProps extends AccordionRootProps {}

export const Accordion: ParentComponent<AccordionProps> = (props) => {
  const [local, rest] = splitProps(props, ["class", "children"]);
  return (
    <AccordionRoot class={cn("w-full", local.class)} {...rest}>
      {local.children}
    </AccordionRoot>
  );
};

export interface AccordionItemComponentProps extends AccordionItemProps {}

export const AccordionItemComponent: ParentComponent<
  AccordionItemComponentProps
> = (props) => {
  const [local, rest] = splitProps(props, ["class", "children"]);
  return (
    <AccordionItemPrimitive
      class={cn("border-b border-border last:border-b-0", local.class)}
      {...rest}
    >
      {local.children}
    </AccordionItemPrimitive>
  );
};

export interface AccordionTriggerComponentProps
  extends AccordionItemTriggerProps {
  /**
   * Optional icon or content to display on the left side
   */
  leftIcon?: JSX.Element;
  /**
   * Optional icon or content to display on the right side
   * If not provided, defaults to ChevronDownIcon
   */
  rightIcon?: JSX.Element;
}

export const AccordionTriggerComponent: ParentComponent<
  AccordionTriggerComponentProps
> = (props) => {
  const [local, rest] = splitProps(props, [
    "class",
    "children",
    "leftIcon",
    "rightIcon",
  ]);
  return (
    <AccordionItemTriggerPrimitive
      class={cn(
        "group flex w-full items-center gap-4 py-4 text-left cursor-pointer text-sm font-medium transition-all outline-none",
        "hover:underline",
        "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
        "disabled:pointer-events-none disabled:opacity-50",
        "[&[data-state=open]>.accordion-chevron]:rotate-180",
        "data-[state=closed]:text-primary/60 data-[state=closed]:hover:text-primary data-[state=open]:text-primary",
        local.class
      )}
      {...rest}
    >
      {/* Left Icon Slot */}
      {local.leftIcon}

      {/* Title/Content */}
      <span class="flex-1">{local.children}</span>

      {/* Right Icon Slot */}
      {local.rightIcon ? (
        <span class="ml-auto">{local.rightIcon}</span>
      ) : (
        <ChevronDownIcon class="accordion-chevron text-muted-foreground pointer-events-none size-4 shrink-0 transition-transform duration-200 ml-auto" />
      )}
    </AccordionItemTriggerPrimitive>
  );
};

export interface AccordionContentComponentProps
  extends AccordionItemContentProps {}

export const AccordionContentComponent: ParentComponent<
  AccordionContentComponentProps
> = (props) => {
  const [local, rest] = splitProps(props, ["class", "children"]);
  return (
    <AccordionItemContentPrimitive
      class={cn(
        "overflow-hidden text-sm",
        // Ark UI recommended animations
        "data-[state=closed]:opacity-0 data-[state=open]:opacity-100",
        "data-[state=closed]:animate-accordion-collapse data-[state=open]:animate-accordion-expand",
        // Ark UI recommended data selectors for custom animations:
        "[data-scope=accordion][data-part=item-content][data-state=open]:animate-[slideDown_250ms_ease-in-out]",
        "[data-scope=accordion][data-part=item-content][data-state=closed]:animate-[slideUp_200ms_ease-in-out]",
        local.class
      )}
      {...rest}
    >
      <div class="pb-4">{local.children}</div>
    </AccordionItemContentPrimitive>
  );
};

// Export with cleaner names
export {
  AccordionItemComponent as AccordionItem,
  AccordionTriggerComponent as AccordionTrigger,
  AccordionContentComponent as AccordionContent,
};

Base components are reusable and can be customized. They handle the core functionality and styling, but you can extend them with additional props or styling as needed. Note that SolidJS uses class instead of className, and components use ParentComponent with splitProps for prop handling.

Step 6: Set Up Token CSS

Ocean UI uses a comprehensive design token system for consistent styling. The token.css file contains all color variables, spacing, typography, shadows, and animations used throughout the components.

Copy the token.css file from Ocean UI and add it to your project. Create src/token.css with the following content:

Create src/token.css:

src/token.css
@theme inline {
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --color-chart-1: var(--chart-1);
  --color-chart-2: var(--chart-2);
  --color-chart-3: var(--chart-3);
  --color-chart-4: var(--chart-4);
  --color-chart-5: var(--chart-5);
  --color-sidebar: var(--sidebar);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-ring: var(--sidebar-ring);

  /* Transparent color */
  --color-transparent: oklch(0 0 0 / 0);

  /* Animations */
  --animate-flip: flip 6s infinite steps(2, end);
  --animate-rotate: rotate 3s linear infinite both;

  @keyframes flip {
    to {
      transform: rotate(360deg);
    }
  }

  @keyframes rotate {
    to {
      transform: rotate(90deg);
    }
  }

  /* Accordion */
  --animate-accordion-collapse: accordion-collapse var(--tw-duration, 200ms)
    ease-out;
  --animate-accordion-expand: accordion-expand var(--tw-duration, 200ms)
    ease-out;

  @keyframes accordion-collapse {
    from {
      height: var(--height);
    }
    to {
      height: 0;
    }
  }

  @keyframes accordion-expand {
    from {
      height: 0;
    }
    to {
      height: var(--height);
    }
  }
}

:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);

  /* Fumadocs */
  --color-fd-diff-remove: oklch(0.88 0.06 18 / 0.7);
  --color-fd-diff-add: oklch(0.9 0.09 164 / 0.5);
  --color-fd-diff-add-symbol: oklch(0.45 0.11 151);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);

  /* Fumadocs */
  --color-fd-diff-remove: oklch(0.64 0.21 25 / 0.4);
  --color-fd-diff-remove-symbol: oklch(0.81 0.1 20);
  --color-fd-diff-add: oklch(0.87 0.14 154 / 0.25);
  --color-fd-diff-add-symbol: oklch(0.96 0.04 157);
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }
  body {
    @apply bg-background text-foreground;
  }
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    @apply tracking-tight;
  }
}

Then import it in your main entry file (typically src/index.css or src/app.css):

@import "tailwindcss";
@import "./token.css";

The token.css file is essential for Ocean UI components to look and function correctly. It includes: - Color system (light and dark mode variables) - Typography scales - Spacing system - Border radius values - Shadow definitions

  • Animation keyframes - Component-specific tokens

You can modify token.css to customize the design system to match your brand, but ensure you maintain the required CSS variable names that components depend on.

Step 7: Create Example Components

Example components (also called "shared" or "demo" components) are complete, ready-to-use implementations that demonstrate how to use base components in real scenarios.

Create an example component that uses your base component. For instance, create src/components/shared/faq-accordion.tsx:

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";
import { For } from "solid-js";

const accordionItems = [
  {
    value: "item-1",
    title: "How do I update my account information?",
    content:
      'You can update your account information by navigating to your profile settings. Click on your profile icon in the top right corner, then select "Account Settings" from the dropdown menu. From there, you can edit your name, email, password, and other personal details.',
  },
  {
    value: "item-2",
    title: "What payment methods are accepted?",
    content:
      "We accept all major credit cards (Visa, Mastercard, American Express), debit cards, PayPal, Apple Pay, and Google Pay. All transactions are processed securely through our encrypted payment gateway to ensure your financial information is protected.",
  },
  {
    value: "item-3",
    title: "How can I track my order?",
    content:
      "Once your order has been shipped, you'll receive a tracking number via email. You can use this tracking number on our website's tracking page or on the carrier's website to monitor your package's journey in real-time. You'll receive updates at each stage of the delivery process.",
  },
];

export default function AccordionDemo() {
  return (
    <Accordion class="w-full max-w-lg mx-auto" defaultValue={[]}>
      <For each={accordionItems}>
        {(item) => (
          <AccordionItem value={item.value}>
            <AccordionTrigger>{item.title}</AccordionTrigger>
            <AccordionContent class="text-tertiary">
              {item.content}
            </AccordionContent>
          </AccordionItem>
        )}
      </For>
    </Accordion>
  );
}

In SolidJS, use the <For> component instead of .map() for rendering lists. This is the idiomatic SolidJS pattern for reactive list rendering.

Example components show practical usage patterns and can be used directly in your application or serve as templates for your own implementations.

Step 8: Use Components in Your App

Now you can import and use your example component in your SolidJS pages or components:

Update src/App.tsx (or your main component file):

import AccordionDemo from "@/components/shared/faq-accordion";

export default function App() {
  return (
    <div>
      <AccordionDemo />
    </div>
  );
}

Your Ocean UI component should now be working in your SolidJS application!

Understanding the Component Structure

It's important to understand the difference between base components and example components:

Base Components (components/ui/)

Base components are the core, reusable building blocks:
  • Located in components/ui/
  • Wrap Ark UI primitives with Ocean UI styling
  • Highly reusable and customizable
  • Export multiple sub-components (e.g., Accordion, AccordionItem, AccordionTrigger, AccordionContent)
  • Use SolidJS patterns: ParentComponent, splitProps, and class prop

Example Components (components/shared/)

Example components are complete implementations:
  • Located in components/shared/ (or components/demos/)
  • Use base components to create real-world examples
  • Show practical usage patterns
  • Can be used directly or as templates
  • Use SolidJS patterns like <For> for list rendering
›Troubleshooting

Components Not Styling Correctly

If components don't look right, ensure:
  • token.css is properly imported in your main CSS file
  • Tailwind CSS is configured correctly in your Vite project
  • All CSS variables from token.css are available
  • The @import "tailwindcss" directive is included before importing token.css

Import Errors

If you see import errors:
  • Verify all dependencies are installed (@ark-ui/solid, lucide-solid)
  • Check that the cn utility is in src/lib/utils.ts
  • Ensure TypeScript path aliases are configured in tsconfig.json (@/ should resolve to your src/ directory)
  • Verify Vite is configured to resolve the @/ alias (check vite.config.ts)

Animation Not Working

If animations aren't working:
  • Ensure token.css includes the accordion animation keyframes
  • Check that Tailwind CSS animations are enabled
  • Verify the animation classes are defined in your Tailwind config
  • Make sure Tailwind CSS v4 is properly configured in your Vite project

Dark Mode Issues

If dark mode isn't working:
  • Ensure token.css includes dark mode variables (inside .dark class)
  • Verify your SolidJS app has dark mode configured (you may need to add a dark mode toggle)
  • Check that the dark class is applied to your root element (typically <html> or <body>)
  • Consider using a library like solid-js/store or a theme provider for dark mode management

SolidJS-Specific Issues

If you encounter SolidJS-specific issues:
  • Ensure you're using class instead of className for styling
  • Use <For> component instead of .map() for list rendering
  • Remember to use splitProps when destructuring props in components
  • Verify that components are using ParentComponent type for proper typing
  • Check that reactive primitives (signals, stores) are used correctly if needed

Next Steps

Now that you have Ocean UI set up manually, you can:
  • Explore other base components from Ocean UI
  • Customize the token.css to match your brand
  • Create your own example components
  • Extend base components with additional functionality
  • Check out component-specific documentation for advanced usage
  • Learn more about SolidJS reactivity patterns and best practices

For more information about specific components, visit their individual documentation pages.

On this page