# How to Add a Character Customizer to Your App — Copy-Paste Prompts Inside

- Tool: App Features
- Last updated: July 2026

## TL;DR

A character customizer needs five pieces: a canvas rendering layer (react-konva for web, Stack widgets for Flutter), a layered asset manager cycling through categories, a color tinting system per layer, a Supabase JSONB column persisting the avatar config, and a PNG export button. With Lovable or V0 you can scaffold it in 1–2 days for $0/month; the hard work is wiring the canvas compositing logic that AI tools get only ~70% right on the first pass.

## What an Interactive Character Customizer Actually Is

A character customizer lets users build and personalize an avatar layer by layer — body, hair, clothing, accessories — and see the result update in real time. The feature stores the selected configuration as a JSONB blob in Supabase and optionally exports the composed result as a downloadable PNG. It appears in gaming apps, social platforms, kids' products, fitness apps with avatars, and any product where user identity matters enough to personalize. The two hard problems that separate a working implementation from a broken one are canvas layer compositing (getting the z-order right and not remounting Konva layers on every state update) and color tinting (working correctly only on PNG assets with transparent backgrounds, not JPEGs).

## Anatomy of the Feature

Six components. AI tools reliably scaffold the asset-switching UI and the Supabase persistence. The canvas rendering layer, color tinting, and PNG export are where first builds break.

- **Canvas rendering layer** (ui): Composites all asset layers into a single visible character. On web: Konva.js via the react-konva wrapper renders each asset as a Konva.Image node on its own Konva.Layer, ordered by z-index. On mobile (FlutterFlow): a Stack widget with absolutely positioned Image widgets layered by z-index achieves the same compositing without a canvas API.
- **Asset layer manager** (ui): A tab or accordion UI cycling through asset categories — body, face, hair, outfit, accessories. Each tab renders a horizontal scroll list of thumbnail options. Selecting a thumbnail updates only that category's Konva.Image node or Flutter Image widget, leaving all other layers unchanged.
- **Color tint system** (service): Applies a color overlay to individual asset layers. On web: CSS filter hue-rotate + saturate applied to a Konva.Image node, or SVG fill replacement for vector assets. On Flutter: the ColorFiltered widget wraps a single Image widget with BlendMode.srcATop to tint only that layer's PNG.
- **Avatar state store** (data): A JSONB column (avatar_config) on the Supabase profiles table stores the complete customizer state: selected asset ID per category plus hex color per layer. Example: {"body":"body_02","hair":"hair_05","hair_color":"#3D1C02","outfit":"outfit_03","accessories":["acc_01"]}. Writes are debounced 1 second to avoid hammering the database on rapid taps.
- **PNG export** (backend): Renders the composed character to a PNG file the user can download or share. On web: html-to-image (or Konva Stage.toDataURL()) captures the canvas node. On Flutter: Flutter's RenderRepaintBoundary.toImage() followed by ByteData conversion produces PNG bytes for download or upload to Supabase Storage.
- **Undo / redo stack** (ui): A React useReducer (or a simple array in Flutter app state) stores the history of avatar_config snapshots with a current index pointer. Undo moves the index back one step; redo moves it forward. Cap the history at 20 entries to bound memory usage.

## Data model

Extend your existing profiles table with two columns; run this in the Supabase SQL editor:

```sql
-- Add avatar columns to existing profiles table
alter table public.profiles
  add column if not exists avatar_config jsonb default '{}'::jsonb,
  add column if not exists avatar_url text;

-- If profiles table does not exist yet, create it:
create table if not exists public.profiles (
  id uuid primary key references auth.users(id) on delete cascade,
  display_name text,
  avatar_config jsonb default '{}'::jsonb,
  avatar_url text,
  updated_at timestamptz not null default now()
);

alter table public.profiles enable row level security;

create policy "Users can view own profile"
  on public.profiles for select
  using (auth.uid() = id);

create policy "Users can update own profile"
  on public.profiles for update
  using (auth.uid() = id);

create policy "Users can insert own profile"
  on public.profiles for insert
  with check (auth.uid() = id);

-- Index for fast profile lookups by user id (covered by PK, but explicit for clarity)
create index if not exists profiles_id_idx on public.profiles (id);
```

avatar_config stores the full customizer state as a JSONB blob — no schema migration needed when you add new asset categories, just update the JSON keys. avatar_url stores the Supabase Storage path of the exported PNG so any part of the app can display the user's avatar without accessing the customizer canvas.

## Build paths

### Lovable — fit 3/10, 1–2 days

Lovable scaffolds the layer-switching UI tabs and Supabase persistence quickly, but react-konva canvas compositing and PNG export require manual iteration beyond what one prompt delivers.

