
How we connect design and code through rigorous engineering patterns to scale interfaces at iFood
At iFood, we faced a common problem in large-scale technology organizations: a growing divergence between design and code. What started as small visual inconsistencies evolved into a significant gap between design intent and final implementation.
It became clear that we didn’t just need a package update – we needed a structural change in how we think about UI in engineering. The answer to this challenge? iFood Design System (IFDS).
IFDS isn’t just a pretty UI Kit in Figma or a loose folder of components. It’s a comprehensive engineering ecosystem built to scale our design language across all of iFood with technical rigor.
In this post, we’re going behind the scenes and detailing our monorepo architecture, strategic package organization, and the rigorous componentization patterns we’ve adopted.
To serve multiple platforms (Web, Android, iOS, Flutter) without duplicating design logic, we divided our architecture into two conceptual parts:
This separation ensures that when the brand’s primary color changes in iFDL, the change propagates via CI/CD (Continuous Integration/Continuous Delivery – a process that automates software integration and distribution) to all consumer platforms automatically.
Design Tokens aren’t just static values in a JSON file – they need to be transformed into specific formats for each consumer platform. In IFDS, this transformation is automated through Style-Dictionary, an Amazon tool that generates themes for multiple platforms from a single source of truth.
Here’s how it works:
--ifdl-color-brand-primary-default);IFDL.color.brand.primary.default);This means that when a designer changes the brand’s primary color in Figma, that change automatically propagates to all platforms without a single engineer touching the code.
The style-dictionary-builder is configured to generate themes for multiple organizations (iFood, Pago, for example), multiple themes (default, dark), and multiple modes (light, dark), all from the same token source. Generated files are read-only and should never be manually edited. They are regenerated with each token change.
Managing a system of this magnitude requires robust tools. Choosing a modern monorepo (using Nx, which is a monorepo build system – a monorepo orchestration tool focused on performance and scalability of large software projects) wasn’t just about putting code in a single repo, it was about gaining operational efficiency at scale.
With Nx, we gained superpowers in our CI/CD pipeline:
How do we physically organize hundreds of components? We defined a clear three-level hierarchy that separates fundamentals, global patterns, and product-specific implementations:
ifds/components/
├── shared/ # Reusable fundamentals (Atoms/Molecules)
│ └── web/
│ └── src/
│ ├── Buttons/
│ ├── Form/
│ ├── Layout/
│ ├── Typography/
│ ├── hooks/ # Shared utility hooks
│ └── utils/ # Shared helper functions
│
├── global/ # Corporate patterns (Organisms)
│ └── web/
│ └── src/
│ ├── Communication/ # e.g. Toasts, Banners
│ ├── Navigation/ # e.g. Sidebars, Headers
│ ├── Rating/ # e.g. Rating stars
│
└── product/ # Business domain-specific
└── driver-app/ # e.g. Components that only exist in Driver App
└── web/
└── src/
The Three-Level Strategy
This structure reflects a strategic decision about the scope and granularity of reuse:
This division prevents fundamental components from being “polluted” with business rules and ensures that complex patterns remain consistent across products.
Moving from monorepo level to file level, standardization continues. In IFDS, creating a component isn’t just about creating a .tsx file.
We defined a rigorous pattern for what constitutes a “Standard Component”. Each component requires a complete structure to ensure that implementation, tests, documentation, styles, and types are all first-class citizens.
Here’s the actual structure of a simple component from the shared layer, like a Button:
Button/
├── Button.tsx # Main implementation [required]
├── Button.test.tsx # Unit/a11y tests (Vitest/RTL) [required]
├── Button.stories.tsx # Storybook stories [required]
├── Button.module.css # Isolated styles (CSS Modules) [required]
├── types.ts # TypeScript definitions and public interfaces [required]
├── index.ts # Public API of the package
This standardization eliminates assumptions. Any engineer joining the project knows exactly where to find logic, styles, or tests.
Having an organized folder structure isn’t enough. What really ensures quality and maintenance are the code patterns we apply within these files.
1. Rigorous and Semantic TypeScript
TypeScript is not optional. In IFDS, we take typing seriously to ensure excellent Developer Experience (DX) and prevent bugs.
Our properties are semantic: they describe behavior or intent (variant="primary", isLoading…), never direct style values (color="red", fontSize="14px"...). This gives us flexibility to change the design without breaking consumer applications.
We use type composition in our types.ts file, separating our custom properties from native HTML attributes. Additionally, we wrap all properties with the Prettify utility type to improve the TypeScript autocomplete experience:
// types.ts (Real IFDS example)
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import { Prettify } from '../../utils';
// Semantic props from our Design System
export type CustomButtonProps = {
children?: ReactNode;
variant?: 'primary' | 'secondary' | 'text' | 'link';
isLoading?: boolean;
};
// Final composition with native HTML button props, excluding what we override
// Prettify improves autocomplete experience in TypeScript
export type ButtonProps = Prettify<
CustomButtonProps & Omit<ComponentPropsWithoutRef<'button'>, keyof CustomButtonProps>
>;
2. Styling: CSS Modules + Mandatory Tokens
The biggest enemy of consistency in a Design System? “Magic numbers” in CSS. In IFDS, we use CSS Modules to ensure isolated scope and avoid class name conflicts in micro-frontends. But the golden rule is: always use design tokens, never hardcoded values.
These tokens are generated from Figma by iFDL via style-dictionary. Engineers don’t “create” colors, we consume them.
Wrong (Hardcoded):
.button {
padding: 16px;
background-color: #ea1d2c; /* iFood's red today, maybe not tomorrow */
border-radius: 8px;
}
Right (Tokenized):
/* Button.module.css */
.button {
border-radius: var(--ifdl-border-radius-re);
padding: var(--ifdl-spacing-scale-12);
font-family: var(--ifdl-font-family-ifood-body);
&.variant-primary {
&.state-disabled {
color: var(--ifdl-background-color-on-disabled);
background-color: var(--ifdl-background-color-disabled);
}
&:not(.state-disabled) {
color: var(--ifdl-background-color-on-brand-primary);
--background-color: var(--ifdl-background-color-brand-primary);
}
}
}
3. Architecture Pattern: Inversion of Control (IoC) and Composition
To create truly reusable components at scale, we need to avoid “React Prop Drilling Hell” – where a parent component receives dozens of props just to pass them to internal children. We adopted Inversion of Control (IoC) through the Composition pattern. Instead of a monolithic component that tries to do everything via configuration props, we offer sub-components that developers assemble.
This allows flexibility while maintaining visual consistency. A classic example is our Dialog component:
// Composition example (IoC)
// The parent Dialog component doesn't need to know what's inside Footer.
<Dialog status="open" onClose={close}>
<Dialog.Header title="Confirmation" />
<Dialog.Body>
Do you really want to delete this item?
</Dialog.Body>
<Dialog.Footer>
<Button variant="tertiary" onClick={close}>Cancel</Button>
<Button variant="primary" onClick={confirm}>Confirm</Button>
</Dialog.Footer>
</Dialog>
Finally, standardization extends to quality assurance in the pipeline. A component in IFDS is only considered “ready” and merged if it includes:
Migrating from a legacy library to IFDS required a serious commitment to software engineering. This organization, from the macro structure of the monorepo with Nx, through the strategic division between shared, global, and product, to the micro structure of files and TypeScript/CSS patterns, isn’t bureaucracy. It’s what ensures the consistency, maintainability, and scalability we need for dozens of product teams at iFood to build interfaces quickly without reinventing the wheel.
The automated token pipeline, from Figma to code, through style-dictionary, ensures that design changes automatically propagate to all platforms, eliminating drift between design and implementation.
Now that we’ve established the technical foundation, our focus turns to the adoption and continuous evolution of the system. How do you organize the distinction between generic and product-specific components at scale? Share in the comments!


Software Engineering
Augusto Sandim works as a Software Engineer at iFood. He holds a degree in Computer Science, is from Mato Grosso do Sul, and loves riding BMX.
We are always looking for passionate developers, designers and data scientists to help us revolutionize the food delivery experience. Join iFood Tech and be part of building the future of food technology.
Discover our CareersEach article is the result of the vision and expertise of our authors. See who contributes to our blog: