Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Facehash Laravel Package

saade/filament-facehash

Generate unique, deterministic SVG avatar faces for Filament panels. The same name always produces the same face. A drop-in replacement for default user avatars, no GD or Imagick needed, with customizable size, colors and variants plus a trait for Blade views and notifications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire two lines into your panel provider

composer require saade/filament-facehash

In app/Providers/Filament/AdminPanelProvider.php:

use Saade\FilamentFacehash\FacehashPlugin;
use Saade\FilamentFacehash\FacehashProvider;

public function panel(Panel $panel): Panel
{
    return $panel
        ->defaultAvatarProvider(FacehashProvider::class)
        ->plugins([
            FacehashPlugin::make(),
        ]);
}

Then php artisan optimize:clear and hard-refresh. Every user without an uploaded avatar now gets a deterministic SVG face. No migration, no storage disk, no gd/imagick in your php.ini.

Where to look first, in this order

  1. vendor/saade/filament-facehash/src/FacehashPlugin.php — the whole day-to-day surface lives here: size(), variant(), initial(), colors(). It's short enough to read in one sitting and it's the file you'll actually edit around.
  2. vendor/saade/filament-facehash/src/FacehashProvider.php — the Filament AvatarProvider implementation. Reading it tells you precisely what defaultAvatarProvider() expects, which is your escape hatch if you ever need to swap it out.
  3. vendor/saade/filament-facehash/src/Concerns/HasFacehashAvatar.php — the trait that backs facehash_avatar_url for Blade, Inertia props, and notifications.
  4. https://facehash.saade.dev — the playground. Cheapest way to validate a palette or a weird name (RTL, emoji, accented, 60 characters long) before you commit config or write a test.

First use case worth shipping

Your UserResource table and any ->avatar() column. Today they render as identical grey circles because nobody uploads photos, which makes scanning a long list genuinely harder. Because defaultAvatarProvider() is a fallback seam, any model already returning an uploaded URL from Filament's HasAvatar contract still wins — so this is a one-line, zero-risk, fully reversible adoption.

Verify on day one, in this order

  • Load one resource list with an avatar column and confirm the images actually paint. CSP is the usual culprit (see Gotchas).
  • Confirm your Filament version exposes defaultAvatarProvider() at all — this is a newer seam.
  • Spot-check three names: plain ASCII, a very long one, and a non-Latin script one. Catching a broken initial before your users do is worth the two minutes.

Implementation Patterns

Centralize the plugin factory so panels don't drift

Each panel builds its own FacehashPlugin::make(), which is correct for per-panel theming — but copy-pasted fluent chains rot fast. Extract one factory:

// app/Support/Facehash.php
namespace App\Support;

use Saade\Facehash\Enums\Variant;
use Saade\FilamentFacehash\FacehashPlugin;

final class Facehash
{
    public static function make(): FacehashPlugin
    {
        return FacehashPlugin::make()
            ->size(40)
            ->variant(Variant::Gradient)
            ->initial(true)
            ->colors(['#ec4899', '#f59e0b', '#3b82f6', '#f97316', '#10b981']);
    }
}

Then ->plugins([Facehash::make()]) everywhere, with deliberate overrides for panels that should differ (internal ops vs. client-facing admin). When someone asks why support's avatars look different from billing's, there's exactly one file to open.

Variant and palette are the highest-leverage knobs

  • Variant::Gradient reads as a designed avatar. Variant::Solid reads as a flat badge and holds up better behind text or in dense tables where gradients turn to mush at 20px.
  • Keep the palette to 5–8 mid-to-dark hues. Very light colors wash the initial out; near-identical hues make adjacent rows visually indistinguishable, which defeats the entire point.
  • size() is in pixels, default 40. Match it to your table row height — leaving the default while Filament renders an sm avatar produces a blurred/oversized face.
  • Set initial(false) if your design system already prints the name next to the avatar. At that point the letter is redundant noise competing with real content.

Use the trait to serve avatars outside a panel

use Saade\FilamentFacehash\Concerns\HasFacehashAvatar;

class User extends Authenticatable
{
    use HasFacehashAvatar;
}
<img src="{{ $user->facehash_avatar_url }}"
     alt="{{ $user->name }}"
     class="h-8 w-8 rounded-full">

It's a computed accessor, so direct property access works without $appends. Only add 'facehash_avatar_url' to $appends if you serialize the model into JSON for an API or an Inertia page prop — and measure the payload first (see Gotchas).

Pick one name-derivation policy and apply it everywhere

Default is the name attribute. Override per model via the documented attribute:

use Illuminate\Database\Eloquent\Casts\Attribute;

