Before we explain how we got there, this is what it looks like in practice:
Before we explain how we got there, this is what it looks like in practice.
The privacy filter in action On the left side, the privacy filter is disabled. This is the default behavior, and the amounts are in cleartext. While on the right side, the privacy filter is enabled, and the amounts are redacted.
You may want to jump to the end of the article to skip the failed attempts and technical details and see what solution we finally settled on.
What is redacting and why do I care?
For non-native English speakers, redacting refers to making a piece of information unreadable. It is a synonym for obscuring, hiding, masking, censoring, or blurring — whichever method works.
For us here at Simon, in the context of a mobile app built to track and analyze money, being able to redact amounts is very convenient.
Say you’re in line at the airport, and you want to quickly record a transaction or check something about yesterday’s gathering with friends. Well, you’d be right to think that last night’s beer budget is no one’s business.
This is one of many situations in which redacting amounts in your digital ledger proves useful. It enables you to feel comfortable and safe while browsing through your accounts in plain sight. It’s very much like those physical privacy stickers for smartphone that turn the screen black when not looking at it from the right angle.
So how did you redact amounts in your app?
Let me start with an overview of our front-end stack: HTML5, CSS3, and JS (ES6) with sprinkles of Livewire and AlpineJS. No trendy framework, no cosmic tooling, and sub-nanosecond build time.
Our mobile app is, in its entirety, a PWA built with HTML, CSS and JS designed specifically to offer a native look and feel on mobile devices.
First implementation: CSS pseudo-elements
Our first experiment consisted in using CSS pseudo-elements to cover amounts with an opaque rectangle.
Here’s a code excerpt:
Redacting amounts with CSS pseudo-elements Let’s see what’s going on:
- When the privacy filter is enabled, a .redacted class is added to the body.
- A new set of CSS rules is then applied to all the amount elements on screen.
- Every amount element gets a position: relative rule to enable relative positioning of the :after pseudo-element used for masking the numbers.
- A :after pseudo-element is created and positioned on top of every amount element to mask their content with a black background.
- We chose not to use a simple opaque background in lieu of fancy blurs and images as we found that cross-browser support wasn’t there yet.
At first glance, that looks simple, lightweight, easy to apply on all the interfaces, and just perfect, right? Well, in practice, this approach has quite a few caveats:
- If an amount element already has a :after pseudo-element, we can’t apply our mask without breaking something else.
- Creating a :after pseudo-element for every single amount element creates a lot of visual spaghetti in the browser devtools, with forever-expanding trees, and pseudo-elements everywhere — none of which we know whether they’re here for regular styling or for redacting an amount. All in all, it just made for a bad developer experience.
- Not all HTML elements support pseudo-elements (input elements, as an example).
- Depending on accessibility settings, system font tweaks, and custom browser rendering, using width: 100% and height: 100% may not always work well to fully cover all the amount elements with an adequately-sized mask.
For all those reasons, we decided to abandon that implementation, and here we are — back to square one.
Second implementation: JS dynamic masking
That title sounds terrible already, and to no one’s surprise, so was the implementation.
For that second attempt, we thought we’d use JS to dynamically perform the following operations:
- Locate all the amount elements on screen.
- Find their real size in pixels, and their (x,y) coordinates.
- Create their clone element with the exact same size and coordinates.
- Append those clone elements to the DOM, with a higher z-index and a position: absolute rule to mask the underlying amount.
At first, it solved two issues very well:
- No more interference with already-existing CSS pseudo-elements.
- No more undersized or oversized masking due to accessibility settings and whatnots.
But boy did it come with new problems:
- JS takes time to run, and even a few milliseconds is enough to cause flashes of unmasked amounts — defeating the very purpose of that feature. (a.k.a the well-known issue of flashes of unstyled content in JS-heavy apps)
- The previous issue is even more emphasized on older mobiles or browsers with a slower JS engine, resulting in a flash of unmasked data on every single page load — making the app feel unreliable, glitchy, and unpleasant.
- Any upstream JS exception happening before the mask was applied would crash the whole bundle, and prevent that part of the code from running at all, therefore disabling the mask altogether. (This could be solved with better code isolation and bundling, or just better exception handling, but I thought it was worth mentioning to whoever would like to go down that path).
- Lazily-loaded elements (a common approach we use to reduce the load on bandwidth and CPU on mobile) wouldn’t be masked at all (because our JS code is executed when the page has finished loading, but not after). So we would have to adapt our masking code to always listen for new DOM node insertions or AJAX call terminations.
After a lot of internal discussions, and enlightened by the aforementioned discoveries, we concluded that this approach would be:
- Neither optimal nor convenient, neither for us developers nor for our users.
- Difficult to maintain, and just too complex for what seemed to be a simple feature.
- Handled by the wrong part of our stack.
There we go again.
Third implementation: Server-side rendering
Riding on the unsexy-yet-reliable tech tide, we chose PHP for our back-end. Also given that we support all 150+ in-use currencies, and multiple languages, we had to implement a few functions to format and output amounts.
At this point, you can safely assume we have a formatAmount function that we use virtually everywhere in our views, with reusable components.
Here’s a simplified example for reference:
Code to output a transaction with a formatted amount in PHP So the intuitive solution would be to wrap all those formatAmount calls with a redact call that takes a formatted amount string for argument, and according to user settings, outputs that string unchanged (if the masking is disabled), or replaces it with symbols to obscure the amount (if the masking is enabled).
Like so:
Code to redact an amount in PHP Alright, so we finally got our solution, or have we? Let’s see:
- Everything is server-side rendered : no more flashes of unmasked content, no more runtime interferences, no lazy-loading gymnastics, and no impact on the mobile’s CPU.
- No more issues with sizing, system font tweaks, and accessibility settings, since we’re not dealing with amounts anymore, but with a fixed-size replacement string (e.g. ***).
You know how the saying goes, when something sounds too good to be true…you go back to coding in Vue. Jokes aside, we found that this solution also came with limitations for our use-case:
- Since the amounts were replaced with masking symbols, we wouldn’t be able to perform arithmetic operations on the front-end anymore. Although 99% of our computations are done server-side for efficiency, that is still a deal-breaker for rendering charts, as an example, or for playing with numbers in a way that doesn’t require a back-and-forth with the server.
- If we wished to mask only the number, and later format it with a currency on the front-end, it wouldn’t work with native localization and formatting methods, since *** is not a number that they can recognize and format into an amount.
- Unmasking the amounts at will with a gesture is not easily feasible anymore, since the amounts were overriden with asterisks. And unfortunately for us, this is a sub feature we wished to implement : enabling the user to disable the mask by tapping 7 times on the header, and re-enable the mask after a few seconds, for a quick glance.
- In terms of development experience and good practices, it felt wrong to call redact (and subsequently, check whether the masking filter is enabled) for every single amount on screen. Also having to wrap hundreds of formatAmount everywhere in our code base felt like introducing debt (pun intended).
Alright so, I’ll spare you the cliff-hanger, that didn’t work either, and we chose to move on. Now keep reading for the grand finale.
The solution
Finding no solace in our endeavours, we sought for a more exotic, unheard-of technique. And it finally descended upon us, in all its glory: a font. But not just a font, a redacted font.
A redacted font is a font that is designed for wireframing and prototyping. Whatever your text is, if it’s rendered with a redacted font, the only thing you’ll see is either square blocks, or illegible manuscript gibberish.
Here’s a picture for the untrained eye:
A mix of renderings with a redacted font - Credits to Christian Naths The redacted text in the picture might be the precise itinerary to a chest filled with $100,000 worth of gold, who knows?
Given how well it works at visually masking text while promising to leave the code untouched, we chose to give it a try.
And so far, using a redacted font has had all the benefits with no compromise for our use-case:
- Simple to maintain and implement globally across our app. Just drop in a font file, add a few CSS rules to apply the font conditionally based on a body-level class, and call it a day.
- No more messing with PHP, JS or pseudo-elements, and no issues with lazy-loading either.
- Zero impact on the developer experience, the browser devtools, or the DOM tree.
- Zero impact on the mobile’s CPU, and no more runtime quirks.
- The font is a single file that weighs less than 5kb, and it is loaded only once on the very first page load. It is then cached forever. So there’s no flash of unmasked amounts or any concern about bandwidth.
- The DOM still holds all the amounts in cleartext, so we can easily unmask everything at will by reverting to the original font, render charts, or perform arithmetic operations.
- Every accessibility setting, system font tweaks, and browser rendering magic all seemed to play well with the redacted font, without having to do anything on the CSS / JS side.
So that’s really just it, a redacted font.
Oh and if you’ve made it this far, here’s the one and only code to redeem your $2,500.00 gift: Just kidding, go share that article to whoever will find it interesting! And try that code too, just in case, you know: REDACT-30
.png)


