Freeframe

Documentation

Prism

v1.0.2Updated Oct 17, 2026A sleek, dark-first website for an AI startup, with a live streaming playground and a usage cost calculator.

Overview#

Prism is a marketing website for an AI startup, built for Prism, a fictional research assistant made by Prism Labs (Toronto and London). The product answers questions from a team's docs, tickets and the web with citations, in an app and through an API. The site has a home page with a streaming playground demo, a product page, API docs, pricing with a cost calculator, a changelog, a company and careers page and a sales form. 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 dark-first: near-black surfaces, a hairline grid, film grain and spectrum gradients taken from light passing through a prism. It exports to static files. Live demo: https://freeframe.dev/templates/prism

The site is a front end only. The playground streams pre-written answers, 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/prism into demo/

Pages#

Route File Purpose and features
/ src/app/page.tsx Hero, streaming playground, logo strip, feature grid, how it works, benchmark chart, testimonials, call to action
/product src/app/product/page.tsx Search, write, briefs and analyse sections with UI mock-ups, integrations grid (#integrations), security badges and data retention table (#security), model cards and latency chart (#models)
/developers src/app/developers/page.tsx Example response, three-step quickstart with code tabs (#quickstart), endpoint list (#endpoints), SDK cards with copy buttons (#sdks), rate limits table (#rate-limits)
/pricing src/app/pricing/page.tsx Monthly / annual switch, three plans, API price table, cost calculator (#calculator), FAQ accordion (#faq)
/changelog src/app/changelog/page.tsx Dated releases on a timeline, topic filter with counts (/changelog?tag=api)
/about src/app/about/page.tsx Story, figures, values, team, open roles with a team filter (/about?team=research#careers)
/contact src/app/contact/page.tsx What happens next, offices, sales form (/contact?topic=security preselects the topic)
404 src/app/not-found.tsx Prism 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, reveals, back to top
    globals.css           Tailwind import, @theme tokens, component classes (buttons, cards, code, playground...)
    fonts.ts              Geist, Geist Mono and Instrument Serif 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 Prism 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, status pill, sign-up form
    playground.tsx        streaming playground (client)
    benchmarks.tsx        chart legend and table; benchmark-chart.tsx draws the SVG (client)
    logo-strip.tsx        customer wordmarks in a looping marquee
    billing.tsx           monthly / annual switch and prices that follow it (client)
    cost-calculator.tsx   seats + tokens estimate (client)
    accordion.tsx         FAQ accordion (client)
    code-tabs.tsx         language tabs for code samples (client)
    copy-button.tsx       copy to clipboard (client)
    filter.tsx            topic filters for releases and roles (client)
    contact-form.tsx      sales form (client); newsletter-form.tsx is the footer form
    use-validation.tsx    inline validation hook shared by both forms
    success-box.tsx       the message that replaces a sent form
    announcer.tsx         one polite live region for short announcements
    reveal-observer.tsx   scroll reveals; back-to-top.tsx; logo.tsx; check-icon.tsx; year.tsx
  data/
    nav.ts                header and footer links
    playground.ts         playground examples, fallback answer and models
    benchmarks.ts         benchmark models and scores
    pricing.tsx           calculator settings, plans, API prices, FAQ
    developers.tsx        code samples, endpoints, SDKs, rate limits
    changelog.ts          releases and topics
    careers.ts            open roles and teams
  lib/
    validation.ts         form rules (required, email, work email, min length, groups)
    code-lang.ts          the remembered code language
    storage.ts            safe localStorage and URL parameter helpers
    announce.ts, copy.ts, metadata.ts, asset.ts, rich-text.tsx
  fonts/                  woff2 files and their OFL licenses
public/img/
  prism-beam.svg          prism and light-beam illustration (CTA panels and 404)
  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-ink-900, text-muted or text-brand-300 comes from them.

Token Default Used for
--color-ink-950 #05060a Page background
--color-ink-900, --color-ink-850, --color-ink-800 #0a0c12, #0f1219, #151924 Cards, inputs, raised panels
--color-fg #eef0f6 Headings, body text, primary buttons
--color-muted #a6acbe Secondary text
--color-subtle #8b92a7 Captions and meta text (still AA on every surface)
--color-brand-200 … --color-brand-700 aqua scale Links, focus rings, highlights, citation chips
--color-ray-rose, -amber, -lemon, -mint, -sky, -violet the spectrum Logo, gradients, chart bars, icons

The spectrum gradient used for dividers and tabs is the --spectrum custom property in :root; the gradient text class .text-spectrum and the .glass card border are defined in the same file. To change the brand colour, edit the brand scale:

css
@theme {
  --color-brand-300: #a5b4fc;
  --color-brand-400: #818cf8;
  --color-brand-500: #6366f1;
}

Prism is a dark-only design (color-scheme: dark in globals.css and colorScheme: "dark" in the viewport export of layout.tsx). There is no light theme or toggle. To lighten it, change the ink and text tokens together and check contrast again. A few colours are written into SVG art and the benchmark chart (src/components/benchmark-chart.tsx, src/data/benchmarks.ts); update those too.

Typography#

Three fonts are self-hosted with next/font/local in src/app/fonts.ts, from the Latin woff2 files Google Fonts serves (src/fonts/): Geist (text and headings), Geist Mono (labels, code, numbers) and Instrument Serif (the italic accent word in headlines, class .serif, and two logo wordmarks). Each keeps its real family name, so the stacks --font-sans, --font-mono and --font-serif in @theme and the font-family attributes in the SVG logos can name them directly. 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 lists and interactive parts lives in src/data/:

File Export What it holds
playground.ts PLAYGROUND Example prompts with their answers, sources, seconds and token counts
PLAYGROUND_FALLBACK The answer for any prompt that isn't an example ({prompt} is replaced)
MODELS Playground models: milliseconds per token and a time factor for the meta line
benchmarks.ts BENCHMARKS Chart models (name, colour) and tests (name, scores in percent)
pricing.tsx PRICING Seat prices, included tokens, input share, API prices per model, slider steps, enterprise threshold, calculator defaults
PLANS, API_PRICES, FAQ Plan cards, the API price table and the FAQ
developers.tsx RESPONSE_SAMPLE, KEY_SAMPLE, REQUEST_SAMPLES, STREAM_SAMPLES Highlighted code samples
ENDPOINTS, SDKS, RATE_LIMITS Endpoint list, SDK cards, rate limits table
changelog.ts TOPICS, RELEASES Filter topics (label and dot colour) and releases
careers.ts TEAMS, ROLES Teams for the filter and open roles
nav.ts NAV, FOOTER_NAV Header menu and footer columns

To add a playground example, add an object to PLAYGROUND. In answer text, **bold** makes emphasis and [n] cites source n:

ts
{
  id: "churn",
  label: "Explain last month’s churn",
  prompt: "Why did churn go up in September?",
  seconds: 1.8,
  tokens: 120,
  sources: [{ kind: "Analytics", title: "Churn dashboard, September" }],
  answer: [
    { p: "Churn rose to **3.9%**, mostly from monthly plans [1]." },
    { ul: ["First point [1].", "Second point [1]."] },
  ],
},

The example buttons are generated from the array. To add a release, add an entry at the top of RELEASES; text between backticks is shown as code and the date label is formatted from date:

ts
{
  date: "2026-10-22",
  version: "v2.1",
  title: "Shared briefs",
  tags: ["product"],
  summary: "Briefs can now be shared with a link.",
  items: ["New `share` field on `/v1/briefs`."],
},

Open roles are { title, team, location } objects in ROLES; team must be a key of TEAMS.

Images and artwork#

There are no photos. Everything is SVG or CSS:

  • Logo — PrismMark in src/components/logo.tsx (header and footer); the same mark is src/app/icon.svg, which Next.js serves as the favicon.
  • Customer logos — inline SVG wordmarks in src/components/logo-strip.tsx. Each has role="img" and an aria-label; keep those when you replace them.
  • Avatars — inline SVG portraits in the testimonials (src/app/page.tsx) and the team grid (src/app/about/page.tsx). To use photos, replace each <svg> with an <img> of the same size class (square, at least 400×400 px for the team, 80×80 px for testimonials) and a real alt.
  • Prism illustration — public/img/prism-beam.svg (800×400), used in the call-to-action panels and the 404 page through asset("/img/prism-beam.svg"), which adds the base path.
  • Product mock-ups — the playground, citation preview, search results, draft editor, briefs and charts are JSX and inline SVG, marked aria-hidden="true" where they are decorative.
  • Background — the grid (.bg-grid), glow (.aurora) and film grain (.grain on <body> in layout.tsx, an SVG noise filter) are classes in globals.css. Remove grain to drop the grain.
  • 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 and the footer columns from 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 NAV (the mobile menu uses the same list plus "Talk to sales"). The current page gets aria-current="page", which also styles it. The header becomes frosted after scrolling (class is-scrolled).

Features#

Playground#

On the home page, #playground streams a pre-written answer token by token: sources appear first ("Searching 4 sources…"), then the text with a caret, numbered citation links to the sources, and a meta line with model, time and tokens. The first example runs once when the playground scrolls into view.

  • Code: src/components/playground.tsx; data in PLAYGROUND, PLAYGROUND_FALLBACK and MODELS.
  • Controls: the example buttons, the prompt (#pg-prompt, run with Ctrl/Cmd + Enter), the Run button that becomes Stop while streaming, and the model select (#pg-model), which changes the streaming speed via msPerToken.
  • Any other prompt gets PLAYGROUND_FALLBACK, with the visitor's text inserted as text (React escapes it).
  • Connecting a real model: call your own backend from run() (never call an AI API with a secret key from the browser), for example a Route Handler that streams, and append each chunk to the answer. The component keeps the answer as tokens (written counts how many are shown), so a real stream can push tokens the same way the timer does.
  • Accessibility: the answer area has aria-live="polite" and aria-busy="true" while streaming, so screen readers read the finished answer once. Example buttons use aria-pressed. With reduced motion the whole answer appears at once and nothing auto-plays.

Benchmark chart#

src/components/benchmark-chart.tsx draws grouped horizontal bars from BENCHMARKS at the box's real width and redraws on resize, so labels stay readable on phones. BenchmarkLegend and BenchmarkTable in src/components/benchmarks.tsx render the legend and the table inside "View as a table" on the server. The first model uses the spectrum gradient (color: "spectrum"); others take any CSS colour. The SVG has role="img" and an aria-label that lists every value. Bars grow in when the figure is revealed (.bar rules in globals.css), instantly with reduced motion.

Pricing and calculator#

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

  • Billing switch BillingToggle (#billing-toggle, role="switch", aria-checked). Plan prices and notes given as { monthly, annual } in PLANS follow it through ByBilling. Annual is the default (PRICING.defaults.annual).
  • Calculator (src/components/cost-calculator.tsx) with #calc-seats (1–250), #calc-tokens (steps from PRICING.tokenSteps, in millions) and the model radios. The estimate is seats × seat price (from PRICING.seat, following the switch) plus billable tokens × a blended price (inputShare input, the rest output), after includedTokensM free tokens. From enterpriseSeats seats a note suggests Enterprise pricing.

Prices are worked out from PRICING; the plan cards and the API table read PLANS and API_PRICES, so keep those in step when prices change. The sliders announce readable values (aria-valuetext), and the total is announced politely 0.7 s after changes stop.

Code tabs and copy buttons#

On /developers, each CodeTabs card (src/components/code-tabs.tsx) has a role="tablist" with cURL, JavaScript and Python tabs and role="tabpanel" panels. Choosing a language switches every card on the page and is remembered (see Saved data); src/lib/code-lang.ts holds the shared value. Arrow keys, Home and End move between tabs.

CopyButton (src/components/copy-button.tsx) copies its text prop (the SDK install commands); without one it copies the visible code block in its [data-tabs] or [data-copy-scope] card. The label changes to "Copied" for 1.8 s and the live region announces it. Code is coloured with spans (t-k, t-s, t-n, t-c, t-f, t-p) styled in globals.css; when you change a sample in src/data/developers.tsx you can drop the spans and keep plain text. Code blocks are focusable so keyboard users can scroll them.

Filters#

src/components/filter.tsx powers the changelog topics and the careers team filter. FilterProvider gets every item's tags, the URL parameter (tag or team) and the noun for announcements; FilterBar renders the buttons with counts (aria-pressed), FilterItem hides items that don't match and FilterEmpty shows when nothing does. The choice is kept in the URL (/changelog?tag=security) with history.replaceState, so filtered views can be shared; unknown values show everything. "Showing 4 releases" is announced after each click.

FAQ accordion#

Accordion (src/components/accordion.tsx) renders FAQ from src/data/pricing.tsx. Each button has aria-expanded and aria-controls pointing at its panel. One answer is open at a time; store a set of open indexes instead of one number to allow several. Without JavaScript every answer is visible.

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, and nothing is hidden for reduced motion or without JavaScript. The logo strip scrolls slowly (paused on hover, static for reduced motion). A back-to-top button (src/components/back-to-top.tsx) appears after one screen of scrolling; remove it from layout.tsx and delete the .back-to-top styles to drop it.

Forms#

There are two forms. Both use useValidation() from src/components/use-validation.tsx, which reads the rules from each field's attributes (required, type, data-validate, minLength, data-label, data-msg, data-group) and applies src/lib/validation.ts: messages appear under the field, linked with aria-describedby and aria-invalid, focus moves to the first problem, and errors update as the visitor types.

Form Where Checks
Newsletter src/components/newsletter-form.tsx (footer of every page) Email required and well-formed
Talk to sales src/components/contact-form.tsx (/contact) First and last name, work email (data-validate="work-email" rejects FREE_EMAIL domains), company, team size, at least one "Interested in" box, optional message of at least 20 characters, consent box

/contact?topic=careers (or sales, trial, enterprise, security, api, partnerships, other) preselects the topic; the options are TOPICS in contact-form.tsx.

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():

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((form.elements.namedItem("first_name") as HTMLInputElement).value.trim());
});

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.

Saved data (localStorage)#

Key Value Validation
prism-code-lang curl, js or python getLang() in src/lib/code-lang.ts ignores anything else and uses curl

It only stores the preferred code language on /developers. Reading and writing go through src/lib/storage.ts, which catches errors, so blocked storage only means the choice isn't remembered. To reset it, run localStorage.removeItem("prism-code-lang") in the console; to stop saving, remove the writeStore() call in setLang().

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://prism.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 with ordered headings.
  • Visible focus rings (:focus-visible in brand aqua) and colour contrast that passes WCAG AA on every surface.
  • ARIA patterns: menu button with aria-expanded, tabs with roving tabIndex, a switch (role="switch"), accordion buttons, aria-pressed filters and examples, live regions for the playground, calculator, filters and copy buttons, and labelled scrollable tables (role="region", tabIndex={0}).
  • prefers-reduced-motion: no streaming animation, no marquee, no reveals, instant back-to-top.
  • Decorative SVG and mock-ups are aria-hidden; meaningful graphics (logos, 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, muted or subtle 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=/prism pnpm build; asset() adds the same prefix to <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, a real model behind the playground), 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#

  • The playground answer appears all at once. Your system has "reduce motion" turned on; streaming is intentionally skipped.
  • A new example doesn't match when typed. Typed prompts match an example only if the text equals its prompt (ignoring case and surrounding spaces); anything else gets the fallback answer.
  • Calculator totals don't match the plan cards. The calculator reads PRICING; the cards read PLANS and the table API_PRICES. Update all three in src/data/pricing.tsx.
  • Copy buttons say "Press Ctrl+C". The browser blocked clipboard access. It works on localhost and HTTPS.
  • A Tailwind class written in a template string has no effect. Keep a space between a class name and a ${...} expression (`h-4 w-4 ${on ? "hidden" : ""}`), otherwise Tailwind can't find the class.
  • 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 or images 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, wordmarks, avatars, prism illustration and social image) is original SVG and covered by the same license. Geist, Geist Mono and Instrument Serif are under the SIL Open Font License 1.1 (src/fonts/*-OFL.txt).

Search documentation

Search the guides and every template's documentation