Landing Page

Fourteen bands that stack into a public site: announcement bar, navbar, hero, logo cloud, features, how it works, stats, testimonials, pricing, a sales form, FAQ, newsletter, CTA and footer. A landing page is the one screen in this section that is not an application, and it is built accordingly.

The other pattern pages are mostly composites wired together. This one is mostly layout — and that is the correct answer, not a gap. A visitor who has never signed in needs a headline and a price, not a data table. Where the package does have the part — a bar that can be closed, a star rating, steps in order, a phone field that knows about countries — the page uses it rather than drawing one.

Where the package stops

Worth being explicit, because the temptation on a page like this is to reach for a composite that was designed for a different job. Every band below leans on the package for structure, surface and colour; the arrangement is yours.

From @comitor/ui

  • Banner — the announcement bar, standalone and in the page flow
  • NavigationMenu and Sheet — the desktop and mobile navs
  • PageContainer — one grid shared with the app screens
  • ComitorLockup — the brand mark, never a re-typed wordmark (from @comitor/ui/shell)
  • Card, Badge, Button, Accordion
  • Timeline — the how-it-works steps
  • Rating — testimonial stars, half stars included
  • ToggleGroup — the billing switch
  • FormField + PhoneInput — the sales form
  • InputGroup + Field — the email capture
  • LetterAvatar — testimonial authors

Hand-built, on purpose

  • The hero, the logo cloud and the CTA band — layout and copy, nothing reusable in them
  • The stats band: StatCard is a dashboard tile with a delta and a loading state, which a headline numeral has no use for
  • Customer logos, which are assets rather than UI

Announcement bar

One line above the header for the release, the event or the outage. Banner on its own — no <Banners> queue — renders in the page flow, so closing it moves the header up and nothing ever floats over the page.

NewComitor 3.0 is out
English by default, Vietnamese built in — one provider at the root.

Close the bar and the header moves up — nothing else on the page is involved.

On its own, the bar reports its close button through onOpenChange(false) — that is where the dismissal gets written. onDismiss reads like the right hook and never fires here: it belongs to banners queued inside <Banners>. Keep the dismissal in a cookie the server reads; a localStorage check after mount paints the bar for every returning visitor, then collapses the page under them.

Navbar

Sticky bar with the lockup, a NavigationMenu whose own documentation names public sites as its job, and the two account actions. Below md the same links move into a Sheet. Both previews are live — open the Product menu, then open the sheet.

