ClickClick generates PNG and JPEG social images by rendering HTML in headless Chromium with
Playwright. It can be used as a library or as the clickclick CLI.
The v1 surface is intentionally small:
- render user-authored HTML/CSS to PNG or JPEG;
- screenshot arbitrary URLs to PNG or JPEG;
- render built-in social image presets;
- opt in to text fitting for elements that should shrink to fit;
- report structured warnings and stable error codes.
It does not manage user preset registries or publish anything externally.
Use Node 20 or newer.
npm install @maurogoncalo/clickclick
npx playwright install chromiumIf Chromium cannot start, install the system dependencies requested by Playwright for your platform.
First-run CLI check:
npx clickclick preset list
npx clickclick preset solid --title "Hello from ClickClick" --out og.pngimport { renderImage, presets, screenshotUrl } from "@maurogoncalo/clickclick";
await renderImage({
document: {
html: "<main><h1 data-clickclick-fit>Hello</h1></main>",
css: "body{margin:0;background:#111;color:white}main{width:1200px;height:630px}",
},
output: { path: "og.png" },
});
await renderImage({
...presets.solid({
title: "Launch notes",
subtitle: "A concise social card",
backgroundColor: "#111827",
textColor: "#fff",
}),
output: { path: "solid.png" },
});
await screenshotUrl({
url: "https://www.anthropic.com/",
viewport: { width: 1200, height: 630 },
render: { fullPage: true, waitUntil: "networkidle" },
output: { path: "anthropic-home.png", format: "png" },
locale: "en-US",
});Use createRenderer() when rendering many images in one process. It reuses the browser while still
creating a fresh browser context for every render. You may also pass an externally managed Playwright
Browser; ClickClick will not close it.
Batch renders through one browser:
import { createRenderer, presets } from "@maurogoncalo/clickclick";
const renderer = await createRenderer();
try {
for (const name of ["launch", "docs", "release"]) {
await renderer.render({
...presets.solid({ title: name, subtitle: "Batch render" }),
output: { path: `dist/${name}.png` },
});
}
} finally {
await renderer.close();
}Use an externally managed Playwright browser when another process owns browser startup and shutdown:
import { chromium } from "playwright";
import { createRenderer, presets } from "@maurogoncalo/clickclick";
const browser = await chromium.launch();
const renderer = await createRenderer({ browser });
await renderer.render({
...presets.gradient({ title: "Managed browser" }),
output: { path: "managed-browser.png" },
});
await renderer.close();
await browser.close();Mutate or wait on the page immediately before capture:
await renderer.render({
document: { html: "<main><h1>Ready</h1></main>", css: "main{width:1200px;height:630px}" },
render: {
beforeScreenshot: async (page) => {
await page.locator("h1").evaluate((node) => { node.textContent = "Captured"; });
},
},
});Read the returned buffer when you do not pass output.path:
const result = await renderer.render({
...presets.quote({ quote: "Return a buffer, not a file." }),
output: { format: "png" },
});
console.log(result.buffer.length, result.format, result.width, result.height, result.path);
for (const warning of result.warnings) console.warn(warning.code, warning.message);Handle structured errors and text-fit warnings:
import { ClickClickError, renderImage } from "@maurogoncalo/clickclick";
try {
const result = await renderImage({
document: {
html: '<main><h1 class="title" data-clickclick-fit>Very long copy...</h1></main>',
css: "main{width:400px;height:200px}.title{font-size:96px;max-height:90px;overflow:hidden}",
},
fitText: [{ selector: ".title", minFontSize: 24, maxFontSize: 96, onOverflow: "warn" }],
});
for (const warning of result.warnings) {
if (warning.code === "TEXT_FIT_OVERFLOW") console.warn(warning.selector);
}
} catch (error) {
if (error instanceof ClickClickError && error.code === "INVALID_INPUT") {
console.error(error.message);
}
}Render an HTML file:
clickclick render ./examples/card.html --css ./examples/card.css --out og.pngScreenshot a URL:
clickclick screenshot-url https://www.anthropic.com/ --out anthropic-home.png --width 1200 --height 630Generate a built-in preset:
clickclick preset gradient --title "Hello" --subtitle "From ClickClick" --out og.pngList presets:
clickclick preset listCommon render flags include --width, --height, --format, --quality, --selector,
--wait-until, --delay, --omit-background, and --strict. URL screenshots also support
--full-page, --omit-background, and --locale. --out and --output are aliases.
Use JPEG when you need smaller opaque images:
clickclick render examples/card.html \
--css examples/card.css \
--out examples/use-cases/jpeg-quality.jpg \
--width 1200 \
--height 630 \
--format jpeg \
--quality 82import { readFile } from "node:fs/promises";
import { renderImage } from "@maurogoncalo/clickclick";
await renderImage({
document: {
html: await readFile("examples/card.html", "utf8"),
css: await readFile("examples/card.css", "utf8"),
},
viewport: { width: 1200, height: 630 },
output: { path: "examples/use-cases/jpeg-quality.jpg", format: "jpeg", quality: 82 },
});Use selector capture when the page contains a larger layout but the image should include only one element:
clickclick render examples/card.html \
--css examples/card.css \
--selector main \
--out examples/use-cases/selector-card.png \
--width 1200 \
--height 630await renderImage({
document: {
html: await readFile("examples/card.html", "utf8"),
css: await readFile("examples/card.css", "utf8"),
},
viewport: { width: 1200, height: 630 },
render: { selector: "main" },
output: { path: "examples/use-cases/selector-card.png" },
});Use transparent PNG output when the document background should stay transparent:
clickclick render examples/use-cases/transparent-card.html \
--css examples/use-cases/transparent-card.css \
--out examples/use-cases/transparent-card.png \
--width 1200 \
--height 630 \
--omit-backgroundawait renderImage({
document: {
html: await readFile("examples/use-cases/transparent-card.html", "utf8"),
css: await readFile("examples/use-cases/transparent-card.css", "utf8"),
},
viewport: { width: 1200, height: 630 },
output: { path: "examples/use-cases/transparent-card.png", omitBackground: true },
});URL screenshots support selector-only capture, full-page capture, wait events, render delays, and locale:
clickclick screenshot-url https://www.anthropic.com/ \
--selector main \
--out examples/use-cases/anthropic-home.png \
--width 1200 \
--height 630 \
--wait-until networkidle \
--delay 1000 \
--locale en-US
clickclick screenshot-url https://www.anthropic.com/ \
--full-page \
--out examples/use-cases/anthropic-home.png \
--width 1200 \
--height 630import { screenshotUrl } from "@maurogoncalo/clickclick";
await screenshotUrl({
url: "https://www.anthropic.com/",
viewport: { width: 1200, height: 630 },
render: { selector: "main", waitUntil: "networkidle", delayMs: 1000 },
output: { path: "examples/use-cases/anthropic-home.png" },
locale: "en-US",
});
await screenshotUrl({
url: "https://www.anthropic.com/",
viewport: { width: 1200, height: 630 },
render: { fullPage: true },
output: { path: "examples/use-cases/anthropic-home.png" },
});These examples cover the workflows ClickClick is meant to make scriptable: live website screenshots, custom HTML cards, template modifications, and config-driven image sets.
Use URL screenshots when you need a reproducible image of a product page, docs page, landing page, or public status page.
CLI:
clickclick screenshot-url https://www.anthropic.com/ \
--out examples/use-cases/anthropic-home.png \
--width 1200 \
--height 630 \
--wait-until networkidle \
--delay 1000Library:
import { screenshotUrl } from "@maurogoncalo/clickclick";
await screenshotUrl({
url: "https://www.anthropic.com/",
viewport: { width: 1200, height: 630 },
render: { waitUntil: "networkidle", delayMs: 1000 },
output: { path: "examples/use-cases/anthropic-home.png" },
locale: "en-US",
});Result:
Use custom HTML/CSS when presets are too constrained, but keep data-clickclick-fit on important
copy that needs to survive variable titles.
CLI:
clickclick render examples/use-cases/fit-text-card.html \
--css examples/use-cases/fit-text-card.css \
--out examples/use-cases/fit-text-card.png \
--width 1200 \
--height 630Library:
import { readFile } from "node:fs/promises";
import { renderImage } from "@maurogoncalo/clickclick";
await renderImage({
document: {
html: await readFile("examples/use-cases/fit-text-card.html", "utf8"),
css: await readFile("examples/use-cases/fit-text-card.css", "utf8"),
},
viewport: { width: 1200, height: 630 },
output: { path: "examples/use-cases/fit-text-card.png" },
});Result:
Attribute-based fitting can set a minimum font size and overflow behavior:
<h1
data-clickclick-fit
data-clickclick-min-font-size="28"
data-clickclick-on-overflow="warn"
>
A launch title that may be much longer than expected
</h1>Programmatic fitting is useful when you cannot edit the source HTML:
import { renderImage } from "@maurogoncalo/clickclick";
const result = await renderImage({
document: {
html: '<main><h1 class="headline">A launch title that may be much longer than expected</h1></main>',
css: "main{width:1200px;height:630px}.headline{width:760px;max-height:220px;font-size:96px;overflow:hidden}",
},
fitText: [
{
selector: ".headline",
minFontSize: 28,
maxFontSize: 96,
onOverflow: "warn",
},
],
});
for (const warning of result.warnings) {
if (warning.code === "TEXT_FIT_OVERFLOW") {
console.warn(`${warning.selector} overflowed at ${warning.minFontSize}px`);
}
}Deliberately long copy can produce a structured warning:
const result = await renderImage({
document: {
html: '<main><h1 class="headline">This headline is intentionally far too long for the available card area and should warn</h1></main>',
css: "main{width:420px;height:180px}.headline{width:360px;max-height:80px;font-size:72px;overflow:hidden}",
},
fitText: [{ selector: ".headline", minFontSize: 32, onOverflow: "warn" }],
});
console.log(result.warnings.map((warning) => warning.code));The CLI --strict flag turns renderer warnings into a non-zero exit:
clickclick render examples/use-cases/fit-text-card.html \
--css examples/use-cases/fit-text-card.css \
--out examples/use-cases/fit-text-card.png \
--strictText fitting changes only font size. It does not rewrite text, change the box size, adjust line-height, or remove transforms; size the containing element for the longest copy you intend to support.
Use local templates when you want a reusable art direction with data-driven layer changes. Layers are
selected with data-layer attributes in the HTML.
CLI:
clickclick template examples/use-cases/product-card.html \
--css examples/use-cases/product-card.css \
--modify-json '[{"name":"label","text":"Template"},{"name":"title","text":"Modify image layers from JSON"},{"name":"subtitle","text":"Change copy, colors, effects, and visibility while the HTML stays reusable."},{"name":"cta","text":"Ship the asset"},{"name":"panel","background":"#312e81","shadow":"0 28px 80px rgba(15,23,42,.35)"},{"name":"badge","text":"JSON"}]' \
--out examples/use-cases/template-modifications.png \
--width 1200 \
--height 630Library:
import { renderTemplate } from "@maurogoncalo/clickclick";
await renderTemplate({
htmlPath: "examples/use-cases/product-card.html",
cssPath: "examples/use-cases/product-card.css",
modifications: [
{ name: "label", text: "Template" },
{ name: "title", text: "Modify image layers from JSON" },
{
name: "subtitle",
text: "Change copy, colors, effects, and visibility while the HTML stays reusable.",
},
{ name: "cta", text: "Ship the asset" },
{ name: "panel", background: "#312e81", shadow: "0 28px 80px rgba(15,23,42,.35)" },
{ name: "badge", text: "JSON" },
],
viewport: { width: 1200, height: 630 },
output: { path: "examples/use-cases/template-modifications.png" },
});Result:
Use recipes when the template, output size, and standard modifications should live in a project config instead of a shell script.
CLI:
clickclick config recipe examples/use-cases/clickclick.config.json releaseLibrary:
import { renderRecipe } from "@maurogoncalo/clickclick";
await renderRecipe("examples/use-cases/clickclick.config.json", "release");Result:
Use template sets when one source template needs to produce several assets for different surfaces.
CLI:
clickclick config set examples/use-cases/clickclick.config.json social \
--out-dir examples/use-cases/config-setLibrary:
import { renderTemplateSet } from "@maurogoncalo/clickclick";
await renderTemplateSet(
"examples/use-cases/clickclick.config.json",
"social",
"examples/use-cases/config-set",
);Results:
Templates are normal HTML and CSS. Mark editable elements with data-layer names, then render them
directly or apply Bannerbear-style local modifications.
CLI:
clickclick template ./examples/card.html \
--css ./examples/card.css \
--modify-json '[{"name":"title","text":"Local launch"},{"name":"card","background":"#f8fafc"}]' \
--out examples/templates/local-template.pngLibrary:
import { renderTemplate } from "@maurogoncalo/clickclick";
await renderTemplate({
html: '<main data-layer="card"><h1 data-layer="title">Old</h1></main>',
css: "main{width:1200px;height:630px;background:white}",
modifications: [
{ name: "title", text: "Local launch", color: "#111827", alignment: "center" },
{ name: "card", background: "#f8fafc", shadow: "0 24px 80px rgba(15,23,42,.18)" },
],
output: { path: "template.png" },
});Result:
Use --modify-file when layer updates are easier to review as JSON:
clickclick template examples/use-cases/image-template.html \
--css examples/use-cases/image-template.css \
--modify-file examples/use-cases/template-modifications.json \
--out examples/use-cases/image-template.png \
--width 1200 \
--height 630 \
--strictLibrary:
import { readFile } from "node:fs/promises";
import { renderTemplate } from "@maurogoncalo/clickclick";
await renderTemplate({
htmlPath: "examples/use-cases/image-template.html",
cssPath: "examples/use-cases/image-template.css",
modifications: JSON.parse(await readFile("examples/use-cases/template-modifications.json", "utf8")),
viewport: { width: 1200, height: 630 },
output: { path: "examples/use-cases/image-template.png" },
});Result:
The JSON file updates an image layer with src, fit, anchor, and effect; it also demonstrates
text updates, style, attributes, x, y, border, shadow, and visibility-compatible layer
fields:
[
{ "name": "hero", "src": "examples/presets/photo-source.svg", "fit": "cover", "anchor": "center", "effect": "grayscale" },
{ "name": "badge", "text": "STRICT", "x": -24, "y": -18, "border": "3px solid #111827" }
]Register a custom font from the CLI or library/config:
clickclick template examples/use-cases/image-template.html \
--css examples/use-cases/image-template.css \
--font "Inter=./fonts/Inter.woff2" \
--modify-file examples/use-cases/template-modifications.json \
--out image-template.pngawait renderTemplate({
htmlPath: "examples/use-cases/image-template.html",
cssPath: "examples/use-cases/image-template.css",
fonts: [{ family: "Inter", source: "./fonts/Inter.woff2" }],
});Choose warning behavior explicitly while developing templates:
clickclick template examples/use-cases/image-template.html \
--css examples/use-cases/image-template.css \
--modify-json '[{"name":"missing","text":"No layer"}]' \
--on-missing-layer warn
clickclick template examples/use-cases/image-template.html \
--css examples/use-cases/image-template.css \
--modify-json '[{"name":"missing","text":"No layer"}]' \
--on-missing-layer error \
--strictDebug bundles write the rendered HTML, CSS, and manifest:
clickclick template examples/use-cases/image-template.html \
--css examples/use-cases/image-template.css \
--modify-file examples/use-cases/template-modifications.json \
--debug-dir .clickclick-debug \
--out image-template.pngSupported modification fields include text, html, src, image_url, color, background,
font_family, alignment, hide, show, style, className, attributes, x, y, border,
shadow, fit, anchor, and effect. Effects are CSS-only: grayscale, sepia, blur,
grayscale-blur, flip-horizontal, flip-vertical, invert, and negate.
Register fonts with --font "Family=./font.woff2" in the CLI or with fonts in the library/config
API. ClickClick injects @font-face rules before capture and waits for browser font readiness.
Use a JSON config file to register reusable templates, named recipes, and multi-output sets:
{
"templates": {
"card": { "htmlPath": "./examples/card.html", "cssPath": "./examples/card.css" }
},
"recipes": {
"launch": {
"template": "card",
"output": { "path": "launch.png", "width": 1200, "height": 630 },
"modifications": [{ "name": "title", "text": "Launch" }]
}
},
"templateSets": {
"social": [
{ "name": "square", "template": "card", "output": { "width": 1080, "height": 1080 } },
{ "name": "wide", "template": "card", "output": { "width": 1200, "height": 630 } }
]
}
}clickclick config templates ./clickclick.config.json
clickclick config recipe ./clickclick.config.json launch
clickclick config set ./clickclick.config.json social --out-dir ./dist/images
clickclick preview ./examples/card.html --css ./examples/card.css --watchAdd --debug-dir ./debug-render to template or recipe renders to write the source HTML/CSS and a
manifest with modifications and warnings.
Generate a one-shot preview while editing an HTML/CSS template:
clickclick preview examples/use-cases/product-card.html \
--css examples/use-cases/product-card.css \
--out-dir .clickclick-preview \
--width 1200 \
--height 630The preview command writes .clickclick-preview/preview.png. Use watch mode for the local authoring
loop:
clickclick preview examples/use-cases/product-card.html \
--css examples/use-cases/product-card.css \
--out-dir .clickclick-preview \
--watchList templates registered in a config file:
clickclick config templates examples/use-cases/clickclick.config.jsonExpected output:
product
Render a recipe with CLI overrides:
clickclick config recipe examples/use-cases/clickclick.config.json release \
--modify-json '[{"name":"title","text":"Recipe override"},{"name":"badge","text":"CLI"}]' \
--out examples/use-cases/config-recipe.png \
--width 1200 \
--height 630Render a template set into an output directory:
clickclick config set examples/use-cases/clickclick.config.json social \
--out-dir examples/use-cases/config-setResults:
Library equivalents:
import { listConfigTemplates, renderRecipe, renderTemplateSet } from "@maurogoncalo/clickclick";
const templates = await listConfigTemplates("examples/use-cases/clickclick.config.json");
await renderRecipe("examples/use-cases/clickclick.config.json", "release", {
modifications: [
{ name: "title", text: "Recipe override" },
{ name: "badge", text: "API" },
],
viewport: { width: 1200, height: 630 },
output: { path: "examples/use-cases/config-recipe.png" },
});
await renderTemplateSet(
"examples/use-cases/clickclick.config.json",
"social",
"examples/use-cases/config-set",
);
console.log(templates);The npm package is prepared as @maurogoncalo/clickclick with MIT licensing, public npm access,
provenance-enabled publishing, a clickclick binary, ESM library exports, and TypeScript
declarations.
Before publishing, run the local release check:
npm run release:checkThis runs type-checking, tests, a production build, and a package dry-run verification that checks the built CLI, library entry points, declaration files, README, and license. To inspect npm's packed file list without the full check:
npm run pack:dryPublishing is intentionally manual. See RELEASE.md for the confirmation checklist
and the GitHub Actions workflow steps. Do not publish from routine CI.
ClickClick currently ships these built-in presets. Keep this list in sync with the exported
presets object and the CLI preset commands.
A branded announcement image with title, subtitle, CTA, a corner logo, and a faint logo watermark.
CLI:
clickclick preset brand-announcement \
--title "New partner program" \
--subtitle "Reusable branded cards with logo marks." \
--cta "Apply today" \
--logo examples/presets/clickclick-logo.svg \
--watermark examples/presets/clickclick-logo.svg \
--out examples/presets/brand-announcement.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.brandAnnouncement({
title: "New partner program",
subtitle: "Reusable branded cards with logo marks.",
cta: "Apply today",
logo: { src: "examples/presets/clickclick-logo.svg", placement: "top-right" },
watermark: { src: "examples/presets/clickclick-logo.svg", opacity: 0.08, scale: 0.58 },
}),
output: { path: "examples/presets/brand-announcement.png" },
});Image options accept https:, data:, file:, absolute paths, and paths relative to the current
working directory. Local paths are inlined as data URLs before Chromium renders the image.
Result:
A centered headline over a large logo or text watermark backdrop.
CLI:
clickclick preset logo-backdrop \
--title "Brand assets in seconds" \
--meta "Backdrop" \
--watermark-text "CLICK" \
--out examples/presets/logo-backdrop.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.logoBackdrop({
title: "Brand assets in seconds",
meta: "Backdrop",
watermark: { text: "CLICK" },
}),
output: { path: "examples/presets/logo-backdrop.png" },
});Result:
A two-logo card for integrations, partnerships, and co-marketing announcements.
CLI:
clickclick preset partner-card \
--title "ClickClick + Acme" \
--partner-name "Integration" \
--logo examples/presets/clickclick-logo.svg \
--partner-logo examples/presets/photo-source.svg \
--out examples/presets/partner-card.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.partnerCard({
title: "ClickClick + Acme",
partnerName: "Integration",
logo: { src: "examples/presets/clickclick-logo.svg" },
partnerLogo: "examples/presets/photo-source.svg",
}),
output: { path: "examples/presets/partner-card.png" },
});Result:
A branded quote card with a logo or text watermark behind the quote.
CLI:
clickclick preset watermark-quote \
--quote "Every launch asset now follows the brand." \
--attribution "ClickClick" \
--watermark-text "QUOTE" \
--out examples/presets/watermark-quote.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.watermarkQuote({
quote: "Every launch asset now follows the brand.",
attribution: "ClickClick",
watermark: { text: "QUOTE" },
}),
output: { path: "examples/presets/watermark-quote.png" },
});Result:
An announcement card with a repeated logo or badge pattern behind foreground copy.
CLI:
clickclick preset badge-grid \
--title "Hiring across product" \
--subtitle "Repeatable badge backgrounds for announcements." \
--badge "Hiring" \
--badge-logo examples/presets/clickclick-logo.svg \
--out examples/presets/badge-grid.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.badgeGrid({
title: "Hiring across product",
subtitle: "Repeatable badge backgrounds for announcements.",
badge: "Hiring",
badgeLogo: "examples/presets/clickclick-logo.svg",
}),
output: { path: "examples/presets/badge-grid.png" },
});Result:
A colorful gradient social image with title, optional subtitle, optional label, configurable
gradient colors, text color, accent color, alignment, size, and font family. It uses
data-clickclick-fit text fitting for title and subtitle.
CLI:
clickclick preset gradient \
--title "Launch faster" \
--subtitle "Colorful social cards from HTML and CSS" \
--label "Preset" \
--from "#0f766e" \
--to "#7c3aed" \
--accent "rgba(255,255,255,0.32)" \
--align center \
--out examples/presets/gradient.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.gradient({
title: "Launch faster",
subtitle: "Colorful social cards from HTML and CSS",
label: "Preset",
fromColor: "#0f766e",
toColor: "#7c3aed",
accentColor: "rgba(255,255,255,0.32)",
align: "center",
}),
output: { path: "examples/presets/gradient.png" },
});Result:
A full-bleed photo-backed hero card with a readable overlay, title, subtitle, label, and optional logo corner.
CLI:
clickclick preset photo-hero \
--title "Launch visuals that feel alive" \
--subtitle "Photo-forward cards with readable overlays and logo corners." \
--label "Photo" \
--image examples/presets/photo-source.svg \
--logo examples/presets/clickclick-logo.svg \
--out examples/presets/photo-hero.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.photoHero({
title: "Launch visuals that feel alive",
subtitle: "Photo-forward cards with readable overlays and logo corners.",
label: "Photo",
image: "examples/presets/photo-source.svg",
logo: { src: "examples/presets/clickclick-logo.svg" },
}),
output: { path: "examples/presets/photo-hero.png" },
});Result:
A magazine-style feature layout with a cropped media panel, headline, kicker, and byline.
CLI:
clickclick preset editorial-feature \
--title "Designing with local images" \
--kicker "Editorial" \
--byline "ClickClick Magazine" \
--image examples/presets/photo-source.svg \
--out examples/presets/editorial-feature.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.editorialFeature({
title: "Designing with local images",
kicker: "Editorial",
byline: "ClickClick Magazine",
image: "examples/presets/photo-source.svg",
}),
output: { path: "examples/presets/editorial-feature.png" },
});Result:
An event or launch poster with image backdrop, date block, metadata, CTA, and optional logo.
CLI:
clickclick preset event-poster \
--title "Summer Launch" \
--date "Jul 16" \
--meta "Online" \
--cta "Register now" \
--image examples/presets/photo-source.svg \
--logo examples/presets/clickclick-logo.svg \
--out examples/presets/event-poster.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.eventPoster({
title: "Summer Launch",
date: "Jul 16",
meta: "Online",
cta: "Register now",
image: "examples/presets/photo-source.svg",
logo: { src: "examples/presets/clickclick-logo.svg" },
}),
output: { path: "examples/presets/event-poster.png" },
});Result:
An image-backed customer story card with customer label, quote, metric, logo, and overlay controls.
CLI:
clickclick preset case-study \
--title "Acme ships images faster" \
--customer "Acme" \
--quote "We replaced hand-made cards with one script." \
--metric "42% faster" \
--image examples/presets/photo-source.svg \
--logo examples/presets/clickclick-logo.svg \
--out examples/presets/case-study.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.caseStudy({
title: "Acme ships images faster",
customer: "Acme",
quote: "We replaced hand-made cards with one script.",
metric: "42% faster",
image: "examples/presets/photo-source.svg",
logo: { src: "examples/presets/clickclick-logo.svg" },
}),
output: { path: "examples/presets/case-study.png" },
});Result:
A launch or event announcement image with title, optional subtitle, badge, meta line, CTA, configurable colors, size, and font family.
CLI:
clickclick preset announcement \
--title "Launch week starts now" \
--subtitle "Five focused updates for faster social image workflows." \
--badge "Event" \
--meta "July 2026" \
--cta "See the schedule" \
--out examples/presets/announcement.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.announcement({
title: "Launch week starts now",
subtitle: "Five focused updates for faster social image workflows.",
badge: "Event",
meta: "July 2026",
cta: "See the schedule",
}),
output: { path: "examples/presets/announcement.png" },
});Result:
A bold checkerboard-pattern social image with title, optional subtitle, optional label, configurable pattern color, text color, accent color, size, and font family.
CLI:
clickclick preset checkerboard \
--title "Make the update impossible to miss" \
--subtitle "High-contrast cards for launches and calls for attention." \
--label "New" \
--out examples/presets/checkerboard.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.checkerboard({
title: "Make the update impossible to miss",
subtitle: "High-contrast cards for launches and calls for attention.",
label: "New",
}),
output: { path: "examples/presets/checkerboard.png" },
});Result:
A two-column before-and-after image with optional heading, configurable panel colors, text color, accent color, size, and font family.
CLI:
clickclick preset compare \
--title "Before and after the preset pass" \
--before-title "Before" \
--before-text "One solid card" \
--after-title "After" \
--after-text "Nine documented presets" \
--out examples/presets/compare.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.compare({
title: "Before and after the preset pass",
beforeTitle: "Before",
beforeText: "One solid card",
afterTitle: "After",
afterText: "Nine documented presets",
}),
output: { path: "examples/presets/compare.png" },
});Result:
A minimal editorial image with title, optional subtitle, optional metadata, configurable colors, alignment, size, and font family.
CLI:
clickclick preset minimal \
--title "Readable social cards without decoration" \
--subtitle "A quiet preset for articles, docs, and product notes." \
--meta "Design note" \
--accent "#111827" \
--out examples/presets/minimal.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.minimal({
title: "Readable social cards without decoration",
subtitle: "A quiet preset for articles, docs, and product notes.",
meta: "Design note",
accentColor: "#111827",
}),
output: { path: "examples/presets/minimal.png" },
});Result:
An editorial quote image with large quote text, optional attribution, optional source, configurable quote mark, alignment, background, text, accent color, size, and font family.
CLI:
clickclick preset quote \
--quote "Small tools should still feel carefully made." \
--attribution "ClickClick" \
--source "Preset gallery" \
--mark ">>" \
--align center \
--out examples/presets/quote.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.quote({
quote: "Small tools should still feel carefully made.",
attribution: "ClickClick",
source: "Preset gallery",
mark: ">>",
align: "center",
}),
output: { path: "examples/presets/quote.png" },
});Result:
A solid-background social image with title, optional subtitle, optional label, configurable colors,
accent color, size, font family, and left or center alignment. It uses the same
data-clickclick-fit text-fitting mechanism available to user-authored HTML.
CLI:
clickclick preset solid \
--title "Launch notes" \
--subtitle "A concise social card" \
--label "Update" \
--background "#111827" \
--text-color "#ffffff" \
--accent "#2563eb" \
--out examples/presets/solid.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.solid({
title: "Launch notes",
subtitle: "A concise social card",
label: "Update",
backgroundColor: "#111827",
textColor: "#ffffff",
accentColor: "#2563eb",
}),
output: { path: "examples/presets/solid.png" },
});Result:
A split-layout social image with text on the left and a bold graphic panel on the right. It supports an optional label, subtitle, panel color, background color, text color, accent color, panel side, size, and font family.
CLI:
clickclick preset split \
--title "Ship a sharper changelog" \
--subtitle "Readable layouts for posts, releases, and product updates." \
--label "Release" \
--panel-side left \
--out examples/presets/split.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.split({
title: "Ship a sharper changelog",
subtitle: "Readable layouts for posts, releases, and product updates.",
label: "Release",
panelSide: "left",
}),
output: { path: "examples/presets/split.png" },
});Result:
A developer-focused social image with title, optional subtitle, command, prompt, optional output line, configurable page, terminal, text, command, accent colors, size, and font families.
CLI:
clickclick preset terminal \
--title "Automate image generation" \
--subtitle "Render social cards from scripts, docs, or CI." \
--prompt ">" \
--command "clickclick preset terminal --out og.png" \
--output-text "created og.png" \
--out examples/presets/terminal.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.terminal({
title: "Automate image generation",
subtitle: "Render social cards from scripts, docs, or CI.",
prompt: ">",
command: "clickclick preset terminal --out og.png",
output: "created og.png",
}),
output: { path: "examples/presets/terminal.png" },
});Result:
These examples use the same built-in presets with different option combinations. They are useful starting points when you want a different tone without writing custom HTML/CSS.
CLI:
clickclick preset gradient \
--title "API v2 is live" \
--subtitle "A sharper endpoint design with fewer moving parts." \
--label "Launch" \
--from "#be123c" \
--to "#f97316" \
--accent "rgba(255,255,255,0.35)" \
--align left \
--out examples/presets/gradient-launch.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.gradient({
title: "API v2 is live",
subtitle: "A sharper endpoint design with fewer moving parts.",
label: "Launch",
fromColor: "#be123c",
toColor: "#f97316",
accentColor: "rgba(255,255,255,0.35)",
align: "left",
}),
output: { path: "examples/presets/gradient-launch.png" },
});Result:
CLI:
clickclick preset minimal \
--title "What changed in the renderer" \
--subtitle "A short technical note for people maintaining image pipelines." \
--meta "Engineering" \
--align center \
--background "#f8fafc" \
--accent "#0f766e" \
--out examples/presets/minimal-article.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.minimal({
title: "What changed in the renderer",
subtitle: "A short technical note for people maintaining image pipelines.",
meta: "Engineering",
align: "center",
backgroundColor: "#f8fafc",
accentColor: "#0f766e",
}),
output: { path: "examples/presets/minimal-article.png" },
});Result:
CLI:
clickclick preset split \
--title "New dashboard filters" \
--subtitle "Pin saved views, compare segments, and scan fresher data." \
--label "Product" \
--panel-side right \
--background "#ffffff" \
--panel-color "#0f172a" \
--accent "#eab308" \
--out examples/presets/split-product.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.split({
title: "New dashboard filters",
subtitle: "Pin saved views, compare segments, and scan fresher data.",
label: "Product",
panelSide: "right",
backgroundColor: "#ffffff",
panelColor: "#0f172a",
accentColor: "#eab308",
}),
output: { path: "examples/presets/split-product.png" },
});Result:
CLI:
clickclick preset terminal \
--title "Install ClickClick" \
--subtitle "Generate social images from your release scripts." \
--prompt "$" \
--command "npm install @maurogoncalo/clickclick" \
--output-text "added 1 package" \
--accent "#38bdf8" \
--command-color "#ffffff" \
--out examples/presets/terminal-install.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.terminal({
title: "Install ClickClick",
subtitle: "Generate social images from your release scripts.",
prompt: "$",
command: "npm install @maurogoncalo/clickclick",
output: "added 1 package",
accentColor: "#38bdf8",
commandColor: "#ffffff",
}),
output: { path: "examples/presets/terminal-install.png" },
});Result:
CLI:
clickclick preset compare \
--title "Migration impact" \
--before-title "Old flow" \
--before-text "Manual screenshots" \
--after-title "New flow" \
--after-text "Scripted preset renders" \
--before-color "#fee2e2" \
--after-color "#dbeafe" \
--accent "#2563eb" \
--out examples/presets/compare-migration.pngLibrary:
import { presets, renderImage } from "@maurogoncalo/clickclick";
await renderImage({
...presets.compare({
title: "Migration impact",
beforeTitle: "Old flow",
beforeText: "Manual screenshots",
afterTitle: "New flow",
afterText: "Scripted preset renders",
beforeColor: "#fee2e2",
afterColor: "#dbeafe",
accentColor: "#2563eb",
}),
output: { path: "examples/presets/compare-migration.png" },
});Result:
ClickClick infers the format from output.path when possible. .jpg and .jpeg produce JPEG;
everything else defaults to PNG unless output.format is provided. JPEG supports quality. PNG
supports omitBackground for explicit transparency.
Add data-clickclick-fit to an element to let ClickClick reduce only its inline font-size until
the content fits its existing box:
<h1 data-clickclick-fit data-clickclick-min-font-size="28">Long title</h1>Programmatic targets are also supported through fitText. The fitter runs after page readiness,
fonts, delay, and the beforeScreenshot hook, but before the screenshot. It does not change text,
box size, line height, letter spacing, or transforms.
Overflow at the minimum size returns a TEXT_FIT_OVERFLOW warning by default. Use
data-clickclick-on-overflow="error" or a programmatic target with onOverflow: "error" to throw a
ClickClickError. The CLI prints warnings to stderr and exits zero unless --strict is set.
Known failures throw ClickClickError with stable codes:
INVALID_INPUTMISSING_SELECTORTEXT_FIT_OVERFLOWBROWSER_LAUNCH_FAILEDRENDER_FAILED
npm test
npm run check
npm run build
npm run pack:dryCI runs on pushes to main and on pull requests across Node 20, 22, and 24. Each run installs
dependencies, installs Playwright Chromium, typechecks, builds, runs tests, and verifies the npm
package contents with npm pack --dry-run.
Publishing to npm is handled by the Publish to npm GitHub Actions workflow. The publish workflow
checks out main, bumps package.json and package-lock.json to the next unused patch version when
the committed version already exists on npm or already has a git tag, verifies the npm package
contents, commits and tags that release version, and publishes it with provenance. It is manually
dispatched and requires the operator to confirm the package name before publishing. The workflow uses
npm trusted publishing with GitHub Actions OIDC, so npm must be configured with a trusted publisher
for:
- Package:
@maurogoncalo/clickclick - Repository:
mintyPT/clickclick - Workflow filename:
publish.yml - Allowed action:
npm publish
The publish job publishes with provenance:
npm publish --access public --provenanceBefore the workflow can publish, the npm package owner must authorize it as a trusted publisher:
npm install -g npm@latest
npm trust github @maurogoncalo/clickclick \
--repo mintyPT/clickclick \
--file publish.yml \
--allow-publish \
--yes
































