npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pictoguys

v0.2.0

Published

Tiny React library for procedural SVG characters. Seed in, character out.

Readme


What is this?

pictoguys makes cute little SVG characters out of thin air.

You hand it a number or a word. It hands you back a character: a colored body, some eyes, eyebrows, and (if you want one) a background tile. The same word always makes the exact same character, so "Bloop" is always Bloop, on every device, forever.

Think of it like a profile-picture generator, except:

  • the art is real vector SVG (crisp at any size, never blurry),
  • there are no image files to download (the parts are baked into the library),
  • and the little guys can blink, hop, breathe, and dance.

No design skills needed. You do not draw anything. You just pick a seed.

What is a "picto"?

A picto is one character. That is the whole vocabulary you need.

Every picto is built from a few parts that get mixed and recolored:

| Part | Choices | | ----------- | ---------------------------------------- | | body color | 10 hand-picked colors, plus blended ones | | body shape | 4 shapes | | eyes | single, double, or triple | | eye coloring| plain, two-tone, or rainbow | | background | 5 tiles |

Pick those yourself, or let a seed pick them for you. Either way you get a picto.

Install

npm install pictoguys

For React components, bring your own react (version 17 or newer). For SVG-only usage, import from pictoguys/core and React is not loaded. There are zero runtime dependencies.

| Import path | Use it for | | ----------- | ---------- | | pictoguys | React projects that want <Picto />, <PictoField />, plus the core helpers | | pictoguys/react | Only the single-picto React component <Picto /> and its props | | pictoguys/react-canvas | Only <PictoField /> (the canvas batch renderer) and its props | | pictoguys/canvas | The framework-agnostic batch renderer core, no React | | pictoguys/core | SVG strings, characters, presets, and catalog helpers without React | | pictoguys/rng | The tiny deterministic RNG only |

Your first picto

Drop this into any React component:

import { Picto } from 'pictoguys'

export default function App() {
  return <Picto seed="Bloop" size={120} />
}

That is it. You just rendered Bloop.

seed can be a word ("Bloop") or a number (7). Same seed, same picto, every single time. Change the word, get a different friend.

Meet some pictos

Here is what a handful of names look like. Try your own.

| | | | | | | |:-:|:-:|:-:|:-:|:-:|:-:| | | | | | | | | "Bloop" | "Mochi" | "Zorp" | "Waffle" | "Gizmo" | "Noodle" | | | | | | | | | "Tofu" | "Bubbles" | "Sprocket" | "Pickle" | "Goose" | "Wizard" |

Waffle and Bubbles got fancy multi-colored eyes. Lucky.

Three ways to make a picto

1. By name (a word). Great for usernames, emails, anything text.

<Picto seed="[email protected]" />

2. By number. Great when you just want "give me number 42".

<Picto seed={42} />

3. By hand. Want an exact look? Spell it out. Anything you leave out gets filled in for you.

<Picto
  config={{
    color: 'pink',     // 'blue' 'cian' 'gray' 'green' 'lime'
                       // 'orange' 'pink' 'purple' 'red' 'yellow'
    shape: 2,          // 1, 2, 3, or 4
    eyes: 'triple',    // 'single' | 'double' | 'triple'
    mode: 'triad',     // 'mono' (plain) | 'hetero' (two-tone) | 'triad' (rainbow)
    bg: 4,             // 1 to 5
  }}
/>

Using TypeScript? Import the config type for full autocomplete:

import type { CharConfig } from 'pictoguys'

const cfg: CharConfig = { color: 'blue', eyes: 'double' }

Constraints and branding (the fun part)

Here is the one rule that gives you total control. For every setting:

| You write | You get | | ------------------ | -------------------------------- | | nothing (omit it) | fully random (picked by the seed)| | one value | locked to that value | | an array of values | random, but only from that set |

Mix and match freely. Add a seed (like a user id) and the random parts become stable per person.

"Use my brand color on every avatar"

Set your brand once with picto.preset(...), then stamp out users. Each person keeps your color but gets their own body and face. You can pass a brand hex straight in, and a matching darker shade is generated for the gradient.

