Best for
IoT dashboards, EV apps, device management UIs, or any product needing an animated battery/power level indicator
Stack
A ready-made Charging Widget UI you can fork, run, and customize with the prompt pack below.
What's actually inside
The honest engineer's breakdown — what the Charging Widgettemplate does, how it's wired, and where it's opinionated.
The Charging Widget renders a self-contained animated battery component driven by two props: a numeric level (0–100) and a boolean charging flag. The BatteryShell is an inline SVG with a terminal nub drawn via viewBox, making it fully responsive without fixed pixel dimensions. ChargeLevelFill is a Framer Motion-animated SVG rect that smoothly expands to match the current level, and ColorIndicator swaps the fill colour through three ranges — red below 20%, yellow 21–50%, green above 50% — via Framer Motion's animate prop.
When the charging prop is true, a LightningBolt SVG icon appears over the fill area with a pulsing glow animation. A PercentageLabel sits centred over the battery, updating reactively as the level changes. Out of the box the widget is purely presentational: you pass it a level and it animates — no internal polling or data fetching is included.
Honest caveat: the template has no built-in data source — level and charging must come from a parent prop, simulation, or external subscription. The SVG fill colour is also controlled via Framer Motion's animate values and Tailwind fill-* utilities, not bg-* classes; applying standard background colours to SVG elements is a known gotcha that trips up most first-time forks.
Key UI components
ChargingWidgetRoot component managing battery level (0-100), charging state boolean, and the animation loop
BatteryShellSVG outline of the battery casing with terminal nub, sized via viewBox for responsive scaling
ChargeLevelFillAnimated SVG rect that grows to the current level value using Framer Motion
LightningBoltSVG icon rendered over the fill area only when the charging boolean is true
PercentageLabelNumeric text (e.g., 73%) centred over the battery, updating as the level changes
ColorIndicatorChanges ChargeLevelFill colour: red (0-20), yellow (21-50), green (51-100) via Framer Motion animate
Libraries it leans on
Framer MotionSmooth width/height transition on ChargeLevelFill and the charging pulse glow animation on LightningBolt
Tailwind CSSOuter card container, background, typography, and fill-* utilities for SVG element colours
SVG (inline)BatteryShell and LightningBolt drawn as inline SVG for crisp rendering at any size
Fork it and get it running
Forking takes under five minutes directly in the browser — no local environment needed. The Vercel Sandbox preview shows the animated widget immediately so you can verify the fill animation and colour transitions before changing anything.
Open the template and fork
Navigate to https://v0.dev/chat/community/921FeYZvkQL in your browser. You will see a live preview of the animated charging widget. Click the 'Fork' button in the top-right area of the preview to create your own editable copy under your V0 account. You need a free V0 account to fork — signing up takes under a minute.
You should see: A new V0 chat opens with the Charging Widget template loaded in your account.
Verify the animation in the Sandbox preview
The Vercel Sandbox preview loads automatically in the right panel. If the template includes a level slider, drag it across the range and confirm the ChargeLevelFill grows smoothly, the ColorIndicator switches colour at the correct thresholds (red at low, yellow mid, green high), and the LightningBolt appears or disappears correctly when you toggle the charging state.
Tip: If no slider is present, prompt the AI to 'add a range input wired to the level prop so I can test the widget manually in the preview'.
You should see: The battery fill animates smoothly, colour thresholds switch correctly, and the lightning bolt toggles with the charging state.
Adjust labels and sizing in Design Mode
Press Option+D (Mac) or Alt+D (Windows) to open Design Mode. Use it to change the label font, card background colour, or outer container size without spending any credits. Design Mode is the fastest way to match the widget to your existing dashboard colour scheme before using AI prompts.
You should see: Visual changes appear live in the preview with no AI credit usage.
Customise thresholds and add a data source with AI prompts
Return to the chat input and use the prompts from the prompt pack below to change colour thresholds, add a simulated charging loop, or wire the widget to a WebSocket or Supabase real-time subscription. Each AI prompt creates a new version saved in the version history — click the clock icon in the top bar to roll back if needed.
Tip: Name the specific components (ChargeLevelFill, ColorIndicator, BatteryShell) in your prompts for more precise changes.
You should see: Each prompt produces a diff preview; the Sandbox updates with the new version within seconds.
Deploy to a live Vercel URL
Click the Share icon in the V0 top bar, open the Publish tab, and click 'Publish to Production'. Vercel builds and deploys in under 60 seconds and returns a public URL. For IoT or device dashboard use, you will likely want to add the URL to your Vercel project settings and configure a custom domain from the Vercel Dashboard under Settings → Domains.
You should see: A live Vercel URL is generated that shows the animated charging widget to anyone with the link.
Export to GitHub for CI/CD integration
Open the Git panel in the V0 left sidebar, click Connect, choose your GitHub organisation, and select or create a repository. V0 creates a branch named v0/main-{hash} and opens a pull request automatically. Merge the PR to push the template code to your main branch, where it will trigger your existing Vercel GitHub integration and deploy automatically on every push.
You should see: A GitHub PR is open with the full template source; merging it deploys via Vercel's GitHub integration.
The prompt pack
Copy-paste these straight into v0's chat to customize the Charging Widgettemplate. Each one names this template's own components — no generic filler.
Add a charging speed label (Fast Charge / Standard)
Adds a contextual speed indicator below the battery to give users more than just a percentage number.
Add a ChargingSpeedLabel component below the BatteryShell that displays 'Fast Charge' when the level rises more than 2% per polling interval and 'Standard' otherwise. Style the label in a secondary text colour using Tailwind text-sm text-muted-foreground. Initially wire it to a simulated interval so it is visible in the preview without a real data source.
Change the ColorIndicator thresholds
Reconfigures the three-tier colour thresholds to tighter danger and warning bands.
Update the ColorIndicator logic in ChargingWidget so the ChargeLevelFill colour changes at these thresholds: red below 15%, amber between 15% and 40%, green above 40%. Adjust all Framer Motion animate colour values and any Tailwind fill-* conditional classes on the SVG elements accordingly — do not use bg-* classes on SVG rects as they have no effect.
Add a simulated charging animation loop
Makes the widget self-animating in the demo so you can visualise the full charging cycle without wiring up an external data feed.
Add a useEffect inside ChargingWidget that increments the battery level by 1% every 500ms when the charging prop is true, resetting to 0 when the level reaches 100, so the widget demo loops continuously without a real data source. Include a cleanup function in the useEffect that clears the interval on unmount to prevent memory leaks.
Accept live battery level via WebSocket
Wires the ChargingWidget to a real-time WebSocket data source so the battery level and charging state update live from an external device.
Replace the simulated increment with a WebSocket connection to ws://localhost:8080 that receives JSON messages shaped as `{ level: number, charging: boolean }` and updates the ChargingWidget state on each message. Move the `new WebSocket()` constructor inside a useEffect with a cleanup function that calls `socket.close()` on component unmount — this prevents the 'window is not defined' SSR error that occurs when WebSocket is created at module scope.Poll battery level from a Supabase real-time subscription
Replaces simulated or WebSocket data with a Supabase real-time subscription, making the widget react to actual database row updates from any backend that writes to device_status.
Create a Supabase table called `device_status` with columns: device_id (text), battery_level (integer), charging (boolean), and updated_at (timestamptz). Subscribe to real-time postgres_changes on the row matching the current `deviceId` prop using the Supabase client's `channel().on('postgres_changes', ...)` API, and update ChargingWidget's level and charging state on each incoming event. Add SUPABASE_URL and SUPABASE_ANON_KEY to the Vars panel in V0. Cancel the subscription in the useEffect cleanup to avoid memory leaks.Add a multi-device fleet view
Scales the single-device widget into a fleet dashboard so operators can monitor all devices at a glance with low-battery alerts.
Wrap ChargingWidget in a new DeviceGrid component that fetches all rows from the Supabase `device_status` table using a server component and renders one ChargingWidget per device in a responsive grid (3 columns on desktop, 1 on mobile). Add a colour-coded alert border (border-red-500) to any DeviceGrid cell where the battery_level is below 15%. Pass SUPABASE_URL and SUPABASE_ANON_KEY via the Vars panel.
Gotchas when you extend it
The failures people actually hit when they push this template past its defaults — and the exact fix for each.
ChargeLevelFill animates from the wrong start position on mountWhy: If the level prop is passed asynchronously (e.g., from a fetch or WebSocket), Framer Motion has no initial value to animate from and snaps to the final value instead of transitioning smoothly.
Fix: Add `initial={{ width: '0%' }}` explicitly on the ChargeLevelFill Framer Motion component so it always starts from 0% regardless of when the level prop arrives.
Set an explicit initial={{ width: '0%' }} on the ChargeLevelFill Framer Motion component so it always animates from 0% to the current level, even when the level prop arrives asynchronously after mountSVG fill colour not updating when applying Tailwind bg-* classesWhy: Tailwind's bg-* utilities set the CSS background-color property, which has no effect on SVG elements — SVG fill requires the fill CSS property or the SVG fill attribute directly.
Fix: Replace all bg-* Tailwind classes on SVG elements (BatteryShell rects, ChargeLevelFill) with the correct fill-* utilities (e.g., fill-green-500), or set the hex value inline on the SVG fill attribute.
Replace all bg-* Tailwind classes on SVG elements in BatteryShell and ChargeLevelFill with the correct fill-* utilities (e.g., fill-green-500) so SVG colours actually apply
ReferenceError: window is not defined when connecting WebSocketWhy: Creating `new WebSocket()` at module scope or inside the component render function runs during Next.js SSR where window does not exist, throwing a hard error that breaks the build.
Fix: Move all WebSocket construction inside a useEffect with a cleanup function that calls socket.close() on unmount.
Move the new WebSocket() call inside a useEffect with a cleanup function that calls socket.close() on component unmount, preventing the window is not defined SSR error
Widget fill shows a sub-pixel gap between ChargeLevelFill and BatteryShell border at non-integer levelsWhy: If the level prop is a float (e.g., 73.6), CSS width percentage calculations may not round consistently across browsers, leaving a 1-pixel gap between the fill rect and the shell border.
Fix: Round the level to the nearest integer using Math.round() before passing it to the ChargeLevelFill width calculation.
Round the battery level value to the nearest integer using Math.round() before using it to calculate the ChargeLevelFill width percentage, so there are no sub-pixel gaps between the fill and the BatteryShell border
Template vs. custom — the honest call
A forked template gets you far, fast. Here's where it holds up, and where you'll outgrow it.
The template is enough when
- You need a single-device battery indicator for a dashboard or status page where the level comes from a simple prop, polling endpoint, or Supabase subscription
- The widget is read-only — informational display, no user interaction beyond observing the animation
- You want the colour-threshold logic and Framer Motion fill animation built in without writing SVG animation from scratch
- You are building an IoT prototype or EV demo and need a polished battery visual in under an hour
Go custom when
- You need to visualise multiple battery cells individually (e.g., multi-cell UPS or EV battery pack with per-cell health data)
- The charging data comes from a hardware vendor SDK that requires native Node.js modules which fail in V0's esm.sh sandbox
- You need historical charge level charting alongside the live widget, requiring a Recharts integration and time-series data store
RapidDev can extend this widget into a full IoT device dashboard with Supabase real-time subscriptions, multi-device fleet views, and low-battery alert notifications — reach out if your use case goes beyond a single-device display.
Frequently asked questions
Is the Charging Widget template free to use?
Yes — all V0 community templates are free to fork. You need a free V0 account, but there is no charge for the template itself. Deploying to Vercel on the Hobby plan is also free for personal or side projects.
Can I use this template commercially, for example in a paid IoT dashboard product?
Yes. V0 community template output is yours once forked, with no licensing restrictions on the generated code. The underlying libraries — Framer Motion (MIT), Tailwind CSS (MIT), Next.js (MIT) — all permit commercial use. You are free to ship this as part of a paid product.
Why does my fork break in the V0 preview when I try to change the fill colour?
The most common cause is applying Tailwind bg-* classes to SVG elements. SVG rects require fill-* Tailwind utilities (e.g., fill-green-500) or an inline fill attribute — background-color has no effect on SVG. Prompt V0 to 'replace all bg-* classes on SVG elements in BatteryShell and ChargeLevelFill with fill-* utilities' and the colours will apply correctly.
How do I connect the widget to real battery data?
There are two paths: for browser-based real-time data use the WebSocket prompt from the prompt pack, which wires `{ level, charging }` messages to the ChargingWidget props. For a database-backed approach, use the Supabase real-time subscription prompt to subscribe to postgres_changes on a device_status table. Both prompts are in the prompt pack above with copy-paste instructions.
Does the template support multiple devices on the same screen?
The template ships as a single-device widget. To display multiple devices, use the 'multi-device fleet view' advanced prompt from the pack, which wraps ChargingWidget in a DeviceGrid that fetches all rows from a Supabase device_status table and renders one widget per device. For more than a handful of devices, a custom implementation with server-side pagination is recommended.
Why does the ChargeLevelFill snap to the final value instead of animating when I pass a level from a fetch?
Framer Motion needs an explicit initial value to animate from. When the level arrives asynchronously (after the first render), Framer Motion has nothing to transition from and jumps directly to the target. Fix: add `initial={{ width: '0%' }}` to the ChargeLevelFill motion component so it always starts from empty and animates up to the fetched level.
Can RapidDev extend this template for a production IoT dashboard?
Yes — RapidDev builds production IoT dashboards from V0 prototypes like this one, adding Supabase real-time subscriptions, multi-device fleet views, low-battery alert notifications, and historical charge charting. Reach out at rapidevelopers.com if your use case has grown beyond a single-device display.
Will the WebSocket approach work when deployed to Vercel?
The ChargingWidget itself (the client component) will connect to WebSocket fine from Vercel — it runs entirely in the browser. Your WebSocket server (ws://localhost:8080 in the prompt) must be hosted separately, for example on a Fly.io or Railway instance, since Vercel serverless functions do not maintain persistent WebSocket connections. Update the WebSocket URL to your hosted server URL in the Vars panel after deployment.
Outgrowing the template?
RapidDev turns v0 prototypes into production apps — real auth, database, and payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.