🌴Ocean UI

Next.js 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 Next.js, React, and TypeScript
  • A text editor or IDE

This guide uses Next.js with the App Router. The steps may vary slightly for Pages Router or other React frameworks.

Step 1: Create a Next.js Project

Start by creating a new Next.js project using the latest version:

pnpm create next-app@latest my-app --yes
cd my-app
pnpm dev

This creates a new Next.js project with TypeScript and the App Router 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 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 React for icons. Install these core dependencies:

pnpm install @ark-ui/react lucide-react
  • @ark-ui/react: Provides accessible, headless UI primitives - lucide-react: 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 components/ui/accordion.tsx:

"use client";

import { cn } from "@/lib/utils";
import { Accordion as AccordionPrimitive } from "@ark-ui/react/accordion";
import { ChevronDownIcon } from "lucide-react";
import type { ComponentProps, ReactNode } from "react";

function Accordion({
  className,
  ...props
}: ComponentProps<typeof AccordionPrimitive.Root>) {
  return (
    <AccordionPrimitive.Root className={cn("w-full", className)} {...props} />
  );
}

function AccordionItem({
  className,
  ...props
}: ComponentProps<typeof AccordionPrimitive.Item>) {
  return (
    <AccordionPrimitive.Item
      className={cn("border-b border-border last:border-b-0", className)}
      {...props}
    />
  );
}

interface AccordionTriggerProps
  extends ComponentProps<typeof AccordionPrimitive.ItemTrigger> {
  /**
   * Optional icon or content to display on the left side
   */
  leftIcon?: ReactNode;
  /**
   * Optional icon or content to display on the right side
   * If not provided, defaults to ChevronDownIcon
   */
  rightIcon?: ReactNode;
}

function AccordionTrigger({
  className,
  children,
  leftIcon,
  rightIcon,
  ...props
}: AccordionTriggerProps) {
  return (
    <AccordionPrimitive.ItemTrigger
      className={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",
        className
      )}
      {...props}
    >
      {/* Left Icon Slot */}
      {leftIcon}

      {/* Title/Content */}
      <span className="flex-1">{children}</span>

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

function AccordionContent({
  className,
  children,
  ...props
}: ComponentProps<typeof AccordionPrimitive.ItemContent>) {
  return (
    <AccordionPrimitive.ItemContent
      className={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",
        className
      )}
      {...props}
    >
      <div className={cn("pb-4", className)}>{children}</div>
    </AccordionPrimitive.ItemContent>
  );
}

export { Accordion, AccordionItem, AccordionTrigger, 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.

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 app/token.css with the following content:

Create app/token.css:

app/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;
  }
}

@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 components/shared/faq-accordion.tsx:

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

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 className="w-full max-w-lg mx-auto" defaultValue={[]}>
      {accordionItems.map((item) => (
        <AccordionItem key={item.value} value={item.value}>
          <AccordionTrigger>{item.title}</AccordionTrigger>
          <AccordionContent className="text-tertiary">
            {item.content}
          </AccordionContent>
        </AccordionItem>
      ))}
    </Accordion>
  );
}

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 Next.js pages or components:

Update app/page.tsx:

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

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

Your Ocean UI component should now be working in your Next.js 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)

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
›Troubleshooting

Components Not Styling Correctly

If components don't look right, ensure:
  • token.css is properly imported in globals.css
  • Tailwind CSS is configured correctly
  • All CSS variables from token.css are available

Import Errors

If you see import errors:
  • Verify all dependencies are installed (@ark-ui/react, lucide-react)
  • Check that the cn utility is in lib/utils.ts
  • Ensure TypeScript path aliases are configured (@/ should resolve to your project root)

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

Dark Mode Issues

If dark mode isn't working:
  • Ensure token.css includes dark mode variables (inside .dark class)
  • Verify your Next.js app has dark mode configured
  • Check that the dark class is applied to your root element

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

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

On this page