---
title: "Extract Card Component"
description: "Create Card component in packages/ui, add it to exports, update features page to import from shared package, and experience instant workspace updates."
canonical_url: "https://vercel.com/academy/production-monorepos/extract-card"
md_url: "https://vercel.com/academy/production-monorepos/extract-card.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-04-11T10:56:55.052Z"
content_type: "lesson"
course: "production-monorepos"
course_title: "Production Monorepos with Turborepo"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Extract Card Component

# Extract card component

You have 6 identical card divs in the features page. Each one has the same structure:

```tsx
<div style={{ padding: '2rem', border: '1px solid #e5e7eb', borderRadius: '0.5rem' }}>
  <h3>Title</h3>
  <p>Content</p>
</div>
```

This has been copy-pasted 6 times. Change the border color? Update 6 places. Add this card to another page? Copy-paste again.

Extract it once into `packages/ui`, and all instances stay in sync automatically.

## Outcome

Extract the `Card` component to a shared package and experience instant workspace synchronization across all apps.

## Fast track

1. Create `packages/ui/src/card.tsx` component with title and children props
2. Add `Card` to package exports (package.json + index.ts)
3. Add `@geniusgarage/ui` dependency to web app + configure transpilation
4. Replace inline divs with `<Card>` components in features page
5. Test instant updates by changing `Card` styles in dev mode

## Hands-on exercise 2.3

Extract the 6 duplicate card divs from the features page into a reusable Card component in the shared UI package.

**Requirements:**

1. Create `Card` component in `packages/ui/src/card.tsx` with optional title and children props
2. Export `Card` from `packages/ui` (both package.json exports field and index.ts)
3. Add `@geniusgarage/ui` as a workspace dependency in `apps/web/package.json`
4. Replace all 6 inline card divs with `<Card>` components
5. Verify instant hot-reload updates when modifying the `Card` component

**Implementation hints:**

- Use `title?: string` for optional title prop (renders h3 only if provided)
- Use `workspace:*` protocol in package.json to link local package (not npm)
- Next.js 15+ with Turbopack automatically handles local packages - no additional config needed
- Test the monorepo magic: change Card border color and watch all 6 cards update instantly
- Keep the dev server running during changes to see hot module replacement work

**Files to create/modify:**

- `packages/ui/src/card.tsx` (new file)
- `packages/ui/package.json` (add exports field)
- `packages/ui/src/index.ts` (export Card)
- `apps/web/package.json` (add dependency)
- `apps/web/app/features/page.tsx` (use Card component)

## Create the card component

Create `packages/ui/src/card.tsx` with the component code:

```tsx title="packages/ui/src/card.tsx"
export interface CardProps {
  title?: string
  children: React.ReactNode
}

export function Card({ title, children }: CardProps) {
  return (
    <div style={{
      padding: '2rem',
      border: '1px solid #e5e7eb',
      borderRadius: '0.5rem',
    }}>
      {title && (
        <h3 style={{
          fontSize: '1.25rem',
          fontWeight: 'bold',
          marginTop: 0,
          marginBottom: '0.5rem'
        }}>
          {title}
        </h3>
      )}
      <div style={{ color: '#666' }}>
        {children}
      </div>
    </div>
  )
}
```

This extracts the structure you've been copying. The `title` prop is optional - if provided, it renders an h3.

## Add card to package exports

Update `packages/ui/package.json` to export the Card:

```json title="packages/ui/package.json" {6-8}
{
  "name": "@geniusgarage/ui",
  "version": "1.0.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    "./card": "./src/card.tsx"
  },
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0",
    "react-dom": "^18.0.0 || ^19.0.0"
  },
  "devDependencies": {
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "typescript": "^5"
  }
}
```

The `"./card": "./src/card.tsx"` export means apps can import with:

```ts
import { Card } from '@geniusgarage/ui/card'
```

Also export from the index file:

```ts title="packages/ui/src/index.ts"
export { Card } from './card'
```

## Add UI package to web app

The web app needs to declare its dependency on the UI package. Open `apps/web/package.json`:

