Site
Product page (water)
Product page sections with a water background, customer logos, numbered steps, FAQ, and closing action.
$ npx context-ds add product-heroUsage
Use it for
The API catalog hero and plugin or integration pages (the ChatGPT page). Pass HeroWater as background for the moving water; without it the hero draws the same white to blue fade in tokens. The band takes the dimmed water texture as background.
Avoid
- White heroes on these pages (that is the site's Hero)
- Centered body copy below the hero
- A black pill on the blue band (use light and clear)
Dependencies: lucide-react
Example
examples/product-hero.tsx
import { ArrowRight } from "lucide-react";import { ProductAction, ProductCallToAction, ProductCompactFaq, ProductCustomerProof, ProductHero, ProductSteps } from "@/components/site/product-hero";const LOGO = (label: string) => `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="118" height="34"><text x="0" y="24" font-family="sans-serif" font-size="20">${label}</text></svg>`)}`;export function Site() { return ( <div className="w-full"> <ProductHero eyebrow={<span>Backed by Y Combinator</span>} title={["Every way to work", "with the web."]} description="Scrape anything, crawl entire sites, run batches, and monitor changes, plus answers and brand data. Choose the APIs your product needs, with one API key." actions={ <> <ProductAction href="https://context.dev/signup">Get API key</ProductAction> <ProductAction tone="frost" href="https://docs.context.dev" target="_blank" rel="noopener noreferrer" className="pr-4"> View documentation <ArrowRight aria-hidden="true" /> </ProductAction> </> } /> <ProductCustomerProof label="Teams build with Context.dev" logos={["Mintlify", "daily.dev", "Daydream", "Super.com"].map((name) => ({ name, src: LOGO(name) }))} /> <ProductSteps numbered title={["Install it once.", "Then just ask."]} steps={[ { title: "Add the plugin", description: "Find Context.dev in the plugin directory and add it to your workspace." }, { title: "Connect your key", description: "Paste your API key once; every chat can use it after that." }, { title: "Ask for the web", description: "Ask it to look something up, read a page, or bring current sources into the chat." }, ]} /> <ProductCompactFaq id="plugin" faqs={[ { question: "Does it cost extra?", answer: "Calls use credits from your Context.dev plan, the same as the API." }, { question: "Which pages can it read?", answer: "Any public page, rendered in a real browser." }, ]} /> <ProductCallToAction title="Ask it to look it up." description="Search the web. Read a page. Get the sources in the chat." actions={ <> <ProductAction tone="light" href="https://context.dev/signup"> Get API key </ProductAction> <ProductAction tone="clear" href="https://docs.context.dev"> View documentation </ProductAction> </> } /> </div> );}Source
components/site/product-hero.tsx
import * as React from "react";import { Minus, Plus } from "lucide-react";import { cn } from "@/lib/utils";// The blue water product pages (the ChatGPT plugin page and the API catalog): a full-bleed blue hero whose water fades// up into white behind a centered title, a quiet row of customer logos, then plain sections in a narrow column and a// blue closing band. The water itself (HeroWater, from the visuals) and the band's texture are slots; without them the// hero shows the same white to blue fade in brand tokens./** The narrow column these pages set their sections in. */function ProductContainer({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="product-container" className={cn("mx-auto w-full max-w-247.5 px-6 sm:px-8 lg:px-10.5", className)} {...props} />;}/** The two-line heading: the first line black, a line break, the second brand blue. `split` is the smaller size. */function ProductHeading({ title, size = "section", className, ...props }: Omit<React.ComponentProps<"h2">, "title"> & { title: readonly [React.ReactNode, React.ReactNode]; size?: "section" | "split" }) { return ( <h2 data-slot="product-heading" className={cn("font-normal tracking-tight text-balance text-fg", size === "split" ? "text-h3 leading-tight sm:text-h2" : "text-h2 leading-tight", className)} {...props}> {title[0]} <br /> <span className="text-fg-brand">{title[1]}</span> </h2> );}const ACTION = "inline-flex min-h-11 items-center justify-center gap-2 rounded-pill px-6 py-2.5 text-body leading-5.5 font-medium shadow-key outline-none transition-[background-color,scale] duration-150 ease-out focus-visible:ring-2 focus-visible:ring-offset-2 active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg]:size-4 [&_svg]:shrink-0";const ACTION_TONES = { /** Black, for the hero. */ dark: "border border-black bg-primary text-on-primary hover:bg-primary-hover focus-visible:ring-focus-ring", /** Frosted white, for the hero's second action. */ frost: "bg-white/70 text-fg ring-1 ring-white/90 ring-inset hover:bg-white focus-visible:ring-focus-ring", /** White, for the blue band. */ light: "border border-hairline bg-white text-fg hover:bg-white/90 focus-visible:ring-white focus-visible:ring-offset-brand", /** Clear, for the blue band's second action. */ clear: "border border-white/20 bg-white/10 text-white hover:bg-white/20 focus-visible:ring-white focus-visible:ring-offset-brand",} as const;/** The pages' keyed pills: `dark` and `frost` on the hero, `light` and `clear` on the blue band. */function ProductAction({ tone = "dark", className, ...props }: React.ComponentProps<"a"> & { tone?: keyof typeof ACTION_TONES }) { return <a data-slot="product-action" className={cn(ACTION, ACTION_TONES[tone], className)} {...props} />;}// The water's own fade, in brand tokens: white at the top, through the pale blues, to the hero's blue at the foot.const FADE = "bg-[linear-gradient(180deg,var(--ds-color-white)_0%,var(--ds-color-blue-10)_22%,var(--ds-color-blue-20)_55%,var(--ds-color-blue-50)_72%,var(--ds-color-blue-80)_100%)]";/** * The blue water hero: centered, an eyebrow line, the two-line title (the second line blue), a muted line and the * actions, with the page's picture under them when there is one (the hero then grows to a screen tall). */function ProductHero({ title, description, eyebrow, actions, background, contentHeight = false, className, children, ...props}: Omit<React.ComponentProps<"section">, "title"> & { title: readonly [React.ReactNode, React.ReactNode]; description?: React.ReactNode; /** Above the title, a quiet line ("Backed by Y Combinator"). */ eyebrow?: React.ReactNode; /** ProductAction pills. */ actions?: React.ReactNode; /** The water (HeroWater) and its texture; replaces the token fade. */ background?: React.ReactNode; /** With a picture, size to the content instead of a full screen. */ contentHeight?: boolean;}) { const titleId = React.useId(); return ( <section data-slot="product-hero" aria-labelledby={titleId} className={cn( "relative isolate flex w-full flex-col items-center justify-center overflow-hidden bg-blue-90 px-6 py-14 sm:px-8", children ? (contentHeight ? "sm:py-18" : "sm:min-h-200 sm:py-14") : "sm:min-h-108", className, )} {...props} > <div aria-hidden="true" className={cn("pointer-events-none absolute inset-0 -z-10 overflow-hidden", !background && FADE)}> {background} </div> <div className="relative flex w-full max-w-225 flex-col items-center gap-6"> <div className="flex w-full flex-col items-center gap-4 text-center"> {eyebrow && <div className="flex items-center gap-1.5 text-body-sm font-medium text-fg-nav [&_img]:size-4.5 [&_img]:shrink-0 [&_img]:rounded-sm">{eyebrow}</div>} <h1 id={titleId} className="text-h2 leading-tight font-normal tracking-tight text-balance text-fg sm:text-h1"> {title[0]} <br /> <span className="text-fg-brand">{title[1]}</span> </h1> {description && <p className="max-w-170 text-body leading-normal text-pretty text-fg-nav sm:text-body-lg">{description}</p>} {actions && <div className="flex flex-wrap items-center justify-center gap-3 pt-0.5">{actions}</div>} </div> {children} </div> </section> );}/** A centered line and a row of customer logos in gray, under the hero. */function ProductCustomerProof({ label = "Teams build with us", logos, className }: { label?: React.ReactNode; logos: { name: string; src: string }[]; className?: string }) { return ( <section data-slot="product-customer-proof" aria-label="Customers" className={cn("pt-9 pb-10.5", className)}> <ProductContainer className="flex flex-col items-center gap-5"> <p className="text-center text-body-sm text-fg-subtle sm:text-body">{label}</p> <ul role="list" className="grid w-full grid-cols-2 items-center justify-center gap-x-4 gap-y-6 sm:flex sm:gap-8.5"> {logos.map((logo) => ( <li key={logo.name} className="flex h-10.5 min-w-0 items-center justify-center sm:w-45"> <img src={logo.src} alt={logo.name} className="h-8.5 w-29.5 object-contain opacity-50 brightness-0 grayscale" /> </li> ))} </ul> </ProductContainer> </section> );}/** Three steps in columns under a two-line heading, optionally numbered in blue mono. `children` follows the steps. */function ProductSteps({ steps, title = ["From your first request", "to a working feature."], numbered = false, className, children,}: { steps: { title: React.ReactNode; description: React.ReactNode }[]; title?: readonly [React.ReactNode, React.ReactNode]; numbered?: boolean; className?: string; children?: React.ReactNode;}) { const titleId = React.useId(); return ( <section data-slot="product-steps" aria-labelledby={titleId} className={cn("py-16 sm:py-18", className)}> <ProductContainer className="flex flex-col gap-10"> <h2 id={titleId} className="flex flex-col gap-2 text-h3 leading-tight font-normal tracking-tight text-fg sm:text-h2"> <span>{title[0]}</span> <span className="text-fg-brand">{title[1]}</span> </h2> <ol className="grid gap-8 md:grid-cols-3"> {steps.map((step, index) => ( <li key={index} className="flex flex-col gap-3"> {numbered && ( <span aria-hidden="true" className="font-mono text-body-sm leading-5 text-fg-brand"> {String(index + 1).padStart(2, "0")} </span> )} <h3 className="text-h4 leading-normal font-normal tracking-normal text-fg">{step.title}</h3> <p className="text-body-lg leading-normal text-fg-subtle">{step.description}</p> </li> ))} </ol> {children} </ProductContainer> </section> );}/** The narrow FAQ: the split heading and a line on the left, hairline questions on the right, the first open. */function ProductCompactFaq({ id, faqs, title = ["A few things", "to know."], description = "More details in the API documentation.", className,}: { id: string; faqs: { question: React.ReactNode; answer: React.ReactNode }[]; title?: readonly [React.ReactNode, React.ReactNode]; description?: React.ReactNode; className?: string;}) { const titleId = React.useId(); return ( <section data-slot="product-compact-faq" aria-labelledby={titleId} className={cn("pt-8 pb-20", className)}> <ProductContainer className="grid gap-10 lg:grid-cols-[17.5rem_minmax(0,1fr)]"> <div className="flex flex-col items-start gap-4.5"> <ProductHeading id={titleId} title={title} size="split" /> {description && <p className="text-body leading-relaxed text-fg-subtle">{description}</p>} </div> <div className="min-w-0"> {faqs.map((faq, index) => ( <details key={index} name={`${id}-questions`} open={index === 0} className="group border-t border-line py-5 first:border-0 first:pt-0 first:pb-5.5"> <summary className="flex min-h-6 cursor-pointer list-none items-center justify-between gap-5 rounded-sm text-body text-fg outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-4 sm:text-body-lg [&::-webkit-details-marker]:hidden"> {faq.question} <Plus aria-hidden="true" className="size-4 shrink-0 text-fg-faint group-open:hidden" /> <Minus aria-hidden="true" className="size-4 shrink-0 text-fg-faint group-not-open:hidden" /> </summary> <div className="mt-3 text-body leading-relaxed text-fg-subtle">{faq.answer}</div> </details> ))} </div> </ProductContainer> </section> );}/** The blue closing band: a centered white heading, a line of text and the light actions over the water texture. */function ProductCallToAction({ title = "Start with your next request.", description, actions, background, className,}: { title?: React.ReactNode; description?: React.ReactNode; /** ProductAction pills, `light` and `clear`. */ actions?: React.ReactNode; /** The water texture, dimmed so the white copy reads. */ background?: React.ReactNode; className?: string;}) { const titleId = React.useId(); return ( <section data-slot="product-call-to-action" aria-labelledby={titleId} className={cn("relative isolate flex w-full items-center overflow-hidden bg-navy-60 py-16 text-center text-white sm:min-h-75", className)}> {background && ( <div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10"> {background} </div> )} <ProductContainer className="relative flex flex-col items-center gap-5.5"> <h2 id={titleId} className="text-h3 leading-tight font-normal tracking-tight text-balance text-white sm:text-h2"> {title} </h2> {description && <p className="max-w-160 text-body leading-7 text-pretty text-white/85 sm:text-body-lg">{description}</p>} {actions && <div className="flex flex-wrap justify-center gap-3 pt-1.5">{actions}</div>} </ProductContainer> </section> );}export { ProductHero, ProductAction, ProductContainer, ProductHeading, ProductCustomerProof, ProductSteps, ProductCompactFaq, ProductCallToAction };Reference: src/components/sections/products/product-hero.tsx; product-customer-proof.tsx; product-workflows.tsx; product-faq.tsx; product-call-to-action.tsx; product-layout.tsx; src/components/sections/shared/hero-background.tsx; cta-background.tsx.