Back to blog
Next.jsTypeScriptCalendlyWeb Development

How to Integrate Calendly in a Next.js App (App Router + TypeScript)

A complete guide to embedding Calendly in Next.js — configure your event, build a typed CalendlyEmbed component, use environment variables, and add a Book a 15-min call section to your site.

FK

Faisal Khawaj

Author

9 min read

If you run a portfolio, agency site, or freelance landing page, you want leads to book a call without leaving your website. Calendly handles scheduling; Next.js handles everything else. This guide walks through the full integration using the App Router, TypeScript, and an inline embed — the same pattern used on this site.

What you'll build

By the end of this guide you'll have:

  • A Calendly event configured for intro calls (15 or 30 minutes)
  • A reusable CalendlyEmbed client component
  • Your event URL stored in an environment variable
  • A contact section with Book a 15-min call that scrolls to the embed
  • Typed helpers so Calendly URLs aren't hard-coded across the app

Prerequisites

  • A Calendly account (free tier works)
  • A Next.js project (App Router) with TypeScript
  • Basic familiarity with React client components and .env files

This guide targets Next.js 14+ App Router. If you use the Pages Router, the component code is the same — only the file paths differ (pages/ vs app/).


Step 1 — Configure your Calendly event

When you create a Calendly account, you get a default 30 Minute Meeting event. For portfolio and freelance sites, a 15-minute intro call converts better — it's a low-commitment first step.

Event types dashboard

Open Scheduling → Event types. You'll see your profile and any existing events:

Calendly event types dashboard showing the default 30 Minute Meeting event
Calendly event types dashboard showing the default 30 Minute Meeting event

Change the duration

  1. Click the menu on your event card
  2. Select Edit
  3. Open the Duration dropdown
  4. Choose 15 min (or keep 30 min if you prefer)

Editing Calendly event duration — selecting 15 min from the dropdown
Editing Calendly event duration — selecting 15 min from the dropdown

Click Save changes.

Other settings worth configuring

SettingRecommendation
LocationGoogle Meet, Zoom, or phone — don't leave "No location set"
AvailabilityMatch your real timezone and working hours
Event name15-min intro call or Project discovery call
DescriptionWhat the call covers and what to prepare

Get your event link

From the event card menu you can:

  • Copy link — the URL you'll embed
  • Add to website — Calendly's embed snippet (we'll build a better Next.js version)
  • View booking page — preview what clients see

Calendly event menu with Copy link, Edit, and Add to website options
Calendly event menu with Copy link, Edit, and Add to website options

Your link will look like:

Code
https://calendly.com/your-username/30min

Rename the event slug in Calendly settings if you want something cleaner like /intro-call.


Step 2 — Store the URL in an environment variable

Never hard-code your Calendly URL in components. Use a public env var so you can change it per environment (local, preview, production).

.env.local

Terminal
NEXT_PUBLIC_CALENDLY_URL=https://calendly.com/your-username/intro-call

src/lib/links.ts

TypeScript
/**
 * 15-minute intro call — set NEXT_PUBLIC_CALENDLY_URL in .env
 */
export const CALENDLY_URL: string =
  process.env.NEXT_PUBLIC_CALENDLY_URL ?? "";

Add the same variable in Vercel → Settings → Environment Variables and redeploy.

Only variables prefixed with NEXT_PUBLIC_ are available in client components. Calendly's embed runs in the browser, so this prefix is required.


Step 3 — Create a typed Calendly embed component

Calendly's inline embed needs two things:

  1. A div with class calendly-inline-widget and a data-url attribute
  2. Calendly's external widget script

In Next.js, load the script with next/script instead of manually appending to <head> in useEffect. That plays nicely with React, avoids duplicate script tags, and supports lazy loading.

src/components/CalendlyEmbed.tsx

TypeScript
"use client";

import Script from "next/script";

type CalendlyEmbedProps = {
  /** Full Calendly event URL, e.g. https://calendly.com/you/intro-call */
  url: string;
  /** Widget height in pixels */
  height?: number;
  /** Brand accent color without # — passed to Calendly */
  primaryColor?: string;
};

export default function CalendlyEmbed({
  url,
  height = 700,
  primaryColor = "0069ff",
}: CalendlyEmbedProps) {
  if (!url) return null;

  const embedUrl = new URL(url);
  embedUrl.searchParams.set("hide_gdpr_banner", "1");
  embedUrl.searchParams.set("primary_color", primaryColor.replace("#", ""));

  return (
    <>
      <div
        className="calendly-inline-widget w-full min-w-0"
        data-url={embedUrl.toString()}
        style={{ minWidth: "280px", height: `${height}px` }}
      />
      <Script
        src="https://assets.calendly.com/assets/external/widget.js"
        strategy="lazyOnload"
      />
    </>
  );
}

Why "use client"?

The Calendly widget attaches to the DOM at runtime. This component must be a client component. Parent pages and layouts can remain server components.

Script loading strategies

StrategyWhen to use
lazyOnloadEmbed below the fold (contact section) — recommended
afterInteractiveEmbed near the top of the page
beforeInteractiveRare — only if scheduling is the primary page purpose

Step 4 — Wire it to your env config

Create a thin wrapper so pages don't repeat env logic:

src/components/CalendlySection.tsx

TypeScript
import CalendlyEmbed from "@/components/CalendlyEmbed";
import { CALENDLY_URL } from "@/lib/links";

