Glyph Field CTA
A closing call to action on a dark panel: the headline, the install command with a copy button, and two links beside your logo printed in monospace glyphs. The mark decodes into place as it scrolls into view and inks in the accent under the pointer.
components/glyph-field-cta.tsx
"use client";
import * as React from "react";
import { GlyphField } from "@/components/ui/glyph-field";
/* ── Content ─────────────────────────────────────────────────
* Everything you'd change to make the section yours. MARK is
* your logo as one SVG path; Glyph Field prints it in type.
* ─────────────────────────────────────────────────────────── */
/** The one accent: the glyph ink and the primary button. */
const ACCENT = { light: "#ea580c", dark: "#fb923c" };
/** Your logo as a single path, and the viewBox it was drawn in. */
const MARK = {
path: "M1 7.5c4.2-.9 8 1.2 11 5 3-3.8 6.8-5.9 11-5-4.4 1.4-8.1 5-11 10.5C9.1 12.5 5.4 8.9 1 7.5Z",
viewBox: "0 3 24 18",
label: "The Tern mark, printed in type",
};
const HEADLINE = "Your next chart is";
const HEADLINE_QUIET = "one sentence away.";
const SUBLINE =
"Install the CLI and sign in once, in your own terminal. From then on your agent adds the events, the tracking and the charts you ask for.";
const COMMAND = "npx tern@latest init";
const PRIMARY_CTA = { label: "Start free", href: "#start" };
const SECONDARY_CTA = { label: "Read the docs", href: "#docs" };
/* ── Section ─────────────────────────────────────────────── */
const focusRing =
"outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950";
export function GlyphFieldCta() {
return (
<section
style={{ "--accent-light": ACCENT.light, "--accent-dark": ACCENT.dark } as React.CSSProperties}
className="bg-white [--accent:var(--accent-light)] dark:bg-zinc-950 dark:[--accent:var(--accent-dark)]"
>
<div className="mx-auto max-w-6xl px-6 py-24 sm:py-32">
<div className="grid overflow-hidden rounded-3xl bg-zinc-950 text-white ring-1 ring-zinc-950/10 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] dark:bg-zinc-900 dark:ring-white/10">
<div className="flex flex-col justify-center px-6 pt-12 pb-4 sm:px-12 sm:pt-16 lg:py-20">
<h2 className="max-w-[14ch] text-4xl/[1.05] font-semibold tracking-[-0.035em] text-balance sm:text-5xl/[1.02]">
{HEADLINE} <span className="text-zinc-500">{HEADLINE_QUIET}</span>
</h2>
<p className="mt-5 max-w-md text-base/7 text-pretty text-zinc-400">{SUBLINE}</p>
<CopyCommand command={COMMAND} />
<div className="mt-8 flex flex-wrap items-center gap-x-6 gap-y-3">
<a
href={PRIMARY_CTA.href}
className={`inline-flex h-11 items-center gap-1.5 rounded-full bg-(--accent) px-5 text-sm font-medium text-white transition-[filter,scale] duration-150 hover:brightness-110 active:scale-[0.97] dark:text-zinc-950 ${focusRing}`}
>
{PRIMARY_CTA.label}
<ArrowIcon className="size-3.5" />
</a>
<a
href={SECONDARY_CTA.href}
className={`rounded-md text-sm font-medium text-zinc-300 underline-offset-4 transition-colors hover:text-white hover:underline ${focusRing}`}
>
{SECONDARY_CTA.label}
</a>
</div>
</div>
<GlyphField
path={MARK.path}
viewBox={MARK.viewBox}
label={MARK.label}
cellSize={11}
glyphClassName="text-zinc-500"
accentClassName="text-(--accent)"
className="h-72 sm:h-96 lg:h-auto lg:min-h-[30rem]"
/>
</div>
</div>
</section>
);
}
/* ── The install command ─────────────────────────────────── */
function CopyCommand({ command }: { command: string }) {
const [copied, setCopied] = React.useState(false);
const timer = React.useRef(0);
React.useEffect(() => () => window.clearTimeout(timer.current), []);
const copy = async () => {
try {
await navigator.clipboard.writeText(command);
} catch {
return;
}
setCopied(true);
window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => setCopied(false), 1600);
};
return (
<div className="mt-8 flex h-11 max-w-sm items-center gap-3 rounded-xl bg-white/[0.06] pr-1.5 pl-4 ring-1 ring-white/10">
<span className="text-zinc-500 select-none" aria-hidden>
$
</span>
<code className="min-w-0 flex-1 truncate font-mono text-[13px] text-zinc-100">{command}</code>
<button
type="button"
onClick={copy}
aria-label={copied ? "Copied" : "Copy the install command"}
className={`grid size-8 shrink-0 place-items-center rounded-lg text-zinc-400 transition-colors hover:bg-white/10 hover:text-white ${focusRing}`}
>
{copied ? <CheckIcon className="size-4 text-(--accent)" /> : <CopyIcon className="size-4" />}
</button>
<span className="sr-only" aria-live="polite">
{copied ? "Copied to the clipboard" : ""}
</span>
</div>
);
}
/* ── Icons ───────────────────────────────────────────────── */
function ArrowIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" className={className} fill="none" stroke="currentColor" strokeWidth={1.6} aria-hidden>
<path d="M3.5 8h9m-3.5-3.5L12.5 8 9 11.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function CopyIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" className={className} fill="none" stroke="currentColor" strokeWidth={1.4} aria-hidden>
<rect x="5.5" y="5.5" width="8" height="8" rx="1.8" />
<path d="M10.5 3.5a1.5 1.5 0 0 0-1.5-1H4A1.5 1.5 0 0 0 2.5 4v5A1.5 1.5 0 0 0 4 10.5" strokeLinecap="round" />
</svg>
);
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" className={className} fill="none" stroke="currentColor" strokeWidth={1.8} aria-hidden>
<path d="m3.5 8.5 3 3 6-7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
Installation
The CLI writes components/glyph-field-cta.tsx, the components it's built with (components/ui/glyph-field.tsx) and lib/utils.ts, and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.
npx shadcn@latest add @keyframeui/glyph-field-ctaThen put it where the section goes. The copy, links and data sit in constants at the top of the file, so making it yours starts there.
app/page.tsx
import { GlyphFieldCta } from "@/components/glyph-field-cta";
export default function Page() {
return <GlyphFieldCta />;
}
Built with
Each of these is a component on its own, with its own examples and props. The block installs them for you.