public function facehashAvatarName(): Attribute
{
    return new Attribute(
        get: fn () => $this->email ?: $this->name ?: "user-{$this->id}"
    );
}

Email is unique where display names collide, but it's PII and it mutates on change. Legal name is stable but collides. Whichever you pick, apply it consistently across every model — a mixed policy means avatars visibly change for reasons no user can explain, which turns a cosmetic feature into a support ticket generator.

Roll out panel by panel

Adoption is additive by construction: enable on one low-traffic panel, keep your upload pipeline untouched, expand as confidence grows. Reverting is deleting one line from one provider — which is the strongest argument for approving the pilot in the first place.

Pin the behavior with two tests

A dependency bump that silently reshuffles everyone's face is a real, if cosmetic, regression:

it('is deterministic per name', function () {
    $a = User::factory()->make(['name' => 'Ada Lovelace']);
    $b = User::factory()->make(['name' => 'Ada Lovelace']);

    expect($a->facehash_avatar_url)->toBe($b->facehash_avatar_url)
        ->and($a->facehash_avatar_url)->toStartWith('data:image/svg+xml;base64,');
});

On a Livewire resource test, assertSee('data:image/svg+xml') covers the panel integration end.

In-app only, never in mail

Filament panels and in-app notifications: yes. Transactional email: no. SVG data-URI support across mail clients (Outlook in particular) is unreliable enough that you should keep a hosted PNG or nothing there.

Gotchas and Tips

CSP will break this in production and not in dev. Output is a data:image/svg+xml;base64,... URI. Without img-src data: in your Content-Security-Policy, every avatar silently fails to paint — and dev has no CSP, so it looks fine right up until deploy. Check middleware, web server config, and any CDN-injected headers. This is by far the most common "it's broken" cause, and it fails quietly rather than loudly.

Watch the HTML payload on big tables. Generation happens in PHP per avatar per render and gets inlined into the Livewire/HTML document. On a 50-row user table that's real bytes, multiplied by every render. Compare document size on your heaviest list view before and after. If the delta hurts, shorten the column on wide tables or memoize the accessor on the model instance when the same user renders repeatedly in one request.

Determinism is a product consequence, not just a property. Two "John Smith"s get pixel-identical faces; a rename yields a brand-new face. On a user-management or moderation screen both cause genuine confusion — "I can't tell which John Smith I'm about to suspend." Mitigate by deriving from a unique attribute, piloting where collisions are rare, and keeping the one-line revert ready.

Guard empty and malformed names yourself. Never let facehashAvatarName() return null or '', or every blank-name user collapses onto the same face — the exact failure mode the feature is meant to fix. The ?: "user-{$this->id}" fallback above gives you a stable per-ID string.

Non-Latin initials, RTL, emoji, and long names are where SVG text gets weird. initial(true) takes the first letter, and grapheme clusters don't always cooperate — an emoji name can render as half a surrogate pair. Throw a handful of real names from your database at the playground before merge. While you're there, verify escaping with a name containing < or &; don't assume it.

Pin the transitive generator, not just the adapter. There are two packages: saade/filament-facehash (this Filament skin) and saade/facehash (the generator, same author). Pinning the adapter does not pin the generator. If byte-identical faces across composer update matter to you, add an explicit constraint on saade/facehash. Note the adoption signals — a handful of stars and zero dependents — and size your risk accordingly rather than raising it in an architecture review as if it were a household dependency.

The adapter is thin — know your exit. The valuable dependency is the generator. If this package stalls, you own roughly one class implementing Filament's AvatarProvider that calls the underlying library and returns a data URI. Keeping that framing in mind defuses the single-maintainer concern entirely.

Don't serialize the data URI into queues. Passing $user->facehash_avatar_url into a queued notification constructor writes a base64 SVG into your jobs table for every recipient. Pass the user or the user ID and resolve the URL inside the notification's toArray() / render path instead.

Debugging order when faces don't appear:

  1. Works in the panel but not in Blade → trait missing on the model, or an existing getFacehashAvatarUrlAttribute() accessor is shadowing it.
  2. Nowhere at all → almost always CSP, or a stale compiled view. php artisan optimize:clear plus a hard refresh before you start reading source.
  3. Renders but looks wrong → variant/palette/size conflict between two FacehashPlugin instances across panels, or a stale compiled view.

Extension points, exhaustively: swap the provider class entirely (any class satisfying Filament's provider contract — useful for A/B testing hashed faces against plain initials), override facehashAvatarName() per model, and re-theme per panel via the plugin. There is no config file to publish and nothing to migrate — which also means styling changes ship with a deploy, not an .env flip.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin