Loading...

Quickstart

Run the Tambo starter template and build your first generative UI feature.

Run the starter template to see Tambo in action. You'll register components, add tools, and send messages.

Tambo starter template demo

Set up the starter app

1. Create the app

Create your tambo project
npm create tambo-app@latest my-tambo-app

This command will:

  • Clone the starter template
  • Install dependencies
  • Initialize git repository
  • Walk you through creating a project, generate an API key, and write it to your env file

The CLI prefers .env.local, but will update .env if that's the only env file present.

To skip automatic setup, use --skip-git-init or --skip-tambo-init flags.

2. Run the app

Run the app
npm run dev

Open localhost:3000 and start chatting.

Customize the template

Try making a change to see how registration works.

In /src/lib/tambo.ts you'll see how components and tools are registered. In /src/app/chat/page.tsx you'll see how they're passed to TamboProvider.

Add a component

Create src/components/recipe-card.tsx:

recipe-card.tsx
"use client";

import { ChefHat, Clock, Minus, Plus, Users } from "lucide-react";
import { useState } from "react";

interface Ingredient {
  name: string;
  amount: number;
  unit: string;
}

interface RecipeCardProps {
  title?: string;
  description?: string;
  ingredients?: Ingredient[];
  prepTime?: number;
  cookTime?: number;
  originalServings?: number;
}

export default function RecipeCard({
  title,
  description,
  ingredients,
  prepTime = 0,
  cookTime = 0,
  originalServings,
}: RecipeCardProps) {
  const [servings, setServings] = useState(originalServings || 1);

  const scaleFactor = servings / (originalServings || 1);

  const handleServingsChange = (newServings: number) => {
    if (newServings > 0) {
      setServings(newServings);
    }
  };

  const formatTime = (minutes: number) => {
    if (minutes < 60) {
      return `${minutes}m`;
    }
    const hours = Math.floor(minutes / 60);
    const remainingMinutes = minutes % 60;
    return remainingMinutes > 0
      ? `${hours}h ${remainingMinutes}m`
      : `${hours}h`;
  };

  const totalTime = prepTime + cookTime;

  return (
    <div className="bg-white max-w-md rounded-xl shadow-lg overflow-hidden border border-gray-200">
      <div className="p-6">
        <div className="mb-4">
          <h2 className="text-2xl font-bold text-gray-900 mb-2">{title}</h2>
          <p className="text-gray-600 text-sm leading-relaxed">{description}</p>
        </div>

        <div className="flex items-center gap-6 mb-6 text-sm text-gray-600">
          <div className="flex items-center gap-2">
            <Clock className="w-4 h-4" />
            <span>{formatTime(totalTime)}</span>
          </div>
          <div className="flex items-center gap-2">
            <Users className="w-4 h-4" />
            <span>{servings} servings</span>
          </div>
          {prepTime > 0 && (
            <div className="flex items-center gap-2">
              <ChefHat className="w-4 h-4" />
              <span>Prep: {formatTime(prepTime)}</span>
            </div>
          )}
        </div>

        <div className="mb-6 p-4 bg-gray-50 rounded-lg">
          <div className="flex items-center justify-between">
            <span className="font-medium text-gray-700">Adjust Servings:</span>
            <div className="flex items-center gap-3">
              <button
                onClick={() => handleServingsChange(servings - 1)}
                className="p-2 rounded-full bg-white border border-gray-300 hover:bg-gray-50 transition-colors"
                disabled={servings <= 1}
              >
                <Minus className="w-4 h-4" />
              </button>
              <span className="font-semibold text-lg min-w-[3rem] text-center">
                {servings}
              </span>
              <button
                onClick={() => handleServingsChange(servings + 1)}
                className="p-2 rounded-full bg-white border border-gray-300 hover:bg-gray-50 transition-colors"
              >
                <Plus className="w-4 h-4" />
              </button>
            </div>
          </div>
        </div>

        <div className="mb-6">
          <h3 className="text-lg font-semibold text-gray-900 mb-3">
            Ingredients
          </h3>
          <ul className="space-y-2">
            {ingredients?.map((ingredient, index) => (
              <li
                key={index}
                className="flex items-center gap-3 p-2 rounded-md hover:bg-gray-50 transition-colors"
              >
                <div className="w-2 h-2 bg-orange-500 rounded-full flex-shrink-0" />
                <span className="font-medium text-gray-900">
                  {(ingredient.amount * scaleFactor).toFixed(
                    (ingredient.amount * scaleFactor) % 1 === 0 ? 0 : 1,
                  )}
                </span>
                <span className="text-gray-600">{ingredient.unit}</span>
                <span className="text-gray-800">{ingredient.name}</span>
              </li>
            ))}
          </ul>
        </div>
      </div>
    </div>
  );
}

Register it in src/lib/tambo.ts:

tambo.ts
  {
    name: "RecipeCard",
    description: "A component that renders a recipe card",
    component: RecipeCard,
    propsSchema: z.object({
      title: z.string().describe("The title of the recipe"),
      description: z.string().describe("The description of the recipe"),
      prepTime: z.number().describe("The prep time of the recipe in minutes"),
      cookTime: z.number().describe("The cook time of the recipe in minutes"),
      originalServings: z
        .number()
        .describe("The original servings of the recipe"),
      ingredients: z
        .array(
          z.object({
            name: z.string().describe("The name of the ingredient"),
            amount: z.number().describe("The amount of the ingredient"),
            unit: z.string().describe("The unit of the ingredient"),
          })
        )
        .describe("The ingredients of the recipe"),
    }),
  },

Refresh the page and send "Show me a recipe". Tambo generates and streams your RecipeCard.

RecipeCard rendered by Tambo

Add a tool

The RecipeCard above generates recipe data from scratch. Add a tool so Tambo can check what ingredients are available.

In src/lib/tambo.ts, add to the tools array:

tambo.ts
  {
    name: "get-available-ingredients",
    description:
      "Get a list of all the available ingredients that can be used in a recipe.",
    tool: () => [
      "pizza dough",
      "mozzarella cheese",
      "tomatoes",
      "basil",
      "olive oil",
      "chicken breast",
      "ground beef",
      "onions",
      "garlic",
      "bell peppers",
      "mushrooms",
      "pasta",
      "rice",
      "eggs",
      "bread",
    ],
    inputSchema: z.object({}),
    outputSchema: z.array(z.string()),
  },

Refresh and send "Show me a recipe I can make". Tambo calls the tool, gets the ingredients, and generates a recipe using them.

Tambo using a tool to get ingredients

Next steps

Add Tambo to an existing app
Integrate Tambo into your current React project
Component registration
Learn how components and tools work together
MCP integration
Connect external systems with Model Context Protocol