Skip to content

DirectorySkills

Dot face builder: 1440, made in this run
Run 1Dot face builder“Add a simple avatar maker to our kids' reading app sign-up so children don't upload photos. Details in brief.md.”
Runs in
  • Claude Code
  • Codex
  • Cursor
  • Gemini CLI
  • GitHub Copilot
  • OpenCode
Generates with
Your agent's model
Tested with
Claude Code 2.1.322Claude Sonnet 5.51 real run, 10 Nov
Needs
Node 20+Chrome
Licence
Recaply licence

Dot Face Builder

A playful avatar builder made of dots: pick a face, eyes, mouth, hair and colours, shuffle, then download a PNG or copy the SVG.

$9.99once, yours to keep

Builds a dot-grid avatar maker into your own project: a big round preview of a face drawn from a 14 by 14 grid of dots, with the same avatar shown small at 64, 40 and 24px; beside it option groups for face, eyes, mouth, hair and three colour rows of swatches; buttons to shuffle, download a PNG and copy the SVG. Plain HTML, CSS and JavaScript, or matched to React, Next.js, Vue, Svelte, Astro or Tailwind when the project uses them. Comes with a working reference and a check that screenshots it at phone, tablet and desktop widths and flags sideways overflow, text under 12px, missing alt text and names, low contrast, no focus styles, motion that ignores reduced motion, leftover sample words and emoji icons. Use when a community, game, school, workplace tool or sign-up flow wants people to make a friendly avatar without uploading a photo, or someone asks for an avatar builder, avatar generator or pixel avatar maker.

By RecaplyUpdated 12 Nov

Install Dot Face Builder

After the download, ask your agent to add it, or run the line yourself.

Read https://recaply.co/skills/dot-face-builder/install.md and follow it to install the dot-face-builder skill from dot-face-builder.zip.

Paste it into your agent. It reads our install steps, finds the ZIP you downloaded and adds the skill for you.

New to skills? How to install a skill

Similar skills

All 735 skills

Who Dot Face Builder is for

What you get

  • One HTML file with its CSS inside: no build step, no framework
  • A dot-grid avatar maker
  • Checked at phone and desktop widths

Good fit

  • When a community, game, school, workplace tool or sign-up flow wants people to make a friendly avatar without uploading a photo

Not for

  • An app with a backend, sign-in or a database
  • Copying another company's site

About this skill

You say

Add a simple avatar maker to our kids' reading app sign-up so children don't upload photos. Details in brief.md.

It said back

I added the avatar maker as a plain HTML, CSS and JS page in signup/. It passes the skill's check at 390, 768 and 1440px (no sideways scrolling, text over 12px, alt text and names, contrast, focus styles, reduced motion, no script errors). I viewed the 390, 768 and 1440 screenshots and the ears now sit fully inside the circle.

There was no existing project in the folder, only brief.md, so index.html is a stand-in sign-up page with a placeholder heading and no real fields.

Show more

