Back to the journal

Your form's submit button is doing nothing for the three seconds after someone clicks it

We audit a lot of landing pages for South African businesses, and there's one conversion killer we see almost every time: the submit button that gives zero feedback after a click. A user fills in their details, taps 'Submit', and then… nothing visible happens for two to four seconds while the request travels to the server. In that dead air, roughly half of users will tap again. Some will tap three times. You end up with duplicate leads in your CRM, double subscription charges, or — worst case — the user assumes it's broken and leaves entirely.

The fix is embarrassingly simple, and it works on every framework. On click, immediately do three things: disable the button, swap the label to something like 'Sending…', and show a small spinner or progress indicator. This takes about fifteen minutes to implement properly. If you want to be thorough, add a timeout fallback — if the server hasn't responded in ten seconds, re-enable the button and show a 'Something went wrong, please try again' message. That single pattern handles flaky mobile connections, which are common on South African networks where a request might hang without failing cleanly.

The broader principle is that perceived responsiveness is a trust signal. A form that visually acknowledges every interaction — focus states on inputs, inline validation before submission, and an animated button state on submit — tells the user that this is software built by people who care about the details. That impression carries straight through to whether they trust you with their credit card number or personal information. If you maintain a landing page with any kind of lead capture or checkout form, spend fifteen minutes this week adding a proper loading state to your submit button. It is the highest-return UX change you are not making.

Your pull request template is probably training your team to write worse code reviews

Most teams we work with have a pull request template. It usually has a checklist: 'Does it have tests? Did you update the docs? Is there a Jira ticket linked?' On paper this looks disciplined. In practice, it turns code review into a box-ticking exercise where reviewers scan for checkmarks instead of reading the actual diff. We've watched senior developers approve PRs in under ninety seconds because every checkbox was filled, missing logic errors that sat in production for weeks.

The problem isn't templates themselves — it's that they front-load administrative compliance and bury the thing that matters: does this change make sense? A reviewer's attention is finite. If the first thing they see is a formatted list of procedural items, their brain shifts into audit mode rather than critical-thinking mode. The best reviews we've seen happen when the PR author writes two or three sentences explaining why they made the trade-offs they made, not whether they remembered to tick a box. That context invites genuine scrutiny.

If you use a PR template, try stripping it back to one required field: a short prose summary of what changed and what you're unsure about. Move your compliance checks into CI — let automation verify that tests exist, that linting passes, that tickets are linked. Free up your human reviewers to do the one thing automation can't: evaluate whether the approach is sound. You'll get slower approvals and fewer production surprises, which is exactly the trade-off you want.

Charging from day one filters out the feedback that wastes your first six months

Most founders we work with want to launch their MVP as a free beta. The logic sounds right: remove friction, get users in, collect feedback, then figure out pricing later. But free users behave fundamentally differently from paying ones. They'll say they love your product in a feedback form and never open it again. They'll request features they'd never actually pay for. Six months in, you've built a roadmap shaped by people with no skin in the game.

Charging even a small amount — R99 a month, a once-off R500, whatever fits — acts as a filter. The person who enters their card details is telling you something no survey can: this problem matters enough to spend money on. Their feature requests carry weight because they're voting with their wallet. Their churn tells you something true about value delivery, not just about whether your onboarding email was engaging enough. You don't need thousands of these users. Five paying customers who actively complain will teach you more than five hundred free ones who passively drift away.

This doesn't mean your pricing has to be final, or even close to final. Grandfather your early users at whatever rate you start with and adjust later. The point isn't to optimise revenue on day one — it's to make sure every signal you're building on is coming from someone who has made a real commitment. If nobody will pay anything at all for your MVP, that's not a pricing problem. That's a validation answer you need sooner rather than later.

Your CSS animations are janking because you're animating the wrong properties

There's a specific reason some websites feel silky and others feel like they're rendering through treacle, and it often comes down to which CSS properties you're transitioning. Animating `width`, `height`, `margin`, or `top` triggers layout recalculation — the browser has to figure out where everything on the page sits, then repaint it, then composite the layers. That's three expensive steps running sixty times a second. On a mid-range Android phone over Vodacom's network, the dropped frames are immediately visible.

The fix is unglamorous but effective: restrict your animations to `transform` and `opacity`. These two properties skip layout and paint entirely, running only on the compositor thread. A sidebar that slides in via `transform: translateX()` instead of animating `left` will feel dramatically smoother on the exact devices your South African users are actually browsing with. If you need to animate size changes, `scale()` inside a transform often gets you close enough visually without the layout thrash.

