Freeframe

Documentation

Orbit

v1.0.2Updated Oct 17, 2026A bright, friendly SaaS website for a shift-scheduling app, with a live drag-and-drop schedule demo in the hero.

Overview#

Orbit is a product-led marketing website for Orbit, a fictional shift-scheduling app made by Orbit Works (Manchester and Lisbon) for restaurants, cafés, hotels and shops. The home page opens on a live week schedule that visitors can click, drag and auto-fill while coverage updates, followed by a product tour, customer metrics and an integrations orbit. The site also has feature deep-dives, pricing with a price calculator and a comparison table, a searchable integrations directory with a detail drawer, filterable customer stories, a changelog, an about and careers page, a sales form that routes by team size and a three-step free trial sign-up. This is the Next.js version (Next.js 16 App Router, React 19, TypeScript, Tailwind CSS v4), version 1.0.0, ported page for page from the HTML template. The design is bright and friendly: warm paper surfaces, a deep indigo ink, a coral brand colour, pastel panels and illustrated SVG avatars. It exports to static files. Live demo: https://freeframe.dev/templates/orbit

The site is a front end only. The schedule demo runs in the browser, and the forms don't send anything until you connect your own services (see Forms).

Quick start#

Requirements: Node.js 24 or newer and pnpm (npm works too).

sh
pnpm install
pnpm dev          # http://localhost:3000

Other scripts:

Script What it does
pnpm build Static export to out/
pnpm start Serve out/ (npx serve out)
pnpm lint ESLint
pnpm typecheck next typegen and TypeScript 7 tsc --noEmit
pnpm build:demo Build for freeframe.dev under /demos/nextjs/orbit into demo/

Pages#