import { picto, Picto } from 'pictoguys'

const brand = picto.preset({ color: '#19c37d' }) // your green

function Avatar({ userId }) {
  const me = React.useMemo(() => brand.character(userId), [userId])
  return <Picto char={me} size={96} />
}

"Always this body, random colors, no random face"

Lock the shape and the face, let the color come from the seed:

<Picto
  config={{
    seed: userId,
    shape: 2,          // always this body
    eyes: 'double',    // always this face
    brow: 'double_1',  //   "
    mode: 'mono',      // always plain eyes
    // color is left out, so it is the only thing that changes per user
  }}
/>

Random, but only from a set

Want variety, but inside guardrails? Pass arrays:

// only ever shapes 1 or 3, only double or triple eyes, only your two greens
<Picto
  config={{
    seed: userId,
    shape: [1, 3],
    eyes: ['double', 'triple'],
    color: ['green', 'lime'],
  }}
/>

Brand colors, spelled out

color accepts an anchor name ('blue'), a brand hex ('#19c37d', auto-gradient), or an array of either. For full manual control, set both gradient stops yourself with light and dark. Want to preview the gradient a hex would make?

import { picto } from 'pictoguys'

picto.gradient('#19c37d') // { light: '#19c37d', dark: '#003329' }
picto.gradient(140)       // a gradient for hue 140 degrees

Tip: pass something stable as the seed (a user id, an email, a username) so the same person always lands on the same picto.

Make them move

Two ways, pick whichever feels easier.

The easy way: just ask for an animation.

<Picto seed="Gizmo" animate="breath" />

Animations available: "blink", "jump", "breath", "dance", "sleeping". breath, dance, and sleeping loop forever. blink and jump play once.

The hands-on way: tell a specific picto to do something.

First make a picto with picto.character(...), then call methods on it:

import { picto, Picto } from 'pictoguys'

function Mascot() {
  const guy = React.useMemo(() => picto.character('Gizmo'), [])

  return (
    <>
      <Picto char={guy} size={140} />
      <button onClick={() => guy.blink()}>blink</button>
      <button onClick={() => guy.dance()}>dance</button>
      <button onClick={() => guy.sleep()}>sleep</button>
      <button onClick={() => guy.stop()}>chill</button>
    </>
  )
}
guy.blink()    // one blink
guy.jump()     // one hop
guy.breath()   // breathe (loops)
guy.dance()    // dance (loops)
guy.sleep()    // sleep with Zs (loops)
guy.stop()     // freeze

Heads up for beginners: guy.blink() works by poking the <Picto char={guy} /> on screen. If that picto is not currently rendered, the call simply does nothing (no crash, no error, just a no-op). So render it first, then animate it.

Rendering many pictos

One picto? Reach for <Picto>. A whole wall of them (a leaderboard, a member directory, a sticker sheet, hundreds or thousands of avatars)? That is where <PictoField> comes in.

<PictoField> draws many pictos onto a single <canvas>, and it can keep hundreds to thousands of them moving at 60fps. It is the recommended way to render multiple pictos. <Picto> is not going anywhere and is not deprecated; it is simply the right tool for one or a few pictos (or when you specifically want real DOM nodes). Both draw the same art and play the same animations, so you can mix them freely.

Hand it an array of characters and it lays them out for you:

import { picto, PictoField } from 'pictoguys'

// build as many little guys as you like
const chars = React.useMemo(
  () => Array.from({ length: 500 }, (_, i) => picto.character(i)),
  [],
)

export default function Wall() {
  return <PictoField chars={chars} size={64} height="70vh" />
}

That is a 500-picto grid that scrolls smoothly. By default <PictoField> owns its own scroll viewport (an overflow:auto box sized by height, default '70vh') and only ever draws the pictos you can actually see, so the count barely matters.

Lay them out your way. By default they auto-flow into a grid. Pass cols to fix the column count, or pass an explicit array of top-left positions:

<PictoField chars={chars} cols={10} gap={16} />
<PictoField chars={chars} layout={[{ x: 0, y: 0 }, { x: 80, y: 0 }, /* ... */]} />