1. Create a new Lovable project with Lovable Cloud connected so Supabase auth and the profiles table are provisioned
2. Run the SQL schema from this page in the Supabase SQL editor to add avatar_config and avatar_url columns
3. Paste the prompt below in Agent Mode; expect 1–2 follow-up prompts to fix Konva layer ordering and PNG export
4. Host all character asset images in Supabase Storage (not external URLs) before testing PNG export — cross-origin assets cause blank exports
5. Click Publish and test PNG export on the published URL; Lovable's preview iframe may block canvas reads from external asset URLs

Starter prompt:

```
Build an interactive character customizer. Use react-konva for the canvas. Canvas size: 400×400px centered on screen. Layer order bottom to top: background, body, clothing, hair, accessories. For each layer, render a Konva.Image node using useRef — swap images via node.image(newImg) to avoid remounting layers on state change. Asset selector: shadcn Tabs component with one tab per category (Body, Hair, Clothing, Accessories); each tab shows a horizontal scroll list of thumbnail images. Selecting a thumbnail updates only that layer. Color picker: a hex color input below each tab applies CSS hue-rotate + saturate filter to that layer's Konva.Image; use PNG assets only (no JPEGs). Undo/redo: useReducer with history array capped at 20 entries; show Undo and Ctrl+Z keyboard shortcut, Redo and Shift+Ctrl+Z. Save button: debounce 1 second, upsert avatar_config JSONB to Supabase profiles table for the current user. Export button: use Konva Stage.toDataURL() to download a PNG named 'my-character.png'; also upload the PNG to Supabase Storage at avatars/{user_id}.png and save the URL to avatar_url in profiles. Default/empty state: show a plain mannequin silhouette. Handle missing assets gracefully with a placeholder image. Mobile-responsive: stack asset selector below the canvas on screens narrower than 640px. Loading skeleton while assets fetch from Supabase Storage.
```

Limitations:

- The ~70% problem applies strongly here — complex SVG layer ordering and color tinting often require 1–2 manual fix iterations after the initial prompt
- PNG export via html-to-image or Konva.toDataURL() fails in Lovable's preview iframe due to content security policy; test only on published URL
- react-konva canvas interactions (drag, pinch-to-scale on mobile) need manual polish beyond what the initial prompt produces

### V0 — fit 3/10, 1–2 days

V0 excels at generating the shadcn-based tab UI and color picker components, but react-konva canvas wiring and undo/redo logic need manual integration after V0 scaffolds the layout skeleton.

1. Prompt V0 with the character customizer spec below to generate the layout, tabs, and color picker components
2. Add Supabase environment variables in the V0 Vars panel (NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY)
3. Run the SQL schema from this page in the Supabase SQL editor
4. Manually wire the react-konva Stage and Layer components into the scaffolded layout — V0 will generate the container divs but not the Konva canvas initialization
5. Publish to Vercel to test PNG export; localStorage hydration and canvas toDataURL both require a real deployment, not V0's sandbox

Starter prompt:

```
Create a Next.js character customizer feature. Component: CharacterCustomizer (client component). Left panel: shadcn Tabs — tabs for Body, Hair, Clothing, Accessories. Each tab renders a horizontal ScrollArea with thumbnail buttons; clicking a thumbnail fires an onLayerChange(category, assetId) callback. Below each tab: a color hex input that fires onColorChange(category, hexColor). Center: a 400×400px div with id='character-canvas' where react-konva Stage will mount — render each layer as a positioned absolute img (fallback) initially; I will wire Konva manually. Right panel (or below canvas on mobile): Undo button (Ctrl+Z), Redo button (Shift+Ctrl+Z), Save button (debounced 1s, calls upsertProfile(avatar_config)), Export PNG button (calls Stage.toDataURL()). State: useReducer with history array for undo/redo (20-step cap). Default state shows mannequin placeholder. Supabase: on mount, fetch profiles row for current user and hydrate state from avatar_config JSONB; use useEffect to avoid SSR localStorage errors. All assets fetched from NEXT_PUBLIC_SUPABASE_URL/storage/v1/object/public/character-assets/.
```

Limitations:

- V0 scaffolds the UI shell but will not auto-wire react-konva — canvas initialization and layer swapping require manual code after V0 generates the layout
- Avatar config localStorage reads on SSR in Next.js App Router cause ReferenceError — must wrap in useEffect or use Supabase server component hydration
- PNG export must be tested on Vercel deployment, not V0's sandbox preview

### Flutterflow — fit 4/10, 1–2 days

Stack + positioned Image widgets handle layering naturally in Flutter; ColorFiltered tinting is a built-in widget; best path for mobile-first or cross-platform character customizers.