One trap in that file: NavigationMenuLink renders a plain anchor unless you hand it asChild with next/link; the package deliberately never imports next/* so the entry keeps resolving outside Next. The sheet’s close button needs nothing: since 3.0.0 its accessible name follows ComitorLocaleProvider — “Close” on this site, “Đóng” under locale="vi".

Hero

Badge, headline, one sentence, and a primary + secondary pair. The container is PageContainer at width="md" — the same component the product screens use, which is what keeps the two sides of the funnel on one grid.

Now in beta

Design systems that ship.

Components, patterns and tokens in one package — so the third screen costs what the first one did.

The code sample uses Button asChild so the CTA is a real link. Note what that costs: Radix Slot accepts exactly one child, so leftIcon and rightIcon are dropped without warning — the arrow has to move inside the <Link>. The preview above keeps rightIcon precisely because its buttons go nowhere.

Logo cloud

Social proof directly under the hero. Pure layout: the package ships no logo component and should not, since customer marks are assets on a CDN rather than UI.

Trusted by teams at world-class companies

  • Acme
  • Globex
  • Initech
  • Umbrella
  • Stark
  • Wayne

Features grid

Three benefits as real Cards rather than bordered <div>s. That single swap buys the surface, radius, border and shadow tokens, so these cards age with the system instead of drifting from it.

Ship faster

Fifty-plus components and eleven screen patterns, already assembled.

Stay consistent

One token contract across web, email and print. Contrast is measured, not guessed.

Scale easily

Nine entry points, tree-shaken. Take the button, leave the charts.

The icon chip follows the colour contract the rest of the system uses: the accent tints the background (bg-primary/15) and the glyph sits on it at text-foreground. Gold-on-gold measures about 1.7:1 against the 3:1 that WCAG 1.4.11 asks of graphical objects.

How it works

Three steps from install to first screen, as a Timeline: a list whose order is the content, with the dots and the rule between them drawn for you. The band stays a server component — every prop it passes is data.

Up and running on day one

Install one package
Components, patterns and tokens arrive together, with one stylesheet import.
Set the language once
Wrap the root layout in one provider. Every label, date and screen-reader string follows it.
Ship the first screen
The shell, the table and the forms are already assembled — the first screen costs what the tenth will.

TimelineTime renders a real <time> and formats nothing: the words are yours, and dateTime takes the machine-readable form — an ISO 8601 duration such as PT5M is as valid there as a date. Leave activeIndex off a static band like this one: written in a Server Component, the items are out of the timeline’s sight, so the server renders every dot as pending and the browser recolours the finished ones after hydration.

Stats band

Four figures with short labels. This is the clearest case on the page for not reaching for a composite — the reasoning is in the code sample, and it is worth reading before the next time a landing-page band tempts you toward a dashboard component.

Active developers
40k+
Components rendered
120M
Uptime SLA
99.9%
Average rating
4.9/5

Marked up as a description list so each figure keeps its label as data, and set in tabular-nums so the four columns line up on the digits.

Testimonials

Quote, rating, attribution. The stars are Rating, read-only and in half steps; authors use LetterAvatar, which derives both the initials and the colour behind them from the name — so the same person is the same colour here, in the app and in an exported PDF.

Comitor cut our design-to-ship time in half. The tokens just work everywhere.
Sarah ChenHead of Design, Globex
The most polished component library we have used. Accessibility is first-class.
Marcus WebbEngineering Lead, Initech
We replaced three internal libraries with Comitor and never looked back.
Priya NairCTO, Stark

A quote wants <figure> semantics and Card renders a <div>, so the card look arrives through the exported cardVariants() instead. Reach for the variant function whenever you need a component's appearance on a different element — copying its class string is how two cards end up one pixel apart.

A read-only Rating is still a radiogroup, one Tab stop, so it takes an aria-label with the figure. In half steps the checked star is the one the value rounds up to — 4.5 checks the fifth — and the package names that star with the exact value, “4.5 stars”. Every prop here is data, so the server component renders Rating itself; only a function prop, such as a custom itemLabel, would need a client file.

Pricing table

Three tiers with a working billing switch — flip it and the Pro column moves from $29 to $23. Both columns are per user per month; the discount is the unit price, never a yearly total dressed up as one.

Starter

For side projects and prototypes.

Free

  • All components
  • Design tokens
  • Community support
Most popular

Pro

For product teams shipping weekly.

$29/user/mo

  • Everything in Starter
  • Private component registry
  • Priority support
  • Design-tool sync

Enterprise

For regulated and multi-brand orgs.

Custom

  • Everything in Pro
  • Custom token themes
  • SSO and audit log
  • 99.9% uptime SLA

The recommended tier is signalled three ways — Badge variant="primary", a filled button and a border-primary-ink ring — because gold on a white card measures 1.86:1 and cannot carry the meaning alone. Note also the onValueChange guard: Radix ToggleGroup emits an empty string when you press the active item again, and without the guard the table would render a plan with no price.

Sales form

Where “Talk to sales” leads. FormField wires every label, description and error, and PhoneInput takes a number the way people dial it at home and hands back E.164. Live: submit it empty to see the errors, then type 0912 345 678 and see what arrives.

Talk to sales

Enterprise is quoted, not listed. Leave a number and we call within one business day.

The country is region, not language: this page is English, and its visitors may be in Hanoi. PhoneInput has no default country, so pass defaultCountry — from where the request says the visitor is, or from the market the page sells to. With one, a domestic 0912 345 678 drops its trunk zero and becomes +84912345678; without one, the same keystrokes become +0912345678, a number no phone answers. Start with + and the field switches to international entry, the flag following the dialling code.

FAQ

Objection handling immediately before the last conversion step. Accordion with type="single" and collapsible: one answer open at a time, and it can be closed again.

Newsletter signup

For the visitor who is not ready to convert. Live: submit an empty or malformed address to see the error, a valid one to see the confirmation.

Stay in the loop

Release notes and design essays. Roughly monthly.

InputGroup puts the icon and the submit button inside the field's own border, so one control reads as one target. Field carries the invalid state for the row and FieldError renders role="alert", which is what makes the message audible rather than merely red. noValidate hands validation to your code so the copy is yours and not the browser's.

Call to action

The last band before the footer: brand ink, gold headline, one button. It is the only surface on the page that does not follow the theme, because a brand band is a brand band in both.

Ready to ship faster?

Join the teams building with clarity and shipping with confidence.

No re-coloured button here. The default gold Button is already correct on ink, and a ring-border keeps the band from dissolving into the page background in dark mode, which is ink itself. Its text breaks from role tokens on purpose: on a surface that never changes, text-primary and text-ash-300 hold in both palettes, where the high-contrast palette would take text-primary-ink down to 2.90:1 on ink in light mode.

Footer

Brand mark, three link columns, legal bar. The mark is ComitorLockup at variant="auto", which follows the theme and keeps the gold rule and slogan at their brand-book proportions — a re-typed wordmark gets none of that.

Design systems that ship.

Product

  • Features
  • Pricing
  • Changelog
  • Roadmap

Resources

  • Docs
  • Guides
  • Support
  • Status

Company

  • About
  • Blog
  • Careers
  • Contact

© 2026 Thien Minh Software Co., Ltd. All rights reserved.

PrivacyTerms

Composing the page

Each band is one file exporting one section; the route stacks them. Note where 'use client' lives — on the sections that own state, never on the route. The route itself reads one cookie, to leave out an announcement the visitor has already closed. Which exports cross that line, and how they fail when they cannot, is in Server and client.

app/page.tsx
import type { Metadata } from 'next'
import { cookies } from 'next/headers'
import { ANNOUNCEMENT_ID } from '@/components/landing/announcement'
import { AnnouncementBar } from '@/components/landing/announcement-bar'
import { SiteHeader } from '@/components/landing/site-header'
import { Hero } from '@/components/landing/hero'
import { LogoCloud } from '@/components/landing/logo-cloud'
import { Features } from '@/components/landing/features'
import { HowItWorks } from '@/components/landing/how-it-works'
import { Stats } from '@/components/landing/stats'
import { Testimonials } from '@/components/landing/testimonials'
import { Pricing } from '@/components/landing/pricing'
import { ContactSales } from '@/components/landing/contact-sales'
import { Faq } from '@/components/landing/faq'
import { Newsletter } from '@/components/landing/newsletter'
import { CallToAction } from '@/components/landing/call-to-action'
import { SiteFooter } from '@/components/landing/site-footer'

export const metadata: Metadata = {
  title: 'Comitor — design systems that ship',
  description: 'Components, patterns and tokens in one package.',
}

/*
 * NO 'use client' on the page. The sections that own state carry the directive
 * themselves — the announcement bar, the header's sheet and nav menu, pricing,
 * the sales form, the newsletter. Hoisting it up here would ship the hero, the
 * timeline, the stats and the footer to the browser to do nothing.
 *
 * Every band is <section> + PageContainer: the section paints edge-to-edge, the
 * container clamps and pads the content. Same container the app screens use, so
 * the public site and the product share one left margin.
 */
