Body Mass Index Calculator Website
March 11, 2024
Project walk-through / case-study - Frontend Mentor BMI Calculator challenge
Frontend Mentor - Body Mass Index Calculator Challenge
In this post, I go through my implementation of the Body Mass Index Calculator challenge on Frontend Mentor. I go over the challenge, the process, the stack choices and findings, sharing some insights and techniques which the reader will hopefully find valuable.
Table of contents
Overview
The challenge
Users should be able to:
- Select whether they want to use metric or imperial units
- Enter their height and weight
- See their BMI result, with their weight classification and healthy weight range
- View the optimal layout for the interface depending on their device’s screen size
- See hover and focus states for all interactive elements on the page
Here are the links to the live website and the github repo.
Links
Built with
- Sass
- CSS Grid
- Web Components
- Alpine.js - JS framework
The process and the stack
Alpine.js with Web Components
I found this challenge to be a perfect use case for Alpine.js, which is a super lightweight Javascript framework designed to compose behavior directly in the markup. Basically, it’s a small set of attributes, properties and methods which allow you to very selectively add the interactivity your project needs with no unnecessary overhead or complexity.
In the case of this project, most of it is static markup and styling; the BMI calculator is the only interactive element, and to be honest, I thought it would be overkill to use React just for that. Also, using React would mean making this website client-rendered; this would hinder SEO, which for this type of website is important. Granted, Next.js could solve this, but again, too much for this small website.
The only thing I don’t like about Alpine is that it doesn’t provide a mechanism to create separate components, as most other frontend frameworks do. This is not a deal breaker for such a small website, but at the very least, I wanted the BMI calculator to be its own thing.
Luckily, I found the Alpine Web Components package, which in the words of its developer:
“is a tiny script which loads the content of a regular HTML file, converts it to a Web component
and makes it usable anywhere in your pages with reactivity and logic powered by Alpine.js
.”
This was exactly what I needed to abstract the BMI calculator to a separate web component, but have all the logic and state being managed by Alpine.js, which features a super intuitive and declarative API.
For example, we can render text conditionally based on state:
<span x-text="unit === 'metric' ? 'cm' : 'inches' "></span>
Or bind inputs to state values:
<input type="radio" id="metric" value="metric" x-model="unit" />
To manage state and unit conversion in Alpine.js, I did have to set several things up within Alpine.data(). This method initializes state variables and defines getter functions. Granted, this is where complexity may start to creep in, but for small projects, it shouldn’t get so bad, provided we keep our methods lean, and abstract all the logic we can to helper methods, as I did inthis case:
window.Alpine.data("bmiCalculator", () => ({
unit: "metric",
prevUnit: "metric",
height: "",
weight: "",
bmi: 0,
converting: false, // Flag to prevent double conversion
get bmiMessage() {
return bmiMessage(this.bmi, this.unit, this.height);
},
convertUnits() {
if (!this.height || !this.weight || this.converting) return;
this.converting = true;
const converted = convertUnits(
this.unit,
this.prevUnit,
this.height,
this.weight
);
this.height = converted.height;
this.weight = converted.weight;
this.prevUnit = this.unit;
setTimeout(() => {
this.converting = false;
this.calculateBMI();
}, 50);
},
idealWeightRange() {
const height = parseFloat(this.height);
if (isNaN(height) || height <= 0) return { min: "--", max: "--" };
if (this.unit === "metric") {
const heightInMeters = height / 100;
const min = (18.5 * heightInMeters ** 2).toFixed(1);
const max = (24.9 * heightInMeters ** 2).toFixed(1);
return { min, max };
} else {
const min = ((18.5 * height ** 2) / 703).toFixed(1);
const max = ((24.9 * height ** 2) / 703).toFixed(1);
return { min, max };
}
},
calculateBMI() {
if (this.converting) return;
this.bmi = calculateBMI(this.unit, this.height, this.weight);
},
init() {
this.$watch("height", () => {
if (!this.converting) this.calculateBMI();
});
this.$watch("weight", () => {
if (!this.converting) this.calculateBMI();
});
this.$watch("unit", (newUnit, oldUnit) => {
this.prevUnit = oldUnit;
this.convertUnits();
});
},
}));
calculateBMI, convertUnits and bmiMessage are all helper methods imported to the script.
With the interactive logic in place, I then focused on structuring and styling the UI efficiently using Sass.
Sass
In regards to styling, I went all in with Sass, using a modified version of the 7-1 pattern, with the main difference being that I have an individuals directory where I have the sass for very specific single entities; I like differentiating reusable components from very specific one-off entities.
Aside from that, there were several cool techniques I was able to apply.
Maps and functions
For example, having values for colors, sizes, etc., in maps and creating functions that leverage those maps:
$colors: (
neutral: (
900: #000000,
100: #ffffff,
),
primary: (
100: #e1e7fe,
300: #b3d3f1,
500: #345ff6,
900: #253347,
),
// ...
@function clr($color, $shade) {
@if map.has-key($colors, $color, $shade) {
@return map.get($colors, $color, $shade);
} @else {
@error '$colors does not have that color!';
}
}
Then, using these functions is a piece of cake and prevents all color related inconsistencies down the line:
body {
font-family: $ff-base;
font-size: fs(400);
font-weight: $fw-400;
color: clr(neutral, 900);
line-height: $line-height-regular;
}
Creating utility classes programmatically
Using Sass features like @each
, I was able to setup the generation of utility classes programmatically. I find this technique super-powerful, as the algorithm is the only thing you have to tweak and maintain, and all the bunch of necessary utility classes will be at your disposal when needed.
For example, this setup:
@each $size-name, $size-value in $font-sizes {
.fs-#{$size-name} {
font-size: $size-value;
@if $size-name > 600 {
line-height: 1.1;
}
}
}
generates classes like fs-400
, fs-600
, etc.
Other Sass features I used were mixins for media queries, nesting, interpolation, etc.
CSS Grid
The design for this project asks for a very peculiar layout grid which, at large viewports, features an irregular and skewed arrangement, and becomes regular and symmetrical at medium sizes and below.
This seemed like a great opportunity to leverage the CSS Grid. Using grid template areas, I was able to accomplish the layout shifts painlessly, going from something like this in large viewports
grid-template-columns: repeat(10, 1fr);
grid-template-areas:
". . . . . gen gen gen gen ."
". . age age age age mus mus mus mus"
"preg preg preg preg race race race race . .";
…to something like this at medium
grid-template-columns: repeat(8, 1fr);
grid-template-areas:
"gen gen gen gen age age age age"
"mus mus mus mus preg preg preg preg"
". . race race race race . .";
… and finally at small viewports
grid-template-columns: repeat(4, 1fr);
grid-template-areas:
"gen gen gen gen"
"age age age age"
"mus mus mus mus"
"preg preg preg preg"
"race race race race";
Every Layout Techniques
Finally, as I have been doing in all my projects, I applied several of the techniques proposed in the Every Layout book. The one I keep using the most is the stack, which allows us to easily add vertical separation to children elements of a parent. I ended up implementing this way for this project:
.stack {
--stack-space: #{size(32)};
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.stack > * {
margin-block: 0;
}
.stack > * + * {
margin-block-start: var(--stack-space);
}
Reflections and continued development
This project allowed me to see that there are still a lot of Sass features and patterns I have to explore. For example, I don’t think I leveraged Sass functions as much as I could have, which would have simplified some processes even more.
On the other hand, although Alpine.js is an awesome minimalistic framework to add interactivity very selectively, I doubt I would use it for any frontend project more complex than this one. I’ve been learning backend development with Adonis.js, and Alpine.js truly shines when used in that context. However, for frontend apps, I’ll be sticking to React and Svelte.
While I’m happy with the results, if I were to redo this challenge, I might experiment with a hybrid approach—perhaps using Web Components for encapsulation but managing state with a more scalable pattern.
Conclusion
The key learning I take away from this project is that there may be scenarios in which a minimalistic and lightweight JavaScript framework such as Alpine.js makes a lot more sense than React, Angular, Vue, etc. However, before dismissing those larger frameworks, one should carefully consider all the conveniences they provide—state management, component architecture, routing, and scalability. While a lightweight alternative may seem appealing for smaller projects, it’s crucial to evaluate whether it will fully meet the project’s needs in the long run. Otherwise, one might end up reinventing the wheel by patching together custom solutions for problems that established frameworks already solve efficiently.
That said, this challenge has reinforced my belief that understanding the trade-offs between different tools is key to making informed decisions as a developer. Each project comes with its own unique requirements, and the best technology choice is the one that balances simplicity, maintainability, and future scalability.
Useful resources
- AlpineJS
- AlpneJS Web Components
- Web Components Book - One valuable resource I’m using to learn about web components.
- Every Layout - This is, by far, one of the most valuable resources on CSS and web layout I’ve found. Highly recomended.
Author
- Website - Cesar Poumian
- Frontend Mentor