1. Create a CharacterCustomizer page with a Stack widget at center; add one Image widget per layer (background, body, clothing, hair, accessories) with absolute positioning and increasing z-index
2. Wrap each Image widget individually in a ColorFiltered widget (BlendMode.srcATop) — do not wrap the entire Stack or all layers tint together
3. In the Action editor, wire each thumbnail tap to a Page State update that changes the selected asset URL for that layer; the Stack rerenders with the new image automatically
4. Add an Undo button wired to a custom FlutterFlow action that pops the last entry from a Page State list (history stack); cap the list at 20 entries
5. PNG export requires a Custom Widget wrapping Flutter's RenderRepaintBoundary.toImage() — add this as a Custom Widget in FlutterFlow (requires Pro tier); wire its output to a Supabase Storage upload action

Limitations:

- PNG export via RenderRepaintBoundary requires a Custom Widget, which requires FlutterFlow Pro ($70/mo) — the free tier cannot do this
- FlutterFlow's Custom Widget support for canvas export involves Dart code; founders without Flutter experience will need this written for them
- ColorFiltered wrapping the entire Stack instead of individual layers is a common mistake — verify each Image has its own ColorFiltered wrapper in the widget tree

### Custom — fit 5/10, 2–4 weeks

Full-control build with PixiJS (web) or Flutter Flame (mobile) for GPU-accelerated rendering; Command pattern undo/redo; server-side PNG generation for consistent cross-device output.

1. PixiJS on web (or Flutter Flame on mobile) for GPU-accelerated sprite compositing — handles 500+ asset layers without frame drops, where Konva/Stack approaches plateau
2. Command pattern undo/redo (each action is an object with execute() and undo() methods) for robust history that survives complex multi-layer operations
3. Server-side PNG generation via a Supabase Edge Function using the Deno canvas API — ensures identical output across iOS, Android, and web regardless of device GPU differences
4. Asset management system: server-side search and pagination for libraries exceeding 500 items, with category tags and rarity/unlock metadata for in-app purchase integration

Limitations:

- PixiJS and Flutter Flame require deep framework knowledge — not suitable for first-time app builders
- 3D character rendering (Three.js, Unity WebGL, or Flutter Flame 3D) is a separate workstream that adds weeks of complexity beyond 2D layering

## Gotchas

- **PNG export produces a blank white image in Lovable preview** — Lovable's preview iframe enforces a Content Security Policy that blocks cross-origin canvas reads. When character assets are loaded from external URLs (not same-origin), the canvas is 'tainted' and toDataURL() throws a SecurityError, returning a blank image. The error is often silent in the preview console. Fix: Host all character asset images in Supabase Storage within the same project. Load them via the Supabase Storage public URL (same origin as the Lovable Cloud deployment) or convert them to data URIs before drawing to the canvas. Test PNG export only on the published URL, not inside the Lovable preview iframe.
- **Layer z-index ordering resets on every state update** — AI-generated react-konva code commonly re-mounts Layer or Image components when the component's parent re-renders on state change (e.g., when the user picks a new hair color). Each remount resets the Konva internal z-order, causing layers to briefly appear in the wrong stack order or flicker. Fix: Keep Konva Stage and Layer components mounted at all times. Use useRef to hold references to individual Konva.Image nodes and call imageRef.current.image(newImageObject) to swap the asset without remounting. Specify 'use node refs to swap images, never remount Layer components' explicitly in your prompt.
- **Color tinting produces wrong colors on JPEG assets** — CSS hue-rotate and Flutter's ColorFiltered only produce predictable tinting on images with a transparent alpha channel (PNG, SVG). JPEG assets have a white or colored background: applying hue-rotate shifts that background to an unexpected tint, making the character look broken rather than colorized. Fix: Use PNG assets with transparent backgrounds for every character part. Add an asset format check in your upload pipeline — reject any JPEG before it enters the character asset library. Mention 'PNG assets with transparent backgrounds only' explicitly when prompting.
- **FlutterFlow's ColorFiltered tints the entire character, not one layer** — A common FlutterFlow mistake is wrapping the outer Stack (the whole character canvas) in a single ColorFiltered widget instead of wrapping each individual Image widget separately. The result is that selecting a hair color changes every layer to the same tint simultaneously. Fix: In FlutterFlow's widget tree, place a ColorFiltered widget as the direct parent of each individual layer Image widget — not around the Stack. Test by changing only the hair color and verifying the body and clothing layers remain unaffected. Check the widget tree carefully in FlutterFlow Preview before deploying.
- **Avatar config not persisted between sessions — localStorage crashes on V0 SSR** — V0 generates Next.js App Router code that may call localStorage.getItem('avatar_config') at the top level of a server component or before a useEffect, triggering ReferenceError: localStorage is not defined during server-side rendering. The avatar never loads and the customizer resets to default on every visit. Fix: Wrap all localStorage access in useEffect(() => { ... }, []) to ensure it runs only in the browser. Better: store avatar_config in the Supabase profiles table and hydrate it via a server component using the Supabase server client — this eliminates SSR issues and works across devices.