You can verify this yourself in Chrome DevTools. Open the Performance panel, record while triggering an animation, and look for long green (paint) and purple (layout) bars. If they spike on every frame, you've found your jank. The `will-change` property can help the browser optimise ahead of time, but apply it sparingly — overuse eats GPU memory, which is already tight on budget devices. Audit your three most-used animations this week: if any of them touch layout properties, swap them to transforms and test on a real phone. The difference is immediate.

Flutter's platform channel is where your beautiful cross-platform app starts lying to the user

Flutter gives you a single codebase that looks identical on iOS and Android, right up until you need to talk to the actual device. Biometric auth, NFC, Bluetooth peripherals, camera configuration beyond the basics — all of these drop you into platform channels, where you're writing Swift on one side and Kotlin on the other, marshalling data through a message-passing layer that has no type safety across the boundary. The beautiful unified codebase quietly forks into three codebases: Dart, iOS native, and Android native. Most teams don't budget for this.

The subtle damage is in behaviour differences you don't notice during development. We recently debugged a payments integration where the native card scanner plugin returned subtly different error codes per platform. On Android it threw a timeout; on iOS it returned a cancellation. The Dart layer treated both as generic failures, so iOS users saw 'payment failed' when they'd simply closed the scanner. Nobody caught it because the Flutter UI looked the same on both platforms during QA — the divergence lived underneath, invisible until real users complained.

Before you pick Flutter for a project that touches native hardware or platform-specific SDKs, map every integration point that will need a platform channel. Count those as separate features with separate testing budgets. If more than a third of your core user flows cross that bridge, you're not building a cross-platform app — you're building two native apps with a shared UI layer, and you should plan your timeline and team accordingly.

The scope document that saves your project is the one written after the client says yes

Most studios treat the scope document as a sales tool. You write it before the deal closes, padded with enough detail to justify the quote, and then never look at it again. The version that actually matters is the one you write together with the client in the first week of work — after the contract is signed, after the excitement settles, after you can have an honest conversation about what's in and what's out. We call this the working scope, and it's a different animal from the proposal.

A proposal scope is optimistic by design. It lists features in broad strokes because you're still earning trust and don't want to say no too early. A working scope is blunt. It names specific screens, lists the integrations you will and won't build, and says things like 'user roles are limited to admin and member — no custom permissions in this phase.' It forces both sides to confront the gap between what was imagined and what the budget actually covers. That confrontation in week one is painless. In week eight, it's a crisis.

At MoJuKo we schedule a dedicated scope workshop in the first five billable days of every new project. The output is a single shared document — not the proposal, not a Jira board, just a plain list of what's being built and what's explicitly deferred. Both parties sign off. When a client later asks for something outside that list, the conversation isn't adversarial; it's just a reference check. If you're running fixed-price work without this second document, you're relying on memory and goodwill, and both run out faster than budget.

That spreadsheet your ops manager emails around every Monday is a database with no backup and no rules

Every business has at least one. A Google Sheet or Excel file that tracks stock, or jobs, or supplier quotes. Someone manually updates it, emails it to three people, and those three people sometimes edit their own local copies. By Wednesday, nobody is sure which version is current. The spreadsheet works right up until the moment it quietly stops working — a deleted row, a pasted formula that overwrites a column, a filter left on that hides fifty records. You only discover the damage when a client phones to ask where their order is.

The fix isn't always a full custom system. Often it's a simple structured database behind a front end that enforces a few rules: required fields, dropdowns instead of free text, timestamps you can't accidentally overwrite. Tools like Airtable, NocoDB, or a lightweight internal app built on something like Supabase can replace that spreadsheet in a week. The key shift is moving from 'anyone can change anything' to 'the system only accepts valid data.' That single constraint eliminates most of the Monday morning firefighting.

If you're not ready to replace the spreadsheet yet, at least do this: remove edit access from the emailed copies and point everyone at a single shared version with protected columns for formulas and totals. Add data validation on the ten columns that matter most. It's thirty minutes of work that prevents the kind of silent data loss that makes a R200k stock discrepancy look like it appeared from nowhere.

FROM IDEAS TO SOMETHING REAL

Your next chapter.
Let’s build it.

A website, an app, a better way to work. Tell us what you have in mind.

Start a conversation