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).
pnpm install
pnpm dev # http://localhost:3000Other 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 imageCustomising#
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):
@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:
{
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 —
OrbitMarkinsrc/components/logos.tsx(header and footer); the same mark issrc/app/icon.svg, which Next.js serves as the favicon. - Integration logos —
IntegrationLogodraws thelogoof an entry insrc/data/integrations.tsx. - Customer wordmarks —
Wordmarkinsrc/components/logos.tsx(170×40, drawn incurrentColor). Each hasrole="img"and the name asaria-label; keep those when you replace them. - Avatars — inline SVG portraits (80×80) in the pages,
TEAMandAVATARS. To use photos, replace each<svg>with an<img>of the same size class, square and at least 160×160 px, with a realalt(oralt=""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 inglobals.css. - Social image —
src/app/opengraph-image.png(1200×630). Editpublic/img/og-image.svgand export it to PNG at 1200×630 to change it; most social sites don't display SVG.
Navigation, header and footer#
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-labelsays 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 tworole="radio"buttons, arrow keys switch). Annual is the default (DEFAULT_BILLING).PlanPriceandBillingNotefollow 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) readsCOMPARE; its header row sticks below the site header while you scroll the table. - FAQ —
Accordion(src/components/accordion.tsx) rendersFAQ. The first answer starts open, one is open at a time, buttons usearia-expandedandaria-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.
Filters and search#
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:
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):
SITE_URL=https://orbit.example.com pnpm buildThe 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, labellednavs,main,footer) and oneh1per page. - Visible focus rings (
:focus-visiblein 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 rovingtabIndex, a radio group for billing, accordion buttons,aria-pressedfilters, a modal<dialog>with focus return, a step indicator witharia-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#
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 toTEAM. - 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 insrc/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
CATEGORIESkey, a value inINDUSTRIESorTOPICSat the top of the page. - A new story or update is never hidden by the filter. Add its tags to
STORIESorUPDATESin 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 insrc/app/fonts.ts. - The demo's links break under a sub-folder. Build with
DEMO_BASE_PATHset 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).