```json title="apps/web/package.json" {12}
{
  "name": "@geniusgarage/web",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@geniusgarage/ui": "workspace:*",
    "next": "^16.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@types/node": "^22",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "^16.0.0",
    "typescript": "^5"
  }
}
```

The `workspace:*` protocol tells pnpm to link to the local `packages/ui` instead of looking for it on npm.

**What `workspace:*` means:**

- `workspace:` = use local package from this monorepo
- `*` = use whatever version the package declares (don't version-lock)
- Result: instant updates without publishing to npm

\*\*Note: npm workspaces\*\*

If using npm instead of pnpm, use `"*"` instead of `"workspace:*"`:

```json
"dependencies": {
  "@geniusgarage/ui": "*"
}
```

npm workspaces don't support the `workspace:` protocol, but `*` achieves the same local linking behavior.

Install to create the symlink. A symlink (symbolic link) is a file that points to another file or directory, like a shortcut. In this case, it points to the `packages/ui` directory.

```bash
pnpm install
```

You'll see output confirming the workspace link:

```
Packages: +1
+
Progress: resolved 1, reused 0, downloaded 0, added 1, done
```

## Next.js automatically handles local packages

With Next.js 15+ and Turbopack (now the default), local monorepo packages work automatically - no configuration needed! Next.js detects workspace packages and compiles them on the fly.

\*\*Note: No transpilePackages needed\*\*

Previous Next.js versions required `transpilePackages: ['@geniusgarage/ui']` in `next.config.js`. With modern Next.js and Turbopack, this is **no longer necessary** for local packages.

If you're using an older Next.js version (< 15) without Turbopack, you may need to add:

```js title="apps/web/next.config.js"
module.exports = {
  transpilePackages: ['@geniusgarage/ui'],
}
```

## Use card in features page

Now replace the inline cards with the shared Card component. Open `apps/web/app/features/page.tsx`:

```tsx title="apps/web/app/features/page.tsx" {2,22-39}
import Link from 'next/link'
import { Card } from '@geniusgarage/ui/card'

export default function Features() {
  return (
    <main style={{ padding: '4rem 2rem', fontFamily: 'system-ui', maxWidth: '1200px', margin: '0 auto' }}>
      <nav style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4rem' }}>
        <Link href="/" style={{ fontSize: '1.5rem', fontWeight: 'bold', textDecoration: 'none', color: '#000' }}>
          🧠 GeniusGarage
        </Link>
        <div style={{ display: 'flex', gap: '2rem' }}>
          <Link href="/features" style={{ textDecoration: 'none', color: '#000', fontWeight: 'bold' }}>Features</Link>
        </div>
      </nav>

      <h1 style={{ fontSize: '2.5rem', marginBottom: '1rem', textAlign: 'center' }}>Features</h1>
      <p style={{ fontSize: '1.2rem', color: '#666', marginBottom: '3rem', textAlign: 'center' }}>
        Everything you need to manage your code snippets
      </p>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '2rem' }}>
        <Card title="⚡ Fast Search">
          Find your snippets instantly with powerful full-text search and filtering by tags
        </Card>
        <Card title="📁 Organized">
          Keep your code organized with tags, folders, and collections
        </Card>
        <Card title="🔗 Shareable">
          Share snippets with your team or make them public for the community
        </Card>
        <Card title="🎨 Syntax Highlighting">
          Beautiful syntax highlighting for 100+ programming languages
        </Card>
        <Card title="📋 One-Click Copy">
          Copy snippets to your clipboard with a single click
        </Card>
        <Card title="🔐 Private & Secure">
          Your private snippets stay private with enterprise-grade security
        </Card>
      </div>
    </main>
  )
}
```

Look how clean this is! From 6 duplicate `<div>` blocks to 6 `<Card>` components.

## Try it

### 1. Start the dev server

```bash
pnpm dev
```

You should see:

```
 >>> @geniusgarage/web:dev: ready started server on 0.0.0.0:3000
 >>> @geniusgarage/web:dev:   ▲ Next.js 16.0.0
 >>> @geniusgarage/web:dev:   - Local:        http://localhost:3000
 >>> @geniusgarage/web:dev:   ✓ Compiled /features in 247ms
```

### 2. Open the features page

Navigate to **<http://localhost:3000/features>**

You should see:

- 6 cards in a responsive grid
- Each card has an emoji + title (⚡ Fast Search, 📁 Organized, etc.)
- Gray borders around each card

The page looks identical to before, but now uses the shared Card component from `packages/ui`.

## Experience instant workspace updates

### 3. Test the monorepo magic

With the dev server still running, change the Card component's border style:

```tsx title="packages/ui/src/card.tsx" {10}
export interface CardProps {
  title?: string
  children: React.ReactNode
}

export function Card({ title, children }: CardProps) {
  return (
    <div style={{
      padding: '2rem',
      border: '2px solid #3b82f6',  // Changed: Blue border instead of gray
      borderRadius: '0.5rem',
    }}>
      {title && (
        <h3 style={{
          fontSize: '1.25rem',
          fontWeight: 'bold',
          marginTop: 0,
          marginBottom: '0.5rem'
        }}>
          {title}
        </h3>
      )}
      <div style={{ color: '#666' }}>
        {children}
      </div>
    </div>
  )
}
```

**Save the file.** Watch your browser - the dev server hot-reloads and **all 6 cards instantly update** with the new blue border. No manual refresh needed.

**This is workspace dependency magic:** Change the component once in `packages/ui`, and all apps using it update immediately. No npm publishing, no version bumping, no waiting. Just instant synchronization.

### 4. Change it back

Before continuing, revert to the original gray border:

```tsx title="packages/ui/src/card.tsx" {5}
export function Card({ title, children }: CardProps) {
  return (
    <div style={{
      padding: '2rem',
      border: '1px solid #e5e7eb',  // Back to gray
      borderRadius: '0.5rem',
    }}>
      {/* Keep all other code (title rendering, children) the same */}
    </div>
  )
}
```

\*\*Note: Workspace Dependencies Working\*\*

You just experienced the core monorepo benefit:

1. Extract component to shared package
2. Import with `workspace:*`
3. Change component → instant updates everywhere

No publishing, no registry, no waiting. Just instant synchronization.

## What you built

You created your first shared component:

**Before:**

```tsx
// 6 duplicate divs with inline styles
<div style={{ padding: '2rem', border: '1px solid #e5e7eb', ... }}>
  <h3>⚡ Fast Search</h3>
  <p>Find your snippets...</p>
</div>
```

**After:**

```tsx
// 6 clean component calls
<Card title="⚡ Fast Search">
  Find your snippets...
</Card>
```

Change the border color, add animations, update spacing - do it once in `packages/ui/src/card.tsx` and all 6 cards update automatically.

## Troubleshooting

\*\*Warning: Build Error: Cannot find module 'react/jsx-runtime'\*\*

If you get a TypeScript error about `react/jsx-runtime`, make sure your `packages/ui/package.json` includes the `devDependencies` section with React types:

```json
"devDependencies": {
  "@types/react": "^19",
  "@types/react-dom": "^19",
  "typescript": "^5"
}
```

Then run `pnpm install` to install the types.

## Commit

Save your work:

```bash
git add .
git commit -m "feat(ui): extract Card component to shared package"
```

## Done-when

Verify your implementation:

- [ ] Card component exists at `packages/ui/src/card.tsx` with title and children props
- [ ] Card is exported from `packages/ui/package.json` exports field
- [ ] Card is exported from `packages/ui/src/index.ts`
- [ ] Web app has `@geniusgarage/ui: "workspace:*"` dependency
- [ ] Features page displays 6 cards using `<Card>` components
- [ ] Changing Card border color updates all 6 cards instantly in dev mode
- [ ] Page loads at <http://localhost:3000/features> with responsive grid

## What's Next

Next lesson: **Add More Components with v0** - You'll use Vercel's v0 to generate a pricing card component, extract Button from the home page, add a lint task to the UI package, and experience Turborepo orchestrating tasks across packages with caching.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