export default async function HomePage() {
  // Reading cookies() renders this route per request. A home page that must stay
  // fully static leaves the bar out instead — a bar that flashes is worse.
  const dismissed = (await cookies()).get('announcement')?.value === ANNOUNCEMENT_ID

  return (
    <>
      <AnnouncementBar dismissed={dismissed} />
      <SiteHeader />
      <main>
        <Hero />
        <LogoCloud />
        <Features />
        <HowItWorks />
        <Stats />
        <Testimonials />
        <Pricing />
        <ContactSales />
        <Faq />
        <Newsletter />
        <CallToAction />
      </main>
      <SiteFooter />
    </>
  )
}

Best practices

Do

  • Wrap every band in PageContainer so the public site and the product share a left margin.
  • Keep one primary CTA per band; the secondary action is variant="outline".
  • Push 'use client' down to the sections that need it, and put a function prop — a Rating’s itemLabel — behind a small client leaf instead of hoisting the directive.
  • Signal the recommended plan with badge, button and border together.
  • Let ComitorLocaleProvider in the root layout decide the language — since 3.0.0 the package speaks English until told otherwise, close buttons and screen-reader text included. See Set the language.
  • Give PhoneInput a defaultCountry from the visitor’s region — never from the page’s language.

Don't

  • Do not re-colour Button or Badge to fit a band — change the band.
  • Do not reach for an app composite because it is nearby; StatCard is not a stats band.
  • Do not mix billing units in one table without a toggle that switches both columns.
  • Do not re-type the wordmark, or rebuild the lockup out of a logo and a <span>.
  • Do not build class names from data (bg-${tone}-300) — Tailwind scans statically and purges what it cannot see.
  • Do not wait for onDismiss on a standalone Banner: it only fires for banners queued inside <Banners>. Listen to onOpenChange.

Accessibility

  • One <h1> per page — the hero headline. Every band opens at <h2>, including the ones whose heading is sr-only, so the outline survives even where the design has no visible title. (The previews on this page use <h3> to sit under the documentation's own outline.)
  • Each band is a <section> named by aria-labelledby pointing at its heading — a <section> without an accessible name is not a landmark at all.
  • Decorative glyphs are aria-hidden; meaning that lives in a glyph is spelled out. A star rating carries meaning, so Rating is a named radiogroup — read-only here — whose checked star says the exact figure.
  • Accent colour tints backgrounds and never paints a glyph that sits on its own tint: gold-on-gold measures about 1.7:1 where 1.4.11 requires 3:1.
  • Give every icon-only button of your own an aria-label — the menu trigger has no text. The package’s own are named for you, in the provider’s language: the sheet’s close, the banner’s close, and the phone field’s country button, which also says which country it holds (“Country: Vietnam (+84)”).
  • The announcement bar is a role="status" region. Its close button leaves with it, so move focus on: the demo hands it to the button that brings the bar back, and a site hands it to the header’s home link.
  • PhoneInput is a group: name it with aria-label, and spread FormField’s control props onto PhoneInputField, so the label, the error and the required state land on the input itself.
  • FieldError is role="alert" and the input points at it with aria-describedby, so the failure is announced and reachable, not just red.
  • CTAs say what they do. “Start 14-day trial” reads correctly in a list of links; “Learn more” five times does not.
  • Tab order follows reading order in every band, and the sticky navbar sits at z-40 — give anchor targets a matching scroll-margin-top or the bar will cover them.