Animate the whole field at once. The easy way is the declarative animate prop, which plays one animation on every picto:

<PictoField chars={chars} animate="breath" />   {/* the whole crowd breathes */}

The hands-on way is the imperative handle. <PictoField> forwards a ref to a renderer you can poke directly. The animation target is either a single Character from your chars array or the literal 'all':

import { picto, PictoField } from 'pictoguys'
import type { PictoRenderer } from 'pictoguys'

function Crowd() {
  const ref = React.useRef<PictoRenderer>(null)
  const chars = React.useMemo(
    () => Array.from({ length: 300 }, (_, i) => picto.character(i)),
    [],
  )

  return (
    <>
      <PictoField ref={ref} chars={chars} size={64} />
      <button onClick={() => ref.current?.blink('all')}>everyone blink</button>
      <button onClick={() => ref.current?.dance(chars[0])}>just the first one dances</button>
      <button onClick={() => ref.current?.stop('all')}>chill</button>
    </>
  )
}

The handle exposes blink, jump, breath, dance, sleep, stop, and a general play(target, name) (pass name: null to stop), plus start(), dispose(), and a metrics() peek. (Note: the looping animation string is 'sleeping', but the method is sleep().)

Flat or fancy: the variant prop

Both <Picto> and <PictoField> take a variant prop:

| Variant | Look | | --------- | -------------------------------------------------------------- | | 'fancy' | The original look: gradient body plus soft shadows. Default. | | 'flat' | Drops the body gradient and the body shadow (the eye shadows stay). Cheaper to paint, which is handy across a big grid. |

<Picto seed="Bloop" variant="flat" />
<PictoField chars={chars} variant="flat" />   {/* lighter paint for huge fields */}

fancy is the default everywhere, so leave it off if you want the classic look.

Best practices for many pictos

  • 100+ pictos? Use <PictoField> (canvas), not a pile of <Picto>s. One canvas with culling beats hundreds of DOM nodes.
  • Reach for variant="flat" on very large grids. It skips the gradient and shadow, so each tile is cheaper to paint.
  • Reuse seeds. Identical characters share one cached sprite, so a grid full of repeats is nearly free to draw.
  • Let <PictoField> own its scroller via the height prop (default '70vh'). Only pass scrollParentRef when the field must scroll inside an existing scroll container you already control.
  • Animate via the ref ('all' or a single character) or the animate prop. Both routes share the renderer's per-character clock.
  • The canvas pixels match the SVG at the rendered size, so a <PictoField> tile and a <Picto> of the same size look identical.
  • Keep <Picto> (DOM/SVG) for single avatars or anywhere you need real DOM nodes, CSS styling, or accessibility hooks on the element itself.

Without React (custom layouts, other frameworks)

The batch renderer has a framework-agnostic core under pictoguys/canvas. Give createPictoRenderer a <canvas> and drive it yourself:

import { createPictoRenderer, canvasSupported } from 'pictoguys/canvas'
import { picto } from 'pictoguys/core'

if (canvasSupported) {
  const canvas = document.querySelector('canvas')!
  const renderer = createPictoRenderer({ canvas, size: 64, variant: 'flat' })

  renderer.setItems([
    { char: picto.character('Bloop'), x: 0, y: 0 },
    { char: picto.character('Mochi'), x: 80, y: 0 },
  ])
  renderer.start()
  renderer.breath('all')
  // ...later: renderer.dispose()
}

createPictoRenderer is always safe to call: on the server or anywhere without a canvas it returns a harmless no-op, and canvasSupported lets you fall back to <Picto> when you need to.

PictoField props

<PictoField> accepts these. Only chars is required.