export default function CalendlySection() {
  if (!CALENDLY_URL) return null;

  return (
    <section id="calendly" className="scroll-mt-28">
      <h2 className="text-2xl font-semibold">Book a 15-min intro call</h2>
      <p className="mt-2 text-muted-foreground">
        Pick a time that works for you. We&apos;ll discuss your project, timeline,
        and fit — no pressure.
      </p>
      <div className="mt-6">
        <CalendlyEmbed url={CALENDLY_URL} primaryColor="efff00" />
      </div>
    </section>
  );
}

Use this in your contact page or homepage:

app/contact/page.tsx (or inside an existing contact section)

TypeScript
import CalendlySection from "@/components/CalendlySection";

export default function ContactPage() {
  return (
    <main>
      {/* email, WhatsApp, etc. */}
      <CalendlySection />
    </main>
  );
}

Step 5 — Add a CTA button that scrolls to the embed

Link buttons to #calendly so users jump straight to the scheduler:

TypeScript
import { CALENDLY_URL } from "@/lib/links";

export function BookCallButton() {
  if (!CALENDLY_URL) {
    return (
      <a href="mailto:you@example.com">Start a project</a>
    );
  }

  return (
    <a href="#calendly">Book a 15-min call</a>
  );
}

Place this in your hero, navbar, or contact section. If CALENDLY_URL is missing, fall back to email so the button never leads nowhere.


Step 6 — Optional: popup widget instead of inline

For a floating "Schedule" button, use Calendly's popup API:

src/lib/calendly.ts

TypeScript
declare global {
  interface Window {
    Calendly?: {
      initPopupWidget: (options: { url: string }) => void;
    };
  }
}

export function openCalendlyPopup(url: string): void {
  if (typeof window === "undefined") return;
  window.Calendly?.initPopupWidget({ url });
}

Usage in a client component (after the widget script has loaded):

TypeScript
"use client";

import { CALENDLY_URL } from "@/lib/links";
import { openCalendlyPopup } from "@/lib/calendly";

export function SchedulePopupButton() {
  if (!CALENDLY_URL) return null;

  return (
    <button type="button" onClick={() => openCalendlyPopup(CALENDLY_URL)}>
      Schedule a call
    </button>
  );
}

Load the same widget.js script once via next/script in your layout or embed component.

ApproachBest for
Inline embedContact page, dedicated booking section
Popup widgetFloating CTA, minimal pages
Link to CalendlySimplest — but users leave your site

Inline embed keeps visitors on your domain and looks more professional.


Step 7 — Style the embed to match your site

Calendly's iframe has limited styling, but you can:

  1. Wrap it in a bordered container matching your design system
  2. Pass primary_color in the query string (hex without #)
  3. Set a fixed height — 650–750px works well for inline widgets
  4. Use scroll-mt-28 on the section so anchor links don't hide the title behind a sticky navbar
TypeScript
<div className="overflow-hidden rounded-lg border border-gray-800">
  <CalendlyEmbed url={CALENDLY_URL} height={700} primaryColor="efff00" />
</div>

App Router file structure

Code
src/
├── app/
│   ├── layout.tsx
│   └── (site)/
│       ├── page.tsx              # homepage with #calendly CTA
│       └── contact/page.tsx      # optional dedicated page
├── components/
│   ├── CalendlyEmbed.tsx         # client — widget + script
│   └── CalendlySection.tsx       # server — section wrapper
└── lib/
    ├── links.ts                  # CALENDLY_URL from env
    └── calendly.ts               # optional popup helper

Troubleshooting

ProblemFix
Widget shows blank boxConfirm NEXT_PUBLIC_CALENDLY_URL is set and you redeployed after adding it
Script loads but no calendarCheck the URL in a browser — event must be active, not secret
Widget loads on every page visitUse strategy="lazyOnload" — don't import the embed in root layout unless needed globally
Duplicate scrollbarsSet explicit height on the widget div
GDPR banner inside widgetAdd hide_gdpr_banner=1 to the URL (shown in component above)
TypeScript errors on window.CalendlyUse the declare global block in calendly.ts
Works locally, not on VercelAdd env var in Vercel dashboard for Production and Preview

SEO and performance notes

  • The Calendly iframe loads third-party JavaScript — keep it on contact/booking pages only, not every route
  • Use lazyOnload so it doesn't block LCP on your homepage
  • Your booking page should still have proper <title> and meta description via Next.js metadata export
  • Calendly pages on calendly.com are indexed separately — your embed keeps users on your domain

Checklist

  • Calendly event created and duration set (15 or 30 min)
  • Location and availability configured
  • Event link copied
  • NEXT_PUBLIC_CALENDLY_URL in .env.local and Vercel
  • CalendlyEmbed.tsx client component created
  • Contact section with #calendly anchor
  • CTA buttons on hero / nav
  • Tested on mobile (Calendly is responsive inside the iframe)

Wrapping up

Integrating Calendly into Next.js comes down to three pieces: a configured event, an environment variable, and a client component that loads Calendly's script with next/script. Skip the useEffect + manual <script> injection pattern — next/script is cleaner, typed, and avoids hydration edge cases.

Once live, pair your scheduler with email and WhatsApp so international clients can reach you however they prefer.


Need help shipping a Next.js site with booking, contact flows, and production polish? Book a call or get in touch.

Published Jun 30, 2026 · 9 min read