Avatars
View on GitHubAdd a new avatar face
Worked example: adding a happy face to the avatar library. Unlike most extension points in these docs there is no type, registry or backend change involved: the library discovers its faces from a folder, so the whole job is one correctly shaped SVG file plus a visual check.
Step 1 — Create the SVG
File: resources/js/assets/faces/happy.svg
<svg xmlns="http://www.w3.org/2000/svg" id="happy" viewBox="0 0 128 128"><circle cx="64" cy="64" r="58" fill="#f8dc25"></circle><circle cx="44" cy="52" r="7" fill="#4a3600"></circle><circle cx="84" cy="52" r="7" fill="#4a3600"></circle><path d="M36 76c6 20 50 20 56 0" fill="none" stroke="#4a3600" stroke-width="6" stroke-linecap="round"></path></svg>
The file has to follow these rules, all of which come from how the library loads it:
- Square with a
viewBox(the existing faces use0 0 128 128). The face is drawn onto a 256 by 256 canvas, so a non-square viewBox would be stretched. - Self-contained. No
<script>, no external fonts, nohref/srcpointing at other files. The SVG is loaded through an<img>and a blob URL, which blocks external resources, so anything that depends on them renders blank or differently from how it looks in the grid. - A circle that fills the viewBox, with nothing painted in the corners. The grid shows each face inside a round button and the avatar is displayed in a circle everywhere else, so artwork in the corners is simply cropped.
- The filename is the face's name. The id is the file name without
.svg(happy), and it becomes the button's tooltip and its accessible name (Use happy avatar). Prefer a short, lowercase, descriptive name. Faces are listed alphabetically by file path.
Step 2 — Check that it is recognised after a reload
File: resources/js/Components/Organisms/AccountSettingsContent/AccountSettingsAvatarLibrary.tsx
You do not edit this file, but it is the code that picks your SVG up:
const faceModules = import.meta.glob<string>('../../../assets/faces/*.svg', {
eager: true,
query: '?url',
import: 'default',
});
const faces = Object.entries(faceModules)
.sort(([a], [b]) => a.localeCompare(b))
.map(([path, src]) => ({
id: path.split('/').pop()!.replace('.svg', ''),
src,
}));
Vite resolves the glob at build time, so a new file needs the dev
server to notice it (it normally does; if the new face does not show
up, restart npm run dev or make up).
When a user picks a face, handleSelectFace in
AccountSettingsProfileTab.tsx rasterizes it to a PNG and uploads it
through account.upload-avatar. After a page reload the library marks
the current avatar's face by comparing pixels, with a mean per-channel
tolerance of 2 (out of 255) defined by MATCH_TOLERANCE in
resources/js/utils/faces.ts. Two faces that differ by less than that
would both match the same avatar, and the first one alphabetically would
win. The closest pair of faces today differs by about 5.5, so keep a new
face visibly different from every existing one (a different expression
or colour is enough; recolouring a single small detail is not).
Step 3 — Look at it in the app
Run the app (composer dev or make up), open Account settings →
Profile and click the avatar. Check:
- The new face shows in the grid, is round, and has a tooltip with its file name.
- Picking it updates the preview, saves without an error alert, and closes the library.
- Reload the page and open the library again: the same face should carry the check badge. If no face is marked, the uploaded PNG does not match the rasterized SVG, usually because the SVG relies on an external resource or is not square.
Tests
No test changes are needed for a new face: the existing tests do not depend on how many faces exist.
resources/js/Components/Organisms/AccountSettingsContent/AccountSettingsAvatarLibrary.test.tsxcovers matching the current avatar and recovering when a face fails to load.resources/js/Components/Organisms/AccountSettingsContent/AccountSettingsProfileTab.test.tsxcovers rolling back the avatar when saving a face fails.resources/js/utils/faces.test.tscoversrasterizeImage,canvasToBlobandcanvasesMatch.
jsdom has no canvas, so these tests mock rasterization and pixel data. Pixel-level matching of real SVGs is only covered by the manual check in step 3.
