Patterns
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 flowNavigationMenuandSheet— the desktop and mobile navsPageContainer— one grid shared with the app screensComitorLockup— the brand mark, never a re-typed wordmark (from@comitor/ui/shell)Card,Badge,Button,AccordionTimeline— the how-it-works stepsRating— testimonial stars, half stars includedToggleGroup— the billing switchFormField+PhoneInput— the sales formInputGroup+Field— the email captureLetterAvatar— 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.
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.
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
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.
The most polished component library we have used. Accessibility is first-class.
We replaced three internal libraries with Comitor and never looked back.
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
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.
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.
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
PageContainerso 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 — aRating’sitemLabel— behind a small client leaf instead of hoisting the directive. - Signal the recommended plan with badge, button and border together.
- Let
ComitorLocaleProviderin 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
PhoneInputadefaultCountryfrom the visitor’s region — never from the page’s language.
Don't
- Do not re-colour
ButtonorBadgeto fit a band — change the band. - Do not reach for an app composite because it is nearby;
StatCardis 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
onDismisson a standaloneBanner: it only fires for banners queued inside<Banners>. Listen toonOpenChange.
Accessibility
- One
<h1>per page — the hero headline. Every band opens at<h2>, including the ones whose heading issr-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 byaria-labelledbypointing 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, soRatingis 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. PhoneInputis a group: name it witharia-label, and spreadFormField’s control props ontoPhoneInputField, so the label, the error and the required state land on the input itself.FieldErrorisrole="alert"and the input points at it witharia-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 matchingscroll-margin-topor the bar will cover them.