Create UI Components

Learn how to build reusable components, style them with CSS Modules, and integrate Tailwind CSS.

Overview

In this guide, you will learn how to create reusable UI components and share them across your application. We will proceed in four simple steps:

  • Create a shared component.
  • Use the component in a page.
  • Style it using a CSS Module.
  • Enhance the styles using Tailwind CSS modifiers.
  • Enhance the styles using Tailwind CSS modifiers.
  • Bonus: Override this component from another module.
  • Advanced: Restyle a component using the theming system.

What is a Module?
In JopiJS, your code is organized into Modules (folders starting with mod_). This keeps features decoupled. In this guide, we'll use a mod_ui module for shared components and mod_home for our pages.


1. Create a Shared Component

As we saw in the Modules guide, components intended to be shared effectively should be placed in the @alias directory.

File Structure

To help you visualize where everything goes, here is the structure we will build:

Project Structure
src/
├── mod_ui/                   # Your UI Module
│   └── @alias/ui/Card/       # The shared component folder
│       ├── index.tsx         # The React logic
│       ├── style.module.css  # The styles
│       └── style.gen.ts      # (Generated) Helper for theming

└── mod_home/                 # Another module
    └── @routes/
        └── page.tsx          # A page using the component

Let's create the Card component in mod_ui.

File: src/mod_ui/@alias/ui/Card/index.tsx

src/mod_ui/@alias/ui/Card/index.tsx
import { ReactNode } from "react";

interface CardProps {
    title: string;
    children: ReactNode;
}

export default function Card({ title, children }: CardProps) {
    return (
        <div className="card">
            <h3>{title}</h3>
            <div>{children}</div>
        </div>
    );
}

2. Use it in a Page

Now that our component is defined in an @alias folder, it is automatically available via the @ shortcut.

Let's use it in a page, for example in mod_home.

File: src/mod_home/@routes/page.tsx

src/mod_home/@routes/page.tsx
// Import using the '@' alias
import Card from "@/ui/Card";

export default function HomePage() {
    return (
        <div className="p-10 bg-slate-50 min-h-screen">
            <h1 className="text-3xl font-bold mb-6">My Dashboard</h1>
            
            <Card title="Welcome Users">
                <p>This is a reusable card component.</p>
                <button className="mt-4 px-4 py-2 bg-blue-500 text-white rounded">
                    Click me
                </button>
            </Card>
        </div>
    );
}

3. Add a CSS Module

Currently, our card style are inline, mixed with the component. Let's add a CSS Module to split the component and his style. Create a file named style.module.css right next to your component.

File: src/mod_ui/@alias/ui/Card/style.module.css

src/mod_ui/@alias/ui/Card/style.module.css
.container {
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 16px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    background-color: white;
}

.title {
    margin-bottom: 12px;
    font-size: 1.25rem;
    font-weight: bold;
    color: #333;
    border-bottom: 2px solid #eee;
    padding-bottom: 8px;
}

Now, we need to wire this CSS to our component. When you create style.module.css, JopiJS automatically generates a wrapper file named style.gen.ts in the same folder.

Important: You must import this generated file instead of the CSS directly to enable the theming system.

File: src/mod_ui/@alias/ui/Card/index.tsx

src/mod_ui/@alias/ui/Card/index.tsx
import { ReactNode } from "react";
// 1. Import the generated wrapper (not the css directly!)
import styles from "./style.gen.ts";
// Don't do this:
// import styles from "./style.module.css";

// 2. Import the hook
import { useCssModule } from "jopijs/ui";

interface CardProps {
    title: string;
    children: ReactNode;
}

export default function Card({ title, children }: CardProps) {
    // 3. Inject styles
    useCssModule(styles);

    return (
        // 4. Use the classes from the 'styles' object
        <div className={styles.container}>
            <h3 className={styles.title}>{title}</h3>
            <div>{children}</div>
        </div>
    );
}

4. Use Tailwind with @apply

You can use regular CSS as described above, but JopiJS also supports Tailwind CSS directly inside CSS Modules via the @apply directive. This allows you to combine the scoping benefits of CSS Modules with the speed of Tailwind utilities.

Let's update our CSS file to use Tailwind classes.

File: src/mod_ui/@alias/ui/Card/style.module.css

src/mod_ui/@alias/ui/Card/style.module.css
.container {
    /* Using Tailwind utilities */
    @apply border border-slate-200 rounded-xl p-6 shadow-sm bg-white;
}

.title {
    @apply mb-4 text-xl font-semibold text-slate-800 border-b pb-2;
}

Why use @apply?
It allows you to group utility classes into semantic names (like .card or .btn). This keeps your JSX clean, while still using your Tailwind design tokens (colors, spacing, etc.).


5. Overriding the Component

One of JopiJS's most powerful features is the ability to replace any shared component without touching the original file.

Imagine you have a mod_theme_dark that wants to replace the standard Card with a Dark Mode version.

1. Create the new component

Create the component in your new module at the exact same path structure (relative to the module root).

File: src/mod_theme_dark/@alias/ui/Card/index.tsx

export default function DarkCard({ title, children }) {
    return (
        <div className="bg-slate-900 text-white p-4 border border-slate-700">
            <h3 className="text-xl font-bold text-blue-400">{title}</h3>
            <div>{children}</div>
        </div>
    );
}

2. Add the Priority Marker

To tell JopiJS to use THIS component instead of the original one, add an empty file named high.priority in the same folder.

Structure with Override
src/
├── mod_ui/                  # Original
│   └── @alias/ui/Card/
│       └── index.tsx        # Standard Card

└── mod_theme_dark/          # The Overrider
    └── @alias/ui/Card/
        ├── index.tsx        # Dark Card
        └── high.priority    # <--- The Magic Marker!

Now, anywhere in your app where import Card from "@/ui/Card" is used, JopiJS will serve the DarkCard instead. No import paths need to change!


6. Restyling (Theming)

Sometimes you want to change the look of a component without replacing its logic. This is where the style.gen.ts file becomes powerful.

When JopiJS generates style.gen.ts, it looks for a CSS override in a special @alias/styles folder. If found, it points to that one. If not, it falls back to your local style.module.css.

How to override styles?

To restyle the ui/Card component from another module (e.g. mod_theme), simplify create a CSS module at the matching path under @alias/styles.

Path Mapping:

  • Component: @alias/ui/Card
  • Style Override: @alias/styles/ui/Card/style.module.css

Example Structure:

src/
├── mod_ui/                  # Original Module
│   └── @alias/ui/Card/
│       ├── index.tsx        # Imports "./style.gen"
│       └── style.module.css # Default styles (Blue)
│       └── style.gen.ts     # (Generated) Helper for theming

└── mod_theme/               # Your Theme Module
    └── @alias/styles/ui/Card/
        └── style.module.css # New styles (Red)

Just by creating this file in mod_theme, JopiJS updates style.gen.ts behind the scenes. The Card component will instantly use your new red styles, with zero code changes in mod_ui.