Wallet in Telegram is a web app pretending to be a native one. Here is what happened when Apple shipped Liquid Glass and I tried to bring an insane tab bar into the browser.
September 20, 2026
The prototype itself, running. Tap the tabs, drag the indicator sideways, scroll the list under the bar.
Time to admit it: I badly miss designing native apps.
The ache comes from designing a web app that mimics a native one. The web is noticeably short of the range of things native hands you straight out of the box.
Wallet, for instance, was laid out fairly linearly at the very beginning — why would five screens need depth? And then a tab bar shows up in the app, which quietly implies moving all the accumulated legacy onto new rails. So you end up writing documentation for how stack navigation ought to behave, explaining on your fingers a theory that normally already works.
Who knew that wouldn't be the last thing to lose your mind over. Because there is Apple, waving from across the room with Liquid Glass and, suddenly, an insane tab bar.
My research didn't stop at looking at the refraction effect, reading about the math with genuine interest, and crying. Because web performance would suffer catastrophically from a hand-rolled version of all that. We cannot afford to slow the app's navigation down. Okay — so what can we do?
Behind the current native tab bar, on top of the glass itself, you can spot several things at once:
Cross out the glass, cross out the reflection on touch, simplify the blending — and the rest looks more or less within reach for reproducing the experience.
Point two is where I flew off the rails. I was honestly just curious how cleverly it had been done, because somewhere deep down I thought you could make white whiter through Display P3 (lol). And somewhere out on Twitter I saw that it is HDR at 200 nits. Since 26.2, thank god, less. That 200-nit figure was pulled out empirically from Blackmagic Design.
HDR on the web is available for video. So I thought about shoving a chunk of 200-nit video into a square and shining it through on press. But tricks like that don't get along with ordinary opacity, and recoloring it wouldn't be easy either. So I rejected the whole idea.
You can poke at HDR on the web yourself here: dt.in.th/HDRQRCode.
By the way, enthusiasts have dug up the configuration of Liquid Glass itself. Under the hood the folks run, for example, all colors through YUV. I had only ever met that kind of optimization joke before somewhere in Telegram's sticker optimizations.
God bless Cursor and ChatGPT, because thanks to them it became far more interesting, and far clearer, to figure out React and modern CSS and JS. Reading this stuff and working it out is more interesting than writing code blind.
The tab bar is the most stuffed component I have ever designed.
That is how <GlassEffect /> first appeared in our project. At its core it is just a cunning configuration of blur, border and blend modes, so the thing sits a little more responsively and a little more believably in space. Probably one of the fairly simple things I handed over to the engineers that is now used in production.
The best part of it is the escape hatch: on Apple's own stack the browser will hand you the real material, so you throw your own blur away.
/* TabBar.module.scss */
:global(body.apple) & {
border-radius: 100px;
padding: 4px;
background: rgb(
from var(--tg-theme-section-bg-color) r g b / 70%
);
backdrop-filter: saturate(300%) blur(8px);
box-shadow: 0 0 10px 0 rgb(0 0 0 / 10%);
@supports (-apple-visual-effect: -apple-system-glass-material) {
-apple-visual-effect: -apple-system-glass-material;
backdrop-filter: none;
}
}
The hairline on top of it is not a border. It is a gradient sitting in a one-pixel ring, cut out with mask-composite: exclude, so the light can fall off along the rim instead of being uniform.
The tab bar is a layer cake. It has two layers of icons and labels. One is the active one, the other is inactive. But so that the indicator can change its color beautifully — not with a primitive transition — it uses clip-path, whose coordinates change when the tab changes.
// useIndicatorDrag.js
const segmentPercent = 100 / tabsLength
const indicatorWidth = `calc(${segmentPercent}% + 7.33px - 4px)`
const indicatorLeft = `calc(${segmentPercent * activeIndex}% - ${
3.67 * activeIndex
}px)`
const clipLeft = indicatorLeft
const clipRight = `calc(100% - (${indicatorLeft} + ${
indicatorWidth
}) - 2.33px * ${activeIndex})`
// …and while a finger drags, the same window follows the pointer
`inset(0 ${clipRight} 0 ${clipLeft} round 100px)`
Both layers hold identical markup. The active one is simply clipped to the pill, so the color doesn't cross-fade — it gets wiped on, travelling with the indicator.
My favorite hack: the gradient background. It is not just a rectangle with a gradient in it — there is a hole in it, exactly the size of the tab bar. The whole problem is the layering. Otherwise the blur would be smearing colors that have already been passed through the gradient, and the entire effect of the blur falls apart.
The beauty of it is that this is merely an SVG with a cutout. One that knows how to change its size dynamically.
// GradientMask/index.jsx — the fade, minus the tab bar
<mask id={maskId} maskUnits="userSpaceOnUse">
<rect x="0" y="0"
width={overlayWidth} height={overlayHeight}
fill="white" />
<rect x={left} y={top} width={innerWidth} height={innerHeight}
rx={rx} ry={ry} fill="black" />
</mask>
<rect width={overlayWidth} height={overlayHeight}
fill={`url(#${gradId})`} mask={`url(#${maskId})`} />
The frame at the top of this page is that prototype itself, running — the same React component that ships, spring transitions and all. Drag the indicator sideways and you get the last piece: the clip window detaches from the active tab, follows your finger, and snaps to whichever tab it is nearest when you let go.
Spent on the project:
What came out:
Source code: GlassEffect · wallet_animations
Originally posted in Whistles and Papers.