Route File Purpose and features
/ src/app/page.tsx Hero with the interactive schedule demo (#schedule-demo), customer wordmarks, before/after panels, feature bento, product tour tabs (#tour), testimonials, integrations orbit, count-up figures and a call to action
/features src/app/features/page.tsx Schedule builder (#builder), availability (#availability), swaps (#swaps), labor cost (#labor), time clock (#time), announcements (#messages) and an Orbit vs spreadsheets table (#compare)
/pricing src/app/pricing/page.tsx Monthly / annual switch, four plans, price calculator (#calculator), sticky comparison table (#compare), FAQ (#faq)
/integrations src/app/integrations/page.tsx Search and category filter (?q=, ?category=), 20 integration cards, a detail drawer (?integration=paywell), an API call to action
/customers src/app/customers/page.tsx Headline figures, a featured story with an SVG chart, nine stories with an industry filter (?industry=hotels)
/changelog src/app/changelog/page.tsx Updates on a timeline, topic filter (?tag=mobile), email subscription form
/about src/app/about/page.tsx Story, figures, values, team, open roles (#careers), offices
/contact src/app/contact/page.tsx "Who you'll talk to" card and the sales form (?size=500, ?topic=careers)
/signup src/app/signup/page.tsx Three-step free trial sign-up (?plan=starter, team or business)
404 src/app/not-found.tsx Orbit illustration and links back to the site (exported as 404.html, noindex)

Project structure#

src/
  app/
    layout.tsx              <html>, skip link, header, <main>, footer, live region, toast, reveals, count-ups, back to top
    globals.css             Tailwind import, @theme tokens, component classes (buttons, cards, schedule, drawer...)
    fonts.ts                Gabarito and Rethink Sans with next/font/local
    page.tsx                home page; one folder per other route with its page.tsx
    not-found.tsx           404 page
    icon.svg                favicon (the Orbit mark)
    opengraph-image.png     social sharing image (1200×630)
  components/
    site-header.tsx         sticky header, current page, mobile menu (client)
    site-footer.tsx         footer columns and the monthly updates form; nav-link.tsx marks the current page
    schedule-demo.tsx       hero schedule demo (client)
    tour-tabs.tsx           product tour tabs (client)
    billing.tsx             monthly / annual switch and the prices that follow it (client)
    price-calculator.tsx    locations + employees estimate (client)
    compare-table.tsx       plan comparison table
    accordion.tsx           FAQ accordion (client)
    filters.tsx             filters, search, counts and empty states for lists (client)
    integration-drawer.tsx  integration details in a <dialog> (client)
    contact-sales.tsx       sales form and the "Who you'll talk to" card (client)
    signup-wizard.tsx       three-step sign-up and the plan label (client)
    updates-form.tsx        changelog subscription; newsletter-form.tsx is the footer form
    use-validation.tsx      inline validation hook shared by every form
    success-box.tsx         the message that replaces a sent form
    logos.tsx               Orbit mark, integration logos and customer wordmarks
    announcer.tsx, toast.tsx, reveal-observer.tsx, count-up.tsx, back-to-top.tsx, check-icon.tsx, year.tsx
  data/
    nav.ts                  header, mobile and footer links
    schedule.tsx            schedule demo rules and team
    pricing.ts              prices, calculator settings, comparison table, FAQ
    integrations.tsx        integrations, categories and badges
    contact.tsx             team sizes, sales routing, form options and portraits
    signup.ts               plan labels, trial length and sign-up options
  lib/
    validation.ts           form rules (required, email, work email, phone, count, password, email list, groups)
    url.ts                  URL parameter helpers and reduced-motion check
    announce.ts             screen-reader announcements and toasts
    format.ts, metadata.ts, asset.ts
  fonts/                    woff2 files and their OFL licenses
public/img/
  og-image.svg              source of the social image

Customising#

Brand and colours#

The design tokens live in the @theme block of src/app/globals.css. Every utility such as bg-cream, text-ink-soft or bg-coral-500 comes from them.

Token Default Used for
--color-paper #fdfaf5 Page background
--color-cream, --color-sand, --color-line #f7f1e7, #efe6d8, #e7dece Alternate sections and inputs, hovers and tracks, hairlines
--color-ink #1f1d3d Headings, body text, primary buttons
--color-ink-soft, --color-ink-faint #4a4766, #64617e Secondary text, captions (both AA on every surface)
--color-coral-100 … --color-coral-700 coral scale Brand: 500 for fills, 700 for text and links
--color-peach, -mint, -lilac, -butter, -sky, -blush pastels Panels, shift blocks and charts, each with an -ink partner for text on top

To change the brand colour, edit the coral scale (keep 700 dark enough for text on white):

css
@theme {
  --color-coral-300: #93c5fd;
  --color-coral-500: #3b82f6;
  --color-coral-700: #1d4ed8;
}

A few colours are written into SVG art and the schedule demo: the coverage ring (#1fa16a when everything is covered, #ff6b4a otherwise, in src/components/schedule-demo.tsx), the Orbit mark in src/components/logos.tsx, the avatars and the charts. Update those too. Orbit is a light-only design (color-scheme: light); there is no dark theme.

Typography#

Two fonts are self-hosted with next/font/local in src/app/fonts.ts, from the Latin woff2 files Google Fonts serves (src/fonts/, the same files the HTML version loads): Rethink Sans (text, variable weight 400–700 plus italic) and Gabarito (headings and display, variable weight 500–800). Each keeps its real family name, so --font-sans and --font-display in @theme and the font-family attributes of the SVG wordmarks can name them directly. Headings get Gabarito from h1, h2, h3, h4 { font-family: var(--font-display) } in globals.css. To swap a font, put its woff2 file in src/fonts/, point the localFont() call at it, change the font-family declaration and update the token.

Content#

Page copy is in each route's page.tsx. The content behind the interactive parts lives in src/data/:

File Export What it holds
schedule.tsx DEMO Location and week labels, shift length, the 40-hour limit, people needed per shift and day, labels, times and spoken names
TEAM The people in the demo, the week they start with and their portraits
pricing.ts PRICING Team and Business prices per location and per employee (monthly and annual), free limits and the sales threshold
DEFAULT_BILLING, BILLING_NOTES Annual by default, and the note under the switch
CALCULATOR Slider ranges, starting values and the plan choices
COMPARE, FAQ Comparison table groups and rows, FAQ questions and answers
integrations.tsx INTEGRATIONS Every integration: name, category, badge, summary, drawer text, what syncs, setup time, plans and logo
CATEGORIES, BADGE_CLASSES Filter categories and badge colours
contact.tsx CONTACT_ROUTES, DEFAULT_ROUTE Who the visitor talks to for each team size
SIZES, TOPICS, INDUSTRIES, INTERESTS, TIMELINES, AVATARS Sales form options and portraits
signup.ts PLAN_LABELS, DEFAULT_PLAN, TRIAL_DAYS Plan names for ?plan=, the default plan and the trial length
STEPS, BUSINESS_TYPES, LOCATION_OPTIONS, TEAM_OPTIONS, CONNECT_LATER Sign-up steps and options
nav.ts NAV, MOBILE_NAV, FOOTER_NAV Header menu, mobile menu and footer columns

To add an integration, add an object to INTEGRATIONS; the card, the drawer, the counts and the search pick it up. category must be a key of CATEGORIES, and logo is the inside of a 40×40 SVG:

tsx
{
  slug: "rotacloud",
  name: "Rotacloud",
  category: "hr",
  badge: "New",
  summary: "Bring holiday balances into Orbit.",
  detail: "Rotacloud shares holiday balances so managers see them next to time-off requests.",
  syncs: ["Holiday balances", "Approved leave"],
  setup: "5 minutes",
  plans: "Team and up",
  logo: <rect width="40" height="40" rx="11" fill="#0f766e" />,
},

To change the demo week, edit shifts in TEAM ("am", "pm", "off" for approved time off or ""), and needs in DEMO for how many people each shift needs per day (Monday first).

Customer stories and changelog entries are written in their pages. Each story or update is a <FilterItem> whose tags come from the STORIES or UPDATES array at the top of the page, in the same order; add an entry there when you add an item.

Images and artwork#

There are no photos. Everything is SVG or CSS:

  • Logo — OrbitMark in src/components/logos.tsx (header and footer); the same mark is src/app/icon.svg, which Next.js serves as the favicon.
  • Integration logos — IntegrationLogo draws the logo of an entry in src/data/integrations.tsx.
  • Customer wordmarks — Wordmark in src/components/logos.tsx (170×40, drawn in currentColor). Each has role="img" and the name as aria-label; keep those when you replace them.
  • Avatars — inline SVG portraits (80×80) in the pages, TEAM and AVATARS. To use photos, replace each <svg> with an <img> of the same size class, square and at least 160×160 px, with a real alt (or alt="" where a name is already next to it).
  • Product mock-ups — the schedule, tour panels, phone screens and charts are JSX and inline SVG, marked aria-hidden="true" where they are decorative.
  • Background — the pastel blobs (.bg-mesh) and dotted rings (.orbit-ring) are classes in globals.css.
  • Social image — src/app/opengraph-image.png (1200×630). Edit public/img/og-image.svg and export it to PNG at 1200×630 to change it; most social sites don't display SVG.

The header (src/components/site-header.tsx) and footer (src/components/site-footer.tsx) render on every page from src/app/layout.tsx. Menu items come from NAV, MOBILE_NAV and FOOTER_NAV in src/data/nav.ts. To add a page, create src/app/<route>/page.tsx with its own metadata, then add it to those lists. The current page gets aria-current="page", which also styles it. The header pill gains a shadow after scrolling (class is-scrolled).

Features#

Schedule demo#

The home page hero (#schedule-demo, src/components/schedule-demo.tsx) is a 5×7 grid of buttons, one per person and day.

  • Mouse: click a cell to cycle empty → AM → PM → empty. Drag an AM or PM shift onto another cell to move it, or to swap with the shift already there (a drag starts after 6 px; time off can't be a drop target).
  • Keyboard: the grid is one tab stop. Arrow keys move between cells (no wrapping), Home and End jump to Monday and Sunday, Enter or Space cycles, A and P set a morning or evening shift, Delete or Backspace clears.
  • Time off ("off") is locked: clicking it announces "Priya has approved time off on Sunday."
  • Live totals: the coverage ring and percentage, the "Needs" row (−1, −2 or a tick per day), hours per person (flagged "over" above DEMO.maxHours) and the open-shifts note update on every change.
  • Auto-fill gives each open shift to the free person with the fewest hours, never over 40 hours; from the starting week it reaches 100% with 40/40/40/40/24 hours. Reset returns to TEAM; Publish shows a toast.
  • Accessibility: each cell's aria-label says who, which day and what shift; changes are announced in the polite live region; the table has a caption explaining the keys. With reduced motion the pop and shake animations are skipped.

Product tour#

TourTabs (src/components/tour-tabs.tsx) renders the steps from the TOUR array in src/app/page.tsx: a vertical role="tablist" (two columns on phones) with aria-selected, roving tabIndex and one role="tabpanel" per step. Arrow keys move and wrap, Home and End jump. Add a step by adding an object with key, title, text, panelClass and the panel JSX.

Pricing and calculator#

src/app/pricing/page.tsx wraps the page in BillingProvider (src/components/billing.tsx):

  • Billing switch BillingSwitch (#billing, role="radiogroup" with two role="radio" buttons, arrow keys switch). Annual is the default (DEFAULT_BILLING). PlanPrice and BillingNote follow it.
  • Calculator (src/components/price-calculator.tsx, #calc): locations (1–30), employees with shifts (5–600) and the Team or Business plan. The estimate is locations × location price + employees × employee price for the current billing period; with the defaults that's $177 a month annually (saving $540 a year) or $222 monthly. One location with up to 15 people suggests the free Starter plan; 500 or more people suggests talking to sales (/contact?size=500). The call to action follows the plan (/signup?plan=team).
  • Comparison table (src/components/compare-table.tsx) reads COMPARE; its header row sticks below the site header while you scroll the table.
  • FAQ — Accordion (src/components/accordion.tsx) renders FAQ. The first answer starts open, one is open at a time, buttons use aria-expanded and aria-controls. Without JavaScript every answer shows.

The sliders announce readable values (aria-valuetext) and the new estimate is read out 0.6 s after you stop changing them.

src/components/filters.tsx powers the integrations directory, the customer stories and the changelog. FilterProvider gets every item's tags (and search text), the URL parameter (category, industry or tag) and the noun for "Showing 9 stories"; FilterButton shows a count (aria-pressed), FilterItem hides items that don't match, FilterStatus is a polite live region, FilterEmpty shows when nothing matches, FilterSearch narrows by words (every word must match; kept in ?q=) and FilterReset clears both and puts focus back in the search box. The choice is written to the URL with history.replaceState, so filtered views can be shared; unknown values show everything.

Integration drawer#

IntegrationDrawer (src/components/integration-drawer.tsx) shows an integration's details in a native <dialog> that slides in from the right (.drawer in globals.css). Each card's name is an IntegrationOpenButton that covers the card. ?integration=paywell opens it on load. Escape, the close button or a click on the dimmed backdrop close it, the URL parameter is removed and focus returns to the card.

Sales routing#

On /contact, the team size in the form (src/components/contact-sales.tsx) picks the person in the "Who you'll talk to" card from CONTACT_ROUTES (Fergus for 1–24 with a free-trial link, Amara for 25–499, Siobhan for 500+). For 100+ people a required "When would you like to roll out?" question appears. ?size= takes a head count (?size=500) or a key (small, mid, large, enterprise); ?topic= preselects the topic.

Sign-up#

SignupWizard (src/components/signup-wizard.tsx) has three steps with a progress stepper (aria-current="step", ticks on finished steps). Each step is validated before the next and focus moves to the new step's heading. Step 1 has a show/hide password button (aria-pressed) and a four-part strength meter; step 3 takes optional teammate emails (every address is checked). The welcome message names the plan, the business and the trial end date (TRIAL_DAYS from today). ?plan=starter, team or business changes the plan label (PlanLabel) and the hidden plan field.

Header, menu, reveals and back to top#

The mobile menu (#menu-toggle, #mobile-menu) uses aria-expanded, closes on Escape (returning focus to the button), on navigation and when the window grows to desktop width. Elements with .reveal fade up as they enter the viewport (reveal-d1 to reveal-d3 add a delay); src/components/reveal-observer.tsx also reveals everything above. Numbers with data-count count up once (src/components/count-up.tsx). Nothing animates with reduced motion or without JavaScript. A back-to-top button (src/components/back-to-top.tsx) appears after one screen of scrolling and lifts itself above the toast; remove it from layout.tsx and delete the .back-to-top styles to drop it.

Forms#

All forms use useValidation() from src/components/use-validation.tsx, which reads the rules from each field's attributes (required, type, data-validate, minLength, max, data-label, data-msg, data-group, data-min-checked) and applies src/lib/validation.ts: messages appear under the field, linked with aria-describedby and aria-invalid, focus moves to the first problem, a status line says "Please fix the highlighted fields." and errors update as the visitor types.

Form Where Checks
Product updates src/components/newsletter-form.tsx (footer) Email required and well-formed
Changelog updates src/components/updates-form.tsx (/changelog) Email; every release or monthly digest
Talk to sales src/components/contact-sales.tsx (/contact) Names, work email (data-validate="work-email" rejects FREE_EMAIL domains), optional phone, business name, industry, team size, locations (whole number 1–999), rollout timing for 100+ people, at least one interest, optional message of at least 10 characters, consent
Sign-up src/components/signup-wizard.tsx (/signup) Name, email, password (8+ characters with a number or symbol); business name, type, locations and team size; optional invite list; terms

The demo forms don't send anything. On success they show a message that says so. To connect a form service such as Formspree, change the callback passed to useValidation(), for example in ContactForm:

tsx
const v = useValidation(async (form) => {
  const res = await fetch("https://formspree.io/f/your-id", {
    method: "POST",
    body: new FormData(form),
    headers: { Accept: "application/json" },
  });
  if (res.ok) setSent({ first: String(new FormData(form).get("first_name")).trim(), size });
});

Every field has a name, so FormData includes it. If you remove the static export (see below), you can post to a Route Handler instead (src/app/api/contact/route.ts with export async function POST(request: Request)) or call a Server Action. Create real accounts from the sign-up's callback on your backend, never in the browser.

SEO and sharing#

Each route sets its title and description with pageMeta() from src/lib/metadata.ts, which also writes the Open Graph title and description. The image is src/app/opengraph-image.png; set SITE_URL when building so its URL is absolute (metadataBase in layout.tsx):

sh
SITE_URL=https://orbit.example.com pnpm build

The 404 page is noindex. Add src/app/sitemap.ts and src/app/robots.ts if you want them (both work with the static export). There is no structured data; Organization and SoftwareApplication JSON-LD are good additions.

Accessibility#

Built in:

  • Skip link to #main, landmarks (header, labelled navs, main, footer) and one h1 per page.
  • Visible focus rings (:focus-visible in coral) and text colours that pass WCAG AA on every surface.
  • ARIA patterns: menu button with aria-expanded, a grid of labelled buttons with a single tab stop, tabs with roving tabIndex, a radio group for billing, accordion buttons, aria-pressed filters, a modal <dialog> with focus return, a step indicator with aria-current="step", and live regions for the schedule, calculator, filters, routing card and forms.
  • prefers-reduced-motion: no reveals, count-ups, orbit spin, floating badges or pop animations; instant back-to-top.
  • Decorative SVG and mock-ups are aria-hidden; meaningful graphics (wordmarks, charts) have labels.

When editing, keep one h1 per page, give every new input a <label>, keep alt="" on decorative images, and check contrast if you change the ink or coral tokens.

Building and deploying#

sh
pnpm build        # static site in out/
pnpm start        # preview out/

next.config.ts sets output: "export", images.unoptimized and basePath from DEMO_BASE_PATH (for a sub-folder, e.g. DEMO_BASE_PATH=/orbit pnpm build; asset() adds the same prefix to plain <img> paths). Deploy out/ to any static host (Netlify, Cloudflare Pages, GitHub Pages, S3) or import the repository into Vercel. demo/ is the freeframe.dev demo build and can be deleted. To use server features (Route Handlers, Server Actions), remove output: "export" and deploy to a Node.js host or Vercel.

TypeScript 7 setup#

@typescript/native provides TypeScript 7's tsc for pnpm typecheck; typescript is aliased to @typescript/typescript6 because typescript-eslint and parts of the Next.js toolchain need the TypeScript 6 API. Replace both with typescript@7 once typescript-eslint supports it.

Troubleshooting#

  • Auto-fill leaves gaps. Nobody free can take the shift without going over DEMO.maxHours; the announcement says so. Raise the limit or add a person to TEAM.
  • Prices changed but a plan card still shows the old feature list. Prices come from PRICING, but the plan cards' text and feature lists are written in src/app/pricing/page.tsx; edit them there.
  • A filter link opens the full list. The value must match a filter value exactly (case doesn't matter): a CATEGORIES key, a value in INDUSTRIES or TOPICS at the top of the page.
  • A new story or update is never hidden by the filter. Add its tags to STORIES or UPDATES in the same position as the item.
  • The drawer doesn't open from a link. Use the integration's slug, e.g. /integrations?integration=paywell.
  • A Tailwind class written in a template string has no effect. Write class names in full inside quotes; Tailwind can't find classes built from pieces.
  • Fonts fall back to the system font. Check that src/fonts/ still has the woff2 files named in src/app/fonts.ts.
  • The demo's links break under a sub-folder. Build with DEMO_BASE_PATH set to that folder.

License#

MIT, see the repository license (included as LICENSE.txt in the download). All artwork (logo, integration logos, wordmarks, avatars and social image) is original SVG and covered by the same license. Gabarito and Rethink Sans are under the SIL Open Font License 1.1 (src/fonts/*-OFL.txt).

Search documentation

Search the guides and every template's documentation