| Prop | Type | Default | What it does | | ----------------- | ----------------------------- | -------- | ------------------------------------------------------- | | chars | Character[] | — | The pictos to draw, one tile each, in order. Required. | | size | number | 64 | Tile width and height, in pixels. | | variant | 'fancy' \| 'flat' | 'fancy'| Body look (see above). | | background | boolean | false | Draw a background tile behind each picto. | | layout | 'grid' \| {x,y}[] | 'grid' | Auto-flow grid, or explicit top-left positions. | | cols | number | auto | Grid columns. Omit to derive from the canvas width. | | gap | number | 12 | Gap between grid tiles, in pixels. | | animate | "blink" \| "jump" \| "breath" \| "dance" \| "sleeping" \| null | null | Play one animation on every picto. | | height | number \| string | '70vh' | Height of the self-owned scroll viewport. | | scrollParentRef | RefObject<HTMLElement> | none | Advanced: scroll inside your own container instead. | | dpr | number | auto | Device-pixel-ratio override. | | maxCacheBytes | number | 256 MB | Soft sprite-cache size cap. | | style | CSSProperties | none | Applied to the inner <canvas>. | | className | string | none | Applied to the outer wrapper <div>. |

All the props

<Picto> accepts these. Everything is optional.

| Prop | Type | Default | What it does | | ------------ | -------------------------------------- | ------- | ----------------------------------------- | | seed | number \| string | 0 | Build a picto from a number or a word. | | config | CharConfig | none | Build a picto from exact settings. | | char | Character | none | Use a picto you already made (wins). | | size | number | 120 | Width and height, in pixels. | | background | boolean | false | Set true to add a background tile. | | animate | "blink" \| "jump" \| "breath" \| "dance" \| "sleeping" | none | Play an animation on loop or once. | | variant | "fancy" \| "flat" | "fancy"| Body look: flat drops the gradient/shadow. |

Any normal <span> prop works too (className, style, onClick, and so on), because that is what <Picto> renders into.

Rendering a crowd of pictos? See Rendering many pictos for <PictoField>, the canvas batch renderer.

Pictos are see-through by default, so they sit nicely on top of anything. Want a colored tile behind one instead? Flip one switch:

<Picto seed="Bloop" />              {/* bare, the default */}
<Picto seed="Bloop" background />   {/* with a background tile */}

Using it outside React

A picto is just an SVG string under the hood, so you can grab that string and do whatever you want with it (emails, server rendering, saving to a file).

import { picto } from 'pictoguys/core'

const svg      = picto.character('Bloop').svg()                  // bare (default)
// -> "<svg viewBox=\"0 0 40 40\" ...>...</svg>"

const withTile = picto.character('Bloop').svg({ background: true }) // add a tile
const prefixed = picto.character('Bloop').svg({ uid: 'a_' })     // custom id prefix

You can also read what a picto turned out to be:

const guy = picto.character('Bloop')
guy.config
// { color: 'gen319', shape: '4', eyes: 'single', mode: 'mono', ... }

The svg() output is deterministic, and every id inside is prefixed, so you can drop many pictos into one page without their gradients or filters fighting.

Bonus: just the random number maker

The seeded randomness that powers picto lives on its own tiny path, with none of the character art attached. Handy if you only want stable, repeatable random numbers:

import { mulberry32, hashSeed } from 'pictoguys/rng'

const random = mulberry32(hashSeed('any-string'))
random() // a number 0..1, the same every time for that string

Importing pictoguys/rng pulls in well under 1 KB. Importing the full library includes the character art (that art is the whole point, so it ships with it).

How it works (the 20 second version)

  1. Your seed goes through a small, predictable shuffler.
  2. The shuffle picks a color, a shape, eyes, eyebrows, and a background.
  3. Those SVG parts get recolored in OKLCH color space (so the colors always look nice together, not muddy) and stitched into one SVG.
  4. React drops that SVG on the page. Animations are just smooth CSS transforms on the body or the eyes.

Same seed in means same picto out. No randomness leaks, no surprises.

FAQ

Is it really the same picto every time? Yes. "Bloop" is Bloop on your laptop, your phone, and your friend's machine.

Do I need to download or host any images? No. The parts are inside the package. One npm install and you are done.

Can I get a totally specific look? Yes, use config={{ ... }} and set exactly what you want.

How big is it? The art data is around 120 KB before gzip. It compresses well, and it is the actual content of the library, so there is nothing to fetch separately.

License

MIT. Go make a thousand little guys.