What it did in run 1

  1. dot-face-builder
  2. ./brief.md
  3. *
  4. ./.claude/skills/dot-face-builder/reference/index.html
  5. ./.claude/skills/dot-face-builder/reference/script.js
  6. ./.claude/skills/dot-face-builder/reference/style.css
  7. mkdir -p ./signup && cd ./signup && python3 - <<'EOF' def chips(name, items, checked): return '<div class="dm-chips">' + ''.join( f'<label class="chip"><input type="radio" name="{name}" value="{v}"{" checked" if v==checked else ""}><span>{l}</span></label>' for v,l in items) + '</div>' def sws(name, items, checked): return '<div class="dm-sw">' + ''.join( f'<label class="sw" style="--c:{v}"><input type="radio" name="{name}" value="{v}" aria-label="{l}"{" checked" if v==checked else ""}><span></span></label>' for v,l in items) + '</div>' def fs(t, body): return f'<fieldset><legend>{t}</legend>{body}</fieldset>' fur=[("#F6E0C4","Cream"),("#E8B48C","Peach"),("#E39A4B","Ginger"),("#B9784A","Caramel"),("#6B4129","Cocoa"),("#C9C6D1","Cloud grey")] bg=[("#FFE3EC","Blush"),("#E2F5EC","Mint"),("#E6EBFF","Lilac"),("#FFF0C7","Butter"),("#DDF1FB","Sky"),("#FFD9C2","Apricot")] ctl=''.join([ fs("Ears",chips("ears",[("cat","Cat"),("bear","Bear"),("bunny","Bunny"),("none","None")],"cat")), fs("Eyes",chips("eyes",[("dots","Dots"),("wide","Wide"),("wink","Wink")],"dots")), fs("Mouth",chips("mouth",[("smile","Smile"),("grin","Grin"),("flat","Flat"),("oh","Oh")],"smile")), fs("Extras",chips("acc",[("none","None"),("glasses","Glasses"),("hat","Hat"),("bow","Bow")],"none")), fs("Fur colour",sws("fur",fur,"#E8B48C")), fs("Background",sws("bg",bg,"#E2F5EC"))]) html=f'''<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Join Bookworm Club</title> <meta name="description" content="Join Bookworm Club: make your own animal avatar. No photo needed."> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&display=swap"> <link rel="stylesheet" href="avatar.css"> </head> <body> <a class="skip" href="#main">Skip to content</a> <main id="main"> <!-- PLACEHOLDER: replace this heading and the sign-up form fields with the real Bookworm Club sign-up. Keep the hidden "avatar" input and the data-slot preview. --> <form class="p-sec wrap" id="signup" action="#" method="post"> <h1 class="page-h">Join Bookworm Club</h1> <div class="su-avatar"><img data-slot width="72" height="72" alt="" hidden><p data-slot-text>You have not made your avatar yet.</p></div> <input type="hidden" name="avatar" data-avatar-input> <!-- PLACEHOLDER sign-up fields (name, grown-up's email, etc.) go here --> </form> <section class="p-sec dm" id="maker" aria-labelledby="maker-h"><div class="sec-head"><h2 id="maker-h">Make your <em>animal friend</em></h2><p class="sub">Pick ears, eyes and colours. No photo needed!</p></div><div class="wrap dm-in"><div class="dm-prev"><div class="dm-big" role="img" aria-label="Your avatar" data-big></div><div class="dm-smalls" aria-hidden="true"><span data-s="64"></span><span data-s="40"></span><span data-s="24"></span></div><p class="dm-desc" aria-live="polite" data-desc></p></div><div class="dm-ctl" data-form role="group" aria-label="Avatar choices">{ctl}<div class="dm-acts"><button class="btn btn-ghost" type="button" data-shuffle>Surprise me!</button><button class="btn btn-primary btn-big" type="button" data-save>Save my avatar</button></div><p class="dm-status" aria-live="polite" data-status></p></div></div></section> </main> <script src="avatar.js" defer></script> </body> </html> ''' open("index.html","w").write(html) EOF cat > avatar.css <<'EOF' :root{--bg:#FFF9F2;--surface:#FFFFFF;--ink:#2A2340;--muted:#5E5672;--line:#EDE3D6;--accent:#6D4AFF;--on-accent:#FFFFFF;--em:#5B3BEA;--font:'Fredoka',system-ui,sans-serif} *,*::before,*::after{box-sizing:border-box} html{-webkit-text-size-adjust:100%} body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--font);font-size:1.0625rem;line-height:1.6} svg,img{max-width:100%} h1,h2{font-family:var(--font);line-height:1.1;margin:0;text-wrap:balance;font-weight:700} p{margin:0} button,input{font:inherit;color:inherit} :focus-visible{outline:3px solid var(--accent);outline-offset:3px;border-radius:4px} .wrap{width:min(100% - 40px,1180px);margin-inline:auto} .skip{position:absolute;left:-9999px;top:12px;z-index:100;background:var(--ink);color:var(--bg);padding:.6rem 1rem;border-radius:8px} .skip:focus{left:12px} .p-sec{padding-block:clamp(32px,6vw,72px)} .page-h{font-size:clamp(2rem,5vw,3.2rem);letter-spacing:-.02em} .su-avatar{display:flex;align-items:center;gap:1rem;margin-top:1.2rem} .su-avatar img{border-radius:50%;width:72px;height:72px} .sec-head{width:min(100% - 40px,1180px);margin:0 auto clamp(24px,3.6vw,44px)} .sec-head h2{font-size:clamp(1.9rem,4vw,3rem);letter-spacing:-.02em} .sec-head h2 em{font-style:italic;color:var(--em)} .sec-head .sub{color:var(--muted);margin-top:.6rem;max-width:60ch;font-size:1.1rem} .btn{display:inline-flex;align-items:center;justify-content:center;min-height:64px;padding:.7em 1.6em;border-radius:999px;font-weight:600;font-size:1.15rem;line-height:1.2;border:2px solid var(--ink);background:transparent;cursor:pointer} .btn-primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)} .btn-big{flex:1 1 240px;min-height:72px;font-size:1.3rem} .dm-in{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);gap:clamp(24px,4vw,56px);align-items:start} .dm-prev{position:sticky;top:20px;background:var(--surface);border:1px solid var(--line);border-radius:28px;padding:clamp(18px,3vw,32px);text-align:center} .dm-big{width:min(340px,100%);aspect-ratio:1;margin-inline:auto;border-radius:50%;overflow:hidden} .dm-big svg,.dm-smalls svg{width:100%;height:100%;display:block} .dm-smalls{display:flex;justify-content:center;align-items:flex-end;gap:16px;margin-top:1.2rem} .dm-smalls span{border-radius:50%;overflow:hidden;display:block} .dm-smalls [data-s="64"]{width:64px;height:64px}.dm-smalls [data-s="40"]{width:40px;height:40px}.dm-smalls [data-s="24"]{width:24px;height:24px} .dm-desc{margin-top:1rem;color:var(--muted)} .dm-ctl{display:grid;gap:1.3rem} .dm-ctl fieldset{border:0;margin:0;padding:0;min-width:0} .dm-ctl legend{font-weight:600;font-size:1.1rem;margin-bottom:.6rem;padding:0} .dm-chips,.dm-sw{display:flex;flex-wrap:wrap;gap:.6rem} .chip,.sw{position:relative} .chip input,.sw input{position:absolute;inset:0;opacity:0;width:100%;height:100%;margin:0;cursor:pointer} .chip span{display:inline-flex;align-items:center;justify-content:center;min-height:60px;min-width:84px;padding:0 1.3rem;border:2px solid var(--line);border-radius:999px;background:var(--surface);font-weight:500;font-size:1.1rem} .chip input:checked+span{background:var(--accent);border-color:var(--accent);color:var(--on-accent)} .chip input:focus-visible+span,.sw input:focus-visible+span{outline:3px solid var(--accent);outline-offset:3px} .chip:hover span{border-color:var(--ink)} .sw span{display:block;width:60px;height:60px;border-radius:50%;background:var(--c);box-shadow:inset 0 0 0 1px rgba(0,0,0,.15)} .sw input:checked+span{box-shadow:0 0 0 3px var(--bg),0 0 0 6px var(--ink)} .dm-sw{gap:.9rem} .dm-acts{display:flex;flex-wrap:wrap;gap:.8rem;margin-top:.4rem} .dm-status{min-height:1.6em;color:var(--muted)} .dot{transform-box:fill-box;transform-origin:center;animation:dmpop .15s ease} @keyframes dmpop{from{transform:scale(.6)}} @media (max-width:900px){.dm-in{grid-template-columns:1fr}.dm-prev{position:static}} @media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}} EOF cat > avatar.js <<'EOF' // Dot-grid animal avatar: 14 x 14 dots. "Save my avatar" hands a PNG back to the sign-up form (no download). const root = document.querySelector('[data-form]'), N = 14; const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); const get = (k) => root.querySelector('input[name="' + k + '"]:checked').value; const label = (k) => { const i = root.querySelector('input[name="' + k + '"]:checked'); return i.getAttribute('aria-label') || i.nextElementSibling.textContent; }; const opts = () => ({ ears: get('ears'), eyes: get('eyes'), mouth: get('mouth'), acc: get('acc'), fur: get('fur'), bg: get('bg') }); function cells(o) { const g = Array.from({ length: N }, () => new Array(N).fill(null)); const put = (x, y, c) => { if (x >= 0 && y >= 0 && x < N && y < N) g[y][x] = c; }; const both = (x, y, c) => { put(x, y, c); put(N - 1 - x, y, c); }; // ears first, so the face sits on top const ears = { cat: [[2, 1, 'fur'], [2, 2, 'fur'], [3, 2, 'inner'], [2, 3, 'fur'], [3, 3, 'inner'], [4, 3, 'fur']], bear: [[2, 2, 'fur'], [3, 2, 'fur'], [2, 3, 'fur'], [3, 3, 'inner']], bunny: [[3, 0, 'fur'], [4, 0, 'fur'], [3, 1, 'fur'], [4, 1, 'inner'], [3, 2, 'fur'], [4, 2, 'inner'], [3, 3, 'fur'], [4, 3, 'fur']], none: [] }[o.ears]; ears.forEach((p) => both(p[0], p[1], p[2])); for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (Math.pow((x - 6.5) / 4.4, 2) + Math.pow((y - 7.5) / 4.4, 2) <= 1) put(x, y, 'fur'); const ink = 'ink'; if (o.eyes === 'dots') { put(5, 7, ink); put(8, 7, ink); } else if (o.eyes === 'wide') { [4, 5, 8, 9].forEach((x) => put(x, 7, ink)); } else { put(5, 7, ink); put(8, 7, ink); put(9, 7, ink); } if (o.mouth === 'smile') { put(5, 10, ink); put(6, 11, ink); put(7, 11, ink); put(8, 10, ink); } else if (o.mouth === 'grin') { for (let x = 5; x <= 8; x++) put(x, 10, ink); put(6, 11, 'tongue'); put(7, 11, 'tongue'); } else if (o.mouth === 'flat') { for (let x = 5; x <= 8; x++) put(x, 10, ink); } else { put(6, 10, ink); put(7, 10, ink); put(6, 11, ink); put(7, 11, ink); } put(4, 9, 'cheek'); put(9, 9, 'cheek'); if (o.acc === 'glasses') { const f = (x, y) => { if (g[y][x] !== ink) put(x, y, 'frame'); }; [[4, 6], [5, 6], [4, 8], [5, 8], [3, 7], [6, 7]].forEach((p) => { f(p[0], p[1]); f(N - 1 - p[0], p[1]); }); f(6, 7); f(7, 7); } else if (o.acc === 'hat') { [[6, 0], [7, 0]].forEach((p) => put(p[0], p[1], 'pom')); for (let y = 1; y <= 3; y++) for (let x = 7 - y - 1; x <= 6 + y; x++) put(x, y, 'frame'); } else if (o.acc === 'bow') { [[9, 1], [9, 2], [9, 3], [11, 1], [11, 2], [11, 3]].forEach((p) => put(p[0], p[1], 'bow')); put(10, 2, 'knot'); } return g; } function svgFor(o, anim, size) { const g = cells(o), s = 10, col = { fur: o.fur, inner: '#F39AA8', ink: '#2A2340', tongue: '#E2606B', cheek: '#F39AA8', frame: '#6D4AFF', pom: '#FFD34D', bow: '#F0528A', knot: '#B8265F' }; let out = '<svg xmlns="http://www.w3.org/2000/svg"' + (size ? ' width="' + size + '" height="' + size + '"' : ' aria-hidden="true" focusable="false"') + ' viewBox="0 0 ' + N * s + ' ' + N * s + '"><rect width="' + N * s + '" height="' + N * s + '" fill="' + o.bg + '"/>'; for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (g[y][x]) out += '<circle' + (anim ? ' class="dot"' : '') + ' cx="' + (x * s + s / 2) + '" cy="' + (y * s + s / 2) + '" r="' + s * 0.41 + '" fill="' + col[g[y][x]] + '"/>'; return out + '</svg>'; } function describe() { const o = opts(); const extra = { none: '', glasses: ' wearing glasses', hat: ' wearing a party hat', bow: ' wearing a bow' }[o.acc]; return 'A ' + label('fur').toLowerCase() + ' animal with ' + (o.ears === 'none' ? 'no' : o.ears) + ' ears, ' + o.eyes + ' eyes and a ' + o.mouth + ' mouth' + extra + ', on a ' + label('bg').toLowerCase() + ' background.'; } const big = document.querySelector('[data-big]'), desc = document.querySelector('[data-desc]'), status = document.querySelector('[data-status]'); function update() { const o = opts(), d = describe(); big.innerHTML = svgFor(o, !reduceMotion.matches); document.querySelectorAll('[data-s]').forEach((el) => { el.innerHTML = svgFor(o, false); }); big.setAttribute('aria-label', 'Your avatar: ' + d); desc.textContent = d; status.textContent = ''; } root.addEventListener('change', update); document.querySelector('[data-shuffle]').addEventListener('click', () => { ['ears', 'eyes', 'mouth', 'acc', 'fur', 'bg'].forEach((k) => { const r = root.querySelectorAll('input[name="' + k + '"]'); r[Math.floor(Math.random() * r.length)].checked = true; }); update(); }); document.querySelector('[data-save]').addEventListener('click', () => { const o = opts(), d = describe(), img = new Image(), url = URL.createObjectURL(new Blob([svgFor(o, false, 256)], { type: 'image/svg+xml' })); img.onload = () => { const c = document.createElement('canvas'); c.width = c.height = 256; c.getContext('2d').drawImage(img, 0, 0, 256, 256); URL.revokeObjectURL(url); const png = c.toDataURL('image/png'); const input = document.querySelector('[data-avatar-input]'); if (input) input.value = png; const slot = document.querySelector('[data-slot]'); if (slot) { slot.src = png; slot.alt = 'Your avatar: ' + d; slot.hidden = false; } const t = document.querySelector('[data-slot-text]'); if (t) t.textContent = 'Your avatar is ready!'; document.dispatchEvent(new CustomEvent('avatar:save', { detail: { png: png, description: d, options: o } })); status.textContent = 'Saved! Your avatar is on the sign-up form.'; const f = document.getElementById('signup'); if (f) f.scrollIntoView({ behavior: reduceMotion.matches ? 'auto' : 'smooth' }); }; img.onerror = () => { status.textContent = 'Sorry, we could not save your avatar. Please try again.'; }; img.src = url; }); update(); EOF cd .. && node .claude/skills/dot-face-builder/scripts/check.mjs --ready; node .claude/skills/dot-face-builder/scripts/check.mjs signup/index.html --out check 2>&1 | tail -30
  8. ls signup; node .claude/skills/dot-face-builder/scripts/check.mjs --ready
  9. ./signup/avatar.css
  10. ./signup/avatar.js
  11. ./gen.py
  12. python3 gen.py && rm gen.py && node .claude/skills/dot-face-builder/scripts/check.mjs signup/index.html --out check 2>&1 | tail -30
  13. python3 gen.py
  14. ./signup/index.html
  15. rm -f gen.py && node .claude/skills/dot-face-builder/scripts/check.mjs signup/index.html --out check 2>&1 | tail -30
  16. ./check/1440.png
  17. ./check/390.png
  18. ./signup/avatar.js
  19. node .claude/skills/dot-face-builder/scripts/check.mjs signup/index.html --out check 2>&1 | tail -5
  20. ./check/768.png
  21. Done

10 Nov 2026 · Claude Code 2.1.322 with Claude Sonnet 5.5 · 2 min 50 s · $0.47 of API use

Questions

Is Dot Face Builder free?

No. Dot Face Builder costs $9.99 once and stays in your Recaply account, or it comes with the Recaply plan ($19.99 a month), which opens every premium skill and prompt.

Which agents can use Dot Face Builder?

Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.

What do I get when I buy Dot Face Builder?

A ZIP of the whole skill folder (33 KB): the SKILL.md your agent follows and its 6 other files, the references, templates and scripts it uses. You download it from this page whenever you need it, and install it in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode. The Recaply plan opens it too, with every other premium skill and prompt.

How do I install Dot Face Builder?

Buy it, then download the ZIP from this page. For Claude Code, unzip it into .claude/skills/ in your project. For Codex, Cursor and Gemini CLI, unzip it and run npx skills add ./dot-face-builder with your agent's flag.

What do I get from Dot Face Builder?

One HTML file with its CSS inside: no build step, no framework. A dot-grid avatar maker. Checked at phone and desktop widths.

What does Dot Face Builder need?

Node 20+ and Chrome, and no API key. Each use costs nothing beyond your agent.

How big is Dot Face Builder?

7 files, about 22,131 tokens in all. The SKILL.md is about 2,126 tokens, and your agent reads it only when the job fits.

What does a run cost?

Our 1 runs took 2 min 50 s to 2 min 50 s and $0.47 to $0.47 of API use each, in Claude Code with claude-sonnet-5-5. On a Claude plan, a run counts toward your usage instead.

Do I have to name Dot Face Builder to use it?

Type /dot-face-builder first to make sure it loads. Without it, Claude Code may judge a small job simple enough to do without the skill.