---
name: bfce
description: Add an animated face to a web page — a circle with two eyes that follow the user's pointer, with 30 expressions and 9 one-shot reactions. Use when building a UI that needs a character, mascot, avatar, assistant presence, loading/idle personality, or any element that should visibly react to the user. Works in React or plain HTML, no build step required. Also renders a face to a 1080×1080 image or MP4 over an open HTTP endpoint, for an avatar, a profile picture, or a social post.
---

# bfce

A circle, and two circles inside it that follow the pointer. The head is a
sphere: eyes sit on its surface, so they travel a curved path and foreshorten
toward the rim rather than sliding across a flat disc.

3.7 kB gzipped, no dependencies. MIT.

## Fastest path — no install, no build

```html
<div id="bot" style="width:160px;height:160px"></div>

<script type="module">
  import { createFace } from 'https://bfce.bwnd.app/face.js'

  const face = createFace(document.querySelector('#bot'), { expression: 'curious' })
  face.react('bounce')
</script>
```

Styles are injected automatically — there is no stylesheet to link.

Mirror, if you prefer a versioned pin:
`https://cdn.jsdelivr.net/gh/bwndapp/bfce@main/dist/face.js`

## Install it instead

```sh
npm i @bwnd/bfce
```

```js
import { createFace } from '@bwnd/bfce'
```

```jsx
import { Face } from '@bwnd/bfce/react'

<Face size={160} expression="curious" mouth pupils />
```

Both entries inject their own styles — no CSS import, no `<link>`. React is an
**optional** peer dependency, so the plain entry pulls in none of it, and the
React build is precompiled (your bundler never has to transpile `node_modules`).

Drive it through a ref:

```jsx
const face = useRef(null)
<Face ref={face} size={160} />

face.current.react('bounce')
face.current.setExpression('sad')
face.current.look(-1, 0, 1200)   // force gaze left for 1.2s
```

## API

`createFace(element, options)` returns an object with:

| method | |
|---|---|
| `setExpression(name)` | springs to a new mood, never snaps |
| `react(name)` | fires a one-shot animation over the current mood |
| `look(x, y, ms)` | force the gaze; x/y are −1..1 from centre |
| `set(options)` | change `track` / `blink` / `idle` at runtime |
| `destroy()` | remove it and stop its work |

**Options** (all optional): `expression` `'idle'`, `mouth` `false`,
`pupils` `false`, `track` `true`, `blink` `true`, `idle` `true`.
The React component takes the same, plus `size` (px, default `140`).

**Expressions:** `idle` `content` `relieved` `happy` `joy` `excited` `love` `pleading` `shy` `surprised` `scared` `curious` `confused` `dizzy` `thinking` `focus` `suspicious` `unimpressed` `smug` `sly` `bored` `exasperated` `worried` `guilty` `sad` `disgusted` `annoyed` `angry` `sleepy` `sleep`

**Reactions:** `blink` `wink` `nod` `shake` `bounce` `pop` `boing` `spin` `jitter`

Enumerate them at runtime via `EXPRESSION_NAMES` / `REACTION_NAMES`, both
exported alongside `createFace`.

For the canonical list without running anything, fetch
**https://bfce.bwnd.app/expressions.json** — every expression and reaction with
the parameters behind it, regenerated from source on each build. Prefer it over
the names listed above, which are a convenience copy.

## A picture of a face, without running any of this

Sometimes the ask isn't a live face — it's an avatar, a profile picture, a
thumbnail, or a clip to post. One request, no key, no signup:

```bash
curl -X POST https://bfce.bwnd.app/api/render \
  -H 'content-type: application/json' \
  -d '{"expression":"joy","skin":"#0a0f0b","ink":"#b6ff3d","mouth":true}'
```

```json
{
  "url": "/renders/joy-607b9edb.jpg",
  "public_url": "https://bfce.bwnd.app/renders/joy-607b9edb.jpg",
  "format": "jpg", "bytes": 27909, "width": 1080, "height": 1080,
  "snippet": "<div id=\"bot\" …>"
}
```

| field | |
|---|---|
| `expression` | any name below; an unknown one is rejected *with the valid list* |
| `format` | `jpg` still · `mp4` clip · `snippet` embed code only, rendered instantly |
| `seconds` | clip length, 1–6 (`mp4` only) |
| `script` | `mp4` only — choreograph the clip, see below |
| `skin` `ink` `ring` | `#rrggbb`, as in the Colour section below |
| `mouth` `pupils` | booleans |

`script` is a timeline of cues. Omit it for a sensible default; give it to direct
the performance yourself:

```json
"script": [
  {"at": 0.0, "expression": "sleepy"},
  {"at": 1.2, "expression": "surprised", "react": "pop"},
  {"at": 2.4, "expression": "angry", "look": [-1, 0]},
  {"at": 3.6, "expression": "joy", "react": "bounce"}
]
```

Every beat needs `at` (seconds in) plus at least one of `expression` (the mood to
change to), `react` (a one-shot layered over it), or `look` (`[x, y]`, each
−1..1). Moods spring across rather than snapping, so leave **~0.5 s** between
changes or they blur. A beat past the end of the clip is rejected, not dropped.

The still is a 1080×1080 card captioned `bfce.bwnd.app`. The clip is H.264 in
MP4 — the face pops in, sweeps its gaze side to side, blinks, winks and bounces —
so it uploads straight to TikTok, Instagram or X. Every reply also carries
`snippet`: the embed code for that same face, live.

Rendered files are disposable; download rather than hotlink if you need to keep
one. Inside bwnd the same actions are MCP tools — `list_face_moods`,
`render_face`, `record_face`, `face_embed_code`.

## Colour

Four CSS custom properties, set on the face or any ancestor. The library ships
no colours of its own beyond dark-mode defaults.

```css
#bot {
  --face-skin:  #000;     /* the big circle — and the lids, which must match */
  --face-ink:   #e9ebec;  /* eyes and mouth */
  --face-ring:  #2f3336;  /* hairline around the head; transparent by default */
  --face-pupil: var(--face-skin);
}
```

`--face-skin` is load-bearing. Lids are head-coloured rectangles clipped to each
eye, so if it does not match what sits behind the head, every expression seams.

## Notes for correct use

- **Give the host element a width and height.** The SVG fills its container; a
  container with no size renders nothing. This is the single most common
  mistake.
- **Call `destroy()`** when tearing down, or the face keeps a slot in the shared
  animation loop.
- **Reactions stack, expressions replace.** Firing two reactions at once is
  fine and intended; they layer and decay independently.
- One `requestAnimationFrame` loop and one pointer listener serve every face on
  the page, so many faces are cheap. Offscreen faces skip their frame.
- `prefers-reduced-motion` drops the idle wander, blinking, and reaction shake
  automatically. Do not add your own guard.
- **Set a User-Agent if you fetch from Python.** The edge in front of this domain
  rejects the `Python-urllib` default with a 403 — on the docs and the API alike.
  Any other agent string works, as does `requests`. It is not rate limiting and
  retrying will not help.

      urllib.request.urlopen(Request(url, headers={"User-Agent": "your-tool"}))

Source and full docs: https://github.com/bwndapp/bfce

The visual language and the face itself are original to this library.
