Anatomy of a Liquid Fill Goal Widget: From Sketchbook to a Live StreamElements Build
Heads up: TwitchElement sells stream overlays and widgets, and some links in this post go to our shop. The advice stands on its own either way.
You've seen the clip. Sub drops, the jar glugs up another centimeter, chat spams the emote. Then you go looking for that widget and every tutorial starts at "paste this URL into a browser source." Nobody shows the part before that. This is the part before that: one liquid fill goal widget, from a pencil sketch on the back of a shipping label to live JS running in StreamElements, including the two bugs that ate a Saturday.
TL;DR
- The whole build is four stages: sketch and legibility test, vector prep with a separate fill layer, fill-percentage math bound to StreamElements session data, then browser source QA in OBS Studio.
- The math is boring and that's the point: current / target clamped between 0 and 1, driven into a CSS transform, not a height change. Height changes are what make a liquid fill goal widget look like a loading bar.
- Most "my goal widget isn't updating" problems are cache and session-data problems, not code problems. We list the exact fixes.
- A $15 Etsy template is genuinely fine if you want the look. Owning the code is about changing it at 2am without emailing anyone.
Disclosure up front: this is a walkthrough of our own internal liquid fill goal widget build at TwitchElement, not a paid review of somebody's Etsy listing. And it's one build (a liquid jar goal), not a universal StreamElements tutorial that maps onto every widget type you'll ever touch.
Why We Sketched This Goal Widget Before Opening a Single Design File
I've streamed since 2020, somewhere around 8,000 hours at this point, and the thing I keep relearning is that viewers don't read overlays. They glance. Half a second, maybe less, usually while a phone is doing something else in their hand. LATAM audiences especially: a big slice of my chat watches on mobile data with the stream at 480p, and a lot of that is one-handed, vertical, thumb over the corner of the frame.
So the first test for any liquid fill goal widget isn't "does it look cool in Figma." It's: at 180 pixels tall on a phone, can someone tell whether the goal is at 20% or 80% without reading a number?
That's why we sketch. Pencil, three boxes, garbage lines. I drew the jar shape six times, and four of those were unusable because the silhouette got wider at the bottom. That means the first 30% of the fill eats way more visual area than the last 30%. The liquid climbs fast then stalls. It feels like the goal is going backwards even when it isn't. Straight-walled or slightly tapered-inward shapes read linearly. Round-bottom jars lie to your viewers.
Real talk: We've shipped goal widgets to 500+ streamers across Twitch, YouTube, Kick and TikTok, and the single most common thing people ask us to change post-purchase isn't color. It's size. They bought a beautiful jar and then found out it's unreadable on the mobile Twitch app. A liquid fill goal widget that reads perfectly at 400px and turns to mush at 180px is a failed widget. Design for the phone first and the desktop version takes care of itself.
The sketch stage also settles what the widget is actually for. A Follower Goal or Sub Goal behaves differently from a donation total. Follower counts move in 1s. Tips move in weird decimals. Hype train progress moves in violent jumps and then resets to zero, which looks broken on a liquid animation unless you script the drain. We picked a sub goal for this build because subs increment cleanly and the celebration moment is well-defined.
The Liquid Fill Goal Widget Math Nobody Explains: Turning Sub Counts Into Percentage Animation
Here's the part every video skips. Getting a number from StreamElements is easy. Turning it into liquid that behaves like liquid is where it falls apart, and every liquid fill goal widget lives or dies on this one chain of four lines.
The naive approach: set the fill element's height to the percentage. Done, right? No. Animating height forces layout recalculation on every frame, and in a browser source running at 30fps alongside your game capture, you will see it stutter. It also means your wave graphic gets squashed instead of riding on top of the liquid.
What we do instead: the fill layer is full height, always. We move it with translateY and let a clip container hide the overflow. The wave sits welded to the top edge of that layer, so it rises with the liquid instead of stretching.
- Normalize. const pct = Math.min(Math.max(current / target, 0), 1); Clamp it. If you don't, an overfunded goal shoots the liquid out through the lid and off-canvas, which we found out live on stream.
- Map to travel distance. const offset = (1 - pct) * fillHeight; At 0% the fill sits fully below the visible window; at 100% it sits flush at the top.
- Push it with a transform. fillEl.style.transform = 'translateY(' + offset + 'px)'; plus a CSS animation on the container: transition: transform 900ms cubic-bezier(.22,.61,.36,1); That easing curve matters: linear transitions look like a progress bar, eased ones look like weight.
- Layer two waves. Two copies of the same wave SVG, different widths, animated horizontally at different speeds (we used 7s and 11s loops) with transform: translateX. That desync is 90% of why it reads as liquid and not as a shape.
- Overshoot on event. When a new sub lands, we bump the target offset by about 6px past its resting point and let it settle back. Real liquid sloshes. Skipping this is the difference between "nice" and "how did you do that."
None of this needs a WebGL shader. People search for a liquid fill shader goal bar OBS setup and assume there's some exotic pipeline involved. There isn't. A liquid fill goal widget built from two SVG waves, a clip mask, and a transform will out-perform a shader in a browser source because it stays on the compositor thread instead of fighting your encoder for GPU time.
From Vector to StreamElements: The Exact Files We Uploaded to the Custom Widget Editor
Vector prep is where builds die. Our rule: the artwork for a liquid fill goal widget ships as three separate exports, never one flattened file.
| Layer | Format | Why separate |
| Container / jar outline | SVG | Sits above the liquid with the glass highlight; needs its own z-index |
| Liquid body + wave crest | SVG (single path) | This is the element that gets translated; must be one node |
| Back shadow / inner glass | PNG @2x | Soft gradients bloat SVG file size for no gain |
Then into StreamElements. Open your overlay editor, add a Custom Widget, and you get four tabs: HTML, CSS, JS, and Fields. The Fields tab is the one people ignore, and it's the whole reason a streamelements custom widget is worth building over a hardcoded HTML file.
Fields is JSON. You define inputs, StreamElements renders them as a settings panel, and then anyone (including future you) can recolor the widget without touching code. For a liquid fill goal widget, ours looked roughly like this:
- "liquidColor": { "type": "colorpicker", "label": "Liquid color", "value": "#38d9a9" }
- "goalTarget": { "type": "number", "label": "Sub goal target", "value": 50 }
- "waveSpeed": { "type": "slider", "label": "Wave speed", "min": 1, "max": 20, "value": 7 }
In the JS tab, you listen for onWidgetLoad to grab the initial session data, and onEventReceived for live updates. The session object is where your current sub count already lives; you do not need to call the Twitch API yourself. The StreamElements Help Center documentation on custom widgets spells out the event payload shapes; read it once properly and you'll stop guessing at property names.
One gotcha that cost me twenty minutes: field values come through as obj.detail.fieldData.goalTarget on load, and if you typo the key in the Fields JSON, StreamElements silently gives you undefined instead of erroring. Your liquid just sits at the bottom forever, looking fine, doing nothing.
Where a Liquid Fill Goal Widget Breaks: The Browser Source Bugs We Hit (And Fixed)
The widget worked in the StreamElements preview. Then I dropped it into OBS Studio as a browser source and it broke in three separate ways.
Bug 1: the widget shows the old goal after you change the target
Classic cache. The browser source keeps the old page. Fix: right-click the source, Properties, tick Refresh browser when scene becomes active, then hit Refresh cache of current page. If it still sticks, the overlay URL itself needs regenerating in StreamElements. We've seen this on maybe one in five setups where someone duplicated an overlay instead of editing it.
Bug 2: waves stutter or drop to ~10fps during gameplay
Two causes, usually stacked. First, Shutdown source when not visible being off means every hidden goal bar keeps rendering behind your scenes. We had four overlays loaded and the machine was compositing all of them. Second, if your waves animate with background-position or left, they're on the main thread. Move everything to transform and add will-change: transform;. The OBS Studio browser source knowledge base article covers the custom FPS and hardware acceleration settings worth checking too. Capping the source at 30fps instead of 60 halved our CPU hit with no visible difference.
Bug 3: fill doesn't sync with the real sub count
This one's sneaky. If you're testing with StreamElements' test events, the session counter increments. But if you've reset your session and the widget only reads onEventReceived without handling onWidgetLoad, it starts at zero every refresh. Your goal appears to reset mid-stream. Always seed the initial state from session data on load, then increment from events.
Real talk: This won't save a machine that's already pinned. If you're streaming on a single-GPU setup at 1080p60 with CPU encoding, a heavy animated overlay stack is not your friend, no matter how well it's coded. Budget one browser source for the liquid fill goal widget, kill the rest, and check your dropped frames before you blame the widget.
Etsy Template vs Custom Build: What Actually Changes When You Own the Code
I'm going to argue against my own interest here. If you want a liquid fill goal widget and you want it tonight, buy one. A $15 template with a 2 Minute Setup Guide will get you 90% of the visual result with 2% of the effort, and there's zero shame in that. We sell them. Plenty of people should just buy them.
| $10–20 template | Custom build | |
| Time to live | ~5 minutes | 8–20 hours for a first build |
| Cost | Fixed, cheap | Your weekend, or $200+ commissioned |
| Uniqueness | Shared with everyone who bought it | Yours |
| Changing the wave speed at 2am | Only if the seller exposed the field | One line of CSS |
| Maintenance when StreamElements changes something | Wait for the seller | You fix it |
| Best for | Getting a clean look now | Brand identity, reselling, learning |
The real difference isn't looks. It's that template widgets are frozen. When you want the jar to drain during a hype train, or you want the liquid to change color past 75%, or a sponsor wants their exact hex, a template says no and a custom build says fine. That's it. That's the entire value of owning the JS widget code behind a liquid fill goal widget.
The middle path that most people should take: buy a well-built widget that exposes real fields, and learn enough CSS to nudge it. Our jar and hype jar goal widgets and the broader goal bars collection ship with the field panels unlocked for exactly that reason. You can customize it in OBS Studio without opening the JS tab, and open the JS tab when you're ready.
The Sketch-to-Ship Widget Build Checklist
This is the exact sequence we ran for the liquid fill goal widget, in order. Save it. It generalizes to basically any custom goal widget or goal overlay design you build after this one.
- Silhouette test. Sketch the container shape. Check that equal volume increments read as equal visual height. Kill anything that bulges.
- Mobile legibility check. Export a flat mockup, view it at 180px tall on your actual phone, in dark mode. Can you read the fill level in half a second? If no, go back to step 1.
- Layer split. Export container, liquid+crest, and shadow separately. Liquid must be a single SVG path node.
- Fill math first, art second. Build the clamp, offset and transform chain with plain colored rectangles. Confirm it animates smoothly before any art goes in.
- Fields JSON. Define every value you might want to change later: color, target, wave speed, font, celebration toggle. Typo-check the keys against your JS.
- Bind both events. Seed state in onWidgetLoad, update in onEventReceived. Test with a session reset to prove it doesn't zero out.
- Browser source QA. Refresh-on-activate on, shutdown-when-hidden on, custom FPS at 30, hardware acceleration checked. Watch dropped frames for five minutes with a game running.
- Live smoke test. Run it on a real stream at 50% fill, 99% fill, and overfunded. The overfunded case is the one that breaks. Ask chat if they can see it on mobile.
Build It or Grab One That Already Works
If you've got a free weekend and you want to understand what's happening inside your overlay, run the eight steps above. The fill math is 15 lines. The hard part is the silhouette decision and the browser source QA, and now you know both.
If you'd rather spend that weekend streaming, shop the liquid fill goal widget collection. Every liquid fill goal widget in there uses the transform-based fill and desynced wave approach described above, with the field panel unlocked so you can recolor and retarget without touching code. They pair cleanly with our stream overlay packages if you want the whole layout to match, and there are subathon and timer widgets built on the same animation logic.
Got a widget behaving badly? Drop a screenshot in the TwitchElement Discord and I'll tell you which of the three bugs above it is. It's almost always one of the three. You can also catch the builds happening live over on my Twitch channel, usually with worse lighting than the screenshots suggest.
Want this on your stream tonight?
Browse ready-made widgets built for StreamElements and OBS, or start with a free one to try the setup first.
Frequently Asked Questions
How do you make a goal bar fill up on Twitch?
The goal bar reads a count from your bot or overlay platform (for StreamElements that's the session data object) and converts it to a percentage with current divided by target. That percentage drives the visual: a width change for a standard bar, or a vertical transform for a liquid fill goal widget. The key detail most guides skip is clamping the value between 0 and 1 so an overfunded goal doesn't render off-canvas.
Can you customize a StreamElements widget with your own code?
Yes. Add a Custom Widget in the overlay editor and you get full HTML, CSS, JS and Fields tabs. The Fields tab takes JSON that StreamElements turns into a visual settings panel, so you can expose colors, targets and speeds as user-editable options. You listen for onWidgetLoad to get initial session data and onEventReceived for live events.
Why isn't my StreamElements goal widget updating?
In our experience it's one of three things. Browser source cache is serving the old page. Refresh the cache and enable refresh-when-scene-becomes-active in OBS Studio. Or your JS only handles onEventReceived and never seeds state from onWidgetLoad, so the widget resets to zero on every refresh. Or a key in your Fields JSON doesn't match the key your JS reads, which fails silently as undefined.
Is it better to buy or build a custom Twitch overlay?
Buy if you want a clean look live tonight and you're happy with the options the seller exposed. A $10 to $20 template gets you most of the way for almost no effort. Build if you need a specific brand identity, want to modify behavior later without waiting on anyone, or want to resell your work. The realistic middle path is buying a widget with unlocked settings fields and learning enough CSS to adjust it yourself.
Does a liquid fill goal widget hurt stream performance in OBS?
It can, but usually because of how it's animated rather than because it's a liquid fill. Animating height, left or background-position runs on the main thread and stutters; animating transform stays on the compositor and is far cheaper. Cap the browser source at 30fps, enable shutdown-when-not-visible, and avoid stacking four hidden overlays that keep rendering in the background.
One question for you: Would you rather pay $15 for a liquid-fill widget that looks identical to 200 other streamers' overlays, or burn a weekend building one that's genuinely yours? And does that answer change if you're under 50 average viewers?
About the author: Nargis and her team have been coding these since 2020 and founded TwitchElement in 2019, serving 500+ streamers across Twitch, YouTube, Kick, and TikTok. Catch the streams on Twitch, the research on Google Scholar, or come hang out in the TwitchElement Discord.
Get new free widgets first
One email when we release a new freebie or widget. See the current freebies.