## Best practices

- Use node refs to swap Konva.Image assets rather than remounting Layer components — remounting is the single most common cause of flickering and z-order bugs in react-konva customizers
- Store only asset IDs and color hex values in avatar_config, never image URLs — URLs change when you reorganize Storage; stable IDs let you migrate assets without breaking saved avatars
- Debounce avatar_config writes to Supabase by 1 second — rapid-fire taps while exploring options should not create 50 database writes per session
- Validate all character art assets are PNG with transparent backgrounds before adding them to the asset library — JPEG assets break color tinting silently
- Show a mannequin or silhouette default state in the canvas before the user selects any assets — a blank white canvas looks broken, not neutral
- Cap undo/redo history at 20 steps and store snapshots of the avatar_config object only (not canvas pixel data) to keep memory usage flat
- Test PNG export on a published URL on a real mobile device — canvas toDataURL() behavior and file system write permissions differ significantly from desktop browser previews
- Wrap each Flutter Image layer in its own ColorFiltered widget, never the outer Stack — validate this in FlutterFlow Preview before assuming it works

## Frequently asked questions

### How many asset layers can the customizer support before it gets slow?

A react-konva implementation handles 6–8 simultaneous layers at 60fps on modern hardware without any optimization. Beyond that, especially on mobile browsers, you may see frame drops. If you need 10+ layers or animated assets, PixiJS (GPU-accelerated) handles 50+ layers comfortably and is worth the switch for performance-critical customizers.

### Can users share their character on social media as an image?

Yes — the PNG export button downloads the composed canvas as a PNG file. On mobile, use the Web Share API (navigator.share) to trigger the native share sheet with the PNG attached, which lets users post directly to Instagram, TikTok, or any other app. On Flutter, the Share Plus package does the same for native mobile apps.

### Do I need to create all the art assets myself?

Not necessarily. Stock character asset packs (body parts, clothing, accessories in layered PNG format) are available on marketplaces like itch.io and Craftpix. AI art tools like Midjourney or Adobe Firefly can generate character parts with consistent style if you provide detailed prompts. The only non-negotiable requirement is transparent PNG format — JPEG assets break the color tinting system.

### Can I sell individual accessories or outfits as in-app purchases?

Yes, but it adds significant complexity beyond the core customizer. You need a Stripe integration to handle purchases, a Supabase table tracking which assets each user has unlocked, and gating logic in the asset selector that grays out locked items. This is a worthwhile feature for monetization-focused apps, but it is a separate feature from the core customizer — build and ship the customizer first.

### How do I make the customizer work on both iOS and Android?

FlutterFlow is the most straightforward path for true cross-platform mobile — one codebase, one Flutter app, deployed to both stores. A Lovable or V0 web app also works on mobile browsers (iOS Safari and Android Chrome) but lacks native feel. If you want native performance on both platforms without a web wrapper, FlutterFlow with custom PNG export widget is the right choice.

### Can the character animate — walk, wave — or is it just static?

The implementation described here produces a static layered image. For animations you need a sprite sheet system (CSS animations on web, Flutter AnimationController with sprite frames on mobile) or an integration with a dedicated tool like Spine 2D for skeletal animation. This is a significant jump in complexity — typically worth scoping as a V2 feature once the static customizer is live.

### How do I store the user's character so it loads the same next time they open the app?

Store the avatar_config JSONB object in the Supabase profiles table (as shown in the data model above). On app load, fetch the profiles row for the signed-in user and initialize the customizer state from avatar_config. This works across devices and survives app reinstalls. Avoid localStorage as your primary persistence — it is browser-local and does not sync.

### Can I let users upload a photo as their character base?

Yes — add a photo upload option that stores the image in Supabase Storage and loads it as the bottom-most canvas layer instead of a preset body asset. The rest of the layering system (hair, clothing, accessories on top) works the same way. Note that user-uploaded photos as a base layer make color tinting the base layer unpredictable — consider disabling the color picker for the base layer when a custom photo is used.

---

Source: https://www.rapidevelopers.com/app-features/interactive-character-customizer
© RapidDev — https://www.rapidevelopers.com/app-features/interactive-character-customizer
