Building Fast Web Apps for Low-Bandwidth Markets
Performance optimisation isn't a nice-to-have when your users are on 3G in Kampala or Nairobi. Here's the exact stack and techniques we use to build sub-2s load times across East Africa.
Most design systems start as a solution and end up as a problem. Here's how we build component libraries that teams actually want to use — and keep using.
Aisha Nakato
Creative Director

The graveyard of enterprise software is full of design systems that were built with good intentions and abandoned within 18 months. The teams that built them are still proud of the Storybook. The teams that were supposed to use them went back to building their own buttons.
We've designed component libraries for 8 clients over the past 3 years. Here's what separates the ones that get adopted from the ones that get abandoned.
Design systems fail for one of three reasons: they're built by designers who don't consider developer experience, they're built by engineers who don't consider design intent, or they're built for the system rather than for the product. The common thread is that they optimise for the wrong audience.
The adoption test: A component should be faster to use from the system than to build from scratch. If it isn't — if the API is confusing, the documentation is missing, or the customisation model is inflexible — developers will build their own. Every time.
The biggest mistake teams make is jumping straight to building buttons and cards. Tokens — colour, spacing, typography, shadow — are the foundation everything else inherits from. If you get tokens wrong, every component built on top of them carries the mistake.
class=class="text-svc-data">"text-ink-faint italic">/* Good token architecture — semantic, not literal */
@theme {
class=class="text-svc-data">"text-ink-faint italic">/* Semantic colour tokens */
--color-base: #080A0F; class=class="text-svc-data">"text-ink-faint italic">/* page background */
--color-surface: #0D1117; class=class="text-svc-data">"text-ink-faint italic">/* card background */
--color-ink: #F0F4F8; class=class="text-svc-data">"text-ink-faint italic">/* primary text */
--color-ink-muted: #8A9BB0; class=class="text-svc-data">"text-ink-faint italic">/* secondary text */
--color-brand: #00E5FF; class=class="text-svc-data">"text-ink-faint italic">/* primary accent */
class=class="text-svc-data">"text-ink-faint italic">/* Spacing scale — consistent rhythm */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 40px;
}The API of a component is a contract. Once teams are using it in production, changing it is expensive. Spend time on API design before writing implementation. The questions to answer are: What are the required props? What are the optional ones? What are the escape hatches?
<Button> with <ButtonIcon> beats a single component with 12 boolean propsclassName override — never trap consumers inside your stylesclass=class="text-svc-data">"text-ink-faint italic">// ❌ Too many boolean props — combinatorial explosion
<Button primary disabled large iconLeft=class="text-svc-data">"arrow" iconRight=class="text-svc-data">"chevron" />
class=class="text-svc-data">"text-ink-faint italic">// ✅ Variant-based with composition for icons
<Button variant=class="text-svc-data">"primary" size=class="text-svc-data">"lg">
<ButtonIcon icon={ArrowIcon} placement=class="text-svc-data">"left" />
Continue
</Button>Documentation that isn't read is worse than no documentation — it creates a false sense that the system is self-explanatory. The only documentation that gets read is documentation that's in context: in the IDE via TypeScript types, in the component file via JSDoc, and in examples that show real usage patterns rather than synthetic demos.
Every design system needs a process for proposing new components and changing existing ones. But most governance processes are too heavy — they require RFCs, design reviews, and approval chains that take weeks. The result is that teams bypass the process and build locally.
Lightweight governance: A shared Slack channel, a 48-hour RFC period on a PR, and a named component owner who reviews within one business day. That's all the process you need for teams under 20 engineers.
We use Tailwind v4 with a custom theme layer for all our client component libraries. The key insight is that Tailwind's utility classes are the implementation detail, not the API. The API is your component props. Consumers shouldn't need to know what Tailwind classes your Button uses internally.
class=class="text-svc-data">"text-ink-faint italic">// The component consumer sees this
<PrimaryButton onClick={handleSubmit}>
Save Changes
</PrimaryButton>
class=class="text-svc-data">"text-ink-faint italic">// The implementation detail (invisible to consumers)
function PrimaryButton({ children, className = class="text-svc-data">"", ...props }) {
return (
<button
className={class="text-svc-data">`px-class="text-svc-social">7 py-class="text-svc-social">3.5 rounded-md border border-brand text-brand
hover:bg-brand hover:text-base transition-all duration-class="text-svc-social">200
focus-visible:ring-class="text-svc-social">2 focus-visible:ring-brand ${className}`}
{...props}
>
{children}
</button>
);
}A design system that isn't used is a failure, regardless of how well it's designed. Track adoption by running a codebase grep for your component imports monthly. The ratio of system components to locally-created components tells you whether teams trust the system. If that ratio is declining, investigate why before adding new components.
Engineering deep-dives, design thinking, and practical AI — written for builders who care about craft. No fluff. No spray.
No spam. Unsubscribe any time.
Continue Reading
Performance optimisation isn't a nice-to-have when your users are on 3G in Kampala or Nairobi. Here's the exact stack and techniques we use to build sub-2s load times across East Africa.
Building an LLM prototype takes an afternoon. Getting it to production takes months. After shipping 6 agent systems this year, here's the gap between the demo and reality.