Remix

Learn how to use SmoothUI animated React components in Remix and React Router v7 projects with SSR support, Tailwind CSS 4, and Motion.

Last updated: March 7, 2026

Overview

Remix is a full-stack React framework focused on web standards and progressive enhancement. As of v2, Remix has evolved into the framework mode of React Router v7, bringing the same SSR-first architecture with a streamlined API. SmoothUI components work seamlessly in both Remix and React Router v7 projects.

How It Works

SmoothUI components are standard React client components. Remix renders them on the server and hydrates them on the client. The "use client" directive ensures components with animations and browser APIs hydrate correctly. No special wrappers are needed for most components.

Prerequisites

Before getting started, make sure you have the following:

RequirementMinimum VersionPurpose
Node.js18+Runtime
Remix / React Router2.0+ / 7.0+Framework
React19.0+UI library
Tailwind CSS4.0+Styling
Motion12.0+Animations (Framer Motion)

Remix or React Router v7?

Remix has merged into React Router v7. If you're starting a new project, use create react-router. Existing Remix v2 projects can upgrade to React Router v7 — see the migration guide. Both setups work identically with SmoothUI.

Project Setup

Create a New Project

If you're starting from scratch, create a new React Router v7 project (the successor to Remix):

pnpm create react-router@latest my-smoothui-app

Then install dependencies:

cd my-smoothui-app && pnpm install

Set Up Tailwind CSS 4

Remix and React Router v7 use Vite under the hood, so Tailwind CSS 4 setup uses the Vite plugin:

pnpm add tailwindcss @tailwindcss/vite

Add the Tailwind CSS Vite plugin to your Vite config:

vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
});

Update your main CSS file with the Tailwind import:

app/app.css
@import "tailwindcss";

Install Motion

SmoothUI animations are powered by Motion (Framer Motion). Install it as a dependency:

pnpm add motion

Configure Path Aliases

SmoothUI components use the @/ path alias. React Router v7 projects typically use vite-tsconfig-paths to resolve paths from tsconfig.json automatically. Make sure your tsconfig includes the alias:

tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./app/*"]
    }
  }
}

vite-tsconfig-paths

React Router v7 projects include vite-tsconfig-paths by default, which reads your tsconfig.json paths and applies them as Vite aliases. You only need to configure paths in one place — your tsconfig.

Installing SmoothUI Components

Using the SmoothUI CLI

pnpm dlx smoothui-cli@latest add siri-orb

Using the shadcn CLI

pnpm dlx shadcn@latest add @smoothui/siri-orb

shadcn Configuration

If this is your first time using the shadcn CLI in a Remix / React Router project, it will prompt you to create a components.json file. Accept the defaults — the CLI auto-detects the project structure and configures paths accordingly.

SSR Considerations

Remix and React Router v7 render components on the server first, then hydrate them on the client. Here's what you need to know when using SmoothUI components in an SSR environment:

ConcernHow SmoothUI Handles It
"use client" directiveSmoothUI components include this directive. Remix respects it for client-side hydration.
Motion SSRMotion handles SSR gracefully — animations simply start after hydration. No special config needed.
Browser APIsComponents that use window or localStorage during render need a ClientOnly wrapper.
Hydration mismatchesRare with SmoothUI. If seen, ensure no random values are generated during SSR (see Troubleshooting).

The ClientOnly Pattern

If you use a SmoothUI component that relies on browser measurements during initial render, wrap it with a ClientOnly helper:

app/components/client-only.tsx
"use client";

import { type ReactNode, useEffect, useState } from "react";

export type ClientOnlyProps = {
  children: ReactNode;
  fallback?: ReactNode;
};

const ClientOnly = ({ children, fallback = null }: ClientOnlyProps) => {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  return mounted ? children : fallback;
};

export default ClientOnly;
app/routes/home.tsx
import ClientOnly from "@/components/client-only";
import { SiriOrb } from "@/components/smoothui/ui/SiriOrb";

const Home = () => {
  return (
    <ClientOnly fallback={<div className="h-[200px] w-[200px]" />}>
      <SiriOrb size="200px" />
    </ClientOnly>
  );
};

export default Home;

Most Components Don't Need ClientOnly

The majority of SmoothUI components work fine with SSR out of the box. Only use ClientOnly for components that access browser-only APIs like window.matchMedia or ResizeObserver during their initial render.

Using Components in Routes

Here is a complete example of using SmoothUI components in a Remix / React Router v7 route:

app/routes/home.tsx
import { SiriOrb } from "@/components/smoothui/ui/SiriOrb";
import { MagneticButton } from "@/components/smoothui/ui/MagneticButton";

const Home = () => {
  return (
    <main className="flex min-h-screen flex-col items-center justify-center gap-8 p-8">
      <h1 className="text-4xl font-bold">SmoothUI + Remix</h1>
      <SiriOrb size="200px" />
      <MagneticButton>Get Started</MagneticButton>
    </main>
  );
};

export default Home;

Custom Animated Components

If you create custom animated components alongside SmoothUI, follow the same pattern:

app/components/FadeIn.tsx
"use client";

import { motion, useReducedMotion } from "motion/react";
import type { ReactNode } from "react";

export type FadeInProps = {
  children: ReactNode;
};

const FadeIn = ({ children }: FadeInProps) => {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={
        shouldReduceMotion
          ? { duration: 0 }
          : { type: "spring", duration: 0.25, bounce: 0.1 }
      }
    >
      {children}
    </motion.div>
  );
};

export default FadeIn;

Troubleshooting

Next Steps