UI Components Medium
Dish Nutrition Label
FDA-style nutrition facts panel for restaurant dishes: calories, macros bar chart, full nutrient table, allergen badges, and a serving-size adjuster that scales all values.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
/* โโ Design tokens โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
:root {
--cream: #FAF7F1;
--ink: #2C1A0E;
--forest: #345F40;
--forest-d: #213D29;
--terracotta: #C4622D;
--gold: #D4A853;
--warm-gray: #8A7D72;
--bone: #F0EBE0;
--white: #FFFFFF;
--radius-card: 12px;
--radius-pill: 999px;
--shadow-card: 0 4px 24px rgba(44, 26, 14, 0.10), 0 1px 4px rgba(44, 26, 14, 0.06);
--transition: 0.18s ease;
}
/* โโ Reset / base โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background: var(--cream);
color: var(--ink);
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 40px 16px;
}
/* โโ Page wrapper โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.page-wrapper {
width: 100%;
max-width: 420px;
}
/* โโ Card โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.card {
background: var(--white);
border-radius: var(--radius-card);
box-shadow: var(--shadow-card);
padding: 24px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* โโ Card header โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.card-header {
display: flex;
flex-direction: column;
gap: 2px;
}
.card-eyebrow {
font-family: 'Inter', sans-serif;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--warm-gray);
}
.card-title {
font-family: 'Playfair Display', serif;
font-size: 1.5rem;
font-weight: 800;
color: var(--ink);
line-height: 1.2;
}
/* โโ Field row (dish select) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.field-row {
display: flex;
flex-direction: column;
gap: 6px;
}
.field-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--warm-gray);
letter-spacing: 0.04em;
text-transform: uppercase;
}
.select-wrapper {
position: relative;
display: flex;
align-items: center;
}
.dish-select {
width: 100%;
appearance: none;
-webkit-appearance: none;
background: var(--bone);
border: 1.5px solid transparent;
border-radius: 8px;
padding: 10px 40px 10px 14px;
font-family: 'Inter', sans-serif;
font-size: 0.92rem;
font-weight: 600;
color: var(--ink);
cursor: pointer;
transition: border-color var(--transition), box-shadow var(--transition);
outline: none;
}
.dish-select:focus {
border-color: var(--forest);
box-shadow: 0 0 0 3px rgba(52, 95, 64, 0.12);
}
.select-arrow {
position: absolute;
right: 14px;
font-size: 1.1rem;
color: var(--warm-gray);
pointer-events: none;
line-height: 1;
}
/* โโ Serving stepper โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.stepper-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.stepper-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--warm-gray);
}
.stepper {
display: flex;
align-items: center;
background: var(--bone);
border-radius: var(--radius-pill);
padding: 3px;
gap: 0;
}
.stepper-btn {
width: 36px;
height: 36px;
border: none;
background: transparent;
border-radius: var(--radius-pill);
font-size: 1.2rem;
font-weight: 700;
color: var(--forest);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background var(--transition), color var(--transition);
line-height: 1;
user-select: none;
}
.stepper-btn:hover {
background: var(--forest);
color: var(--white);
}
.stepper-btn:active {
background: var(--forest-d);
color: var(--white);
}
.stepper-value {
min-width: 42px;
text-align: center;
font-size: 1rem;
font-weight: 700;
color: var(--ink);
}
/* โโ Divider โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.divider {
height: 1px;
background: var(--bone);
}
/* โโ Calories block โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.calories-block {
display: flex;
align-items: baseline;
gap: 8px;
flex-wrap: wrap;
}
.calories-number {
font-family: 'Playfair Display', serif;
font-size: 3rem;
font-weight: 800;
color: var(--ink);
line-height: 1;
transition: color var(--transition);
}
.calories-unit {
font-size: 1rem;
font-weight: 700;
color: var(--warm-gray);
align-self: flex-end;
padding-bottom: 4px;
}
.calories-sub {
font-size: 0.78rem;
color: var(--warm-gray);
align-self: flex-end;
padding-bottom: 6px;
width: 100%;
margin-top: -4px;
}
/* โโ Macros section โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.macros-section,
.nutrient-section,
.allergen-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.section-heading {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--warm-gray);
}
.macro-row {
display: flex;
align-items: center;
gap: 10px;
}
.macro-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
font-weight: 600;
color: var(--ink);
min-width: 62px;
}
.macro-dot {
width: 9px;
height: 9px;
border-radius: 50%;
flex-shrink: 0;
}
.dot-protein { background: var(--forest); }
.dot-carbs { background: var(--gold); }
.dot-fat { background: var(--terracotta); }
.macro-bar-track {
flex: 1;
height: 9px;
background: var(--bone);
border-radius: var(--radius-pill);
overflow: hidden;
}
.macro-bar {
height: 100%;
border-radius: var(--radius-pill);
transition: width 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
min-width: 4px;
}
.bar-protein { background: var(--forest); }
.bar-carbs { background: var(--gold); }
.bar-fat { background: var(--terracotta); }
.macro-grams {
font-size: 0.82rem;
font-weight: 700;
color: var(--ink);
min-width: 36px;
text-align: right;
}
/* โโ Nutrient table โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.nutrient-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.nutrient-table td {
padding: 7px 10px;
}
.nutrient-table .row-alt td {
background: var(--bone);
}
.nutrient-table tr:first-child td {
border-radius: 6px 6px 0 0;
}
.nutrient-table tr:last-child td {
border-radius: 0 0 6px 6px;
}
.nut-name {
font-weight: 500;
color: var(--ink);
}
.nut-indent {
padding-left: 22px !important;
color: var(--warm-gray);
font-style: italic;
}
.nut-value {
text-align: right;
font-weight: 700;
color: var(--ink);
white-space: nowrap;
}
/* โโ Allergen chips โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.allergen-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.allergen-chip {
display: inline-flex;
align-items: center;
gap: 5px;
background: rgba(196, 98, 45, 0.08);
color: var(--terracotta);
border: 1.5px solid rgba(196, 98, 45, 0.25);
border-radius: var(--radius-pill);
padding: 4px 12px;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.allergen-none {
font-size: 0.82rem;
font-weight: 500;
color: var(--warm-gray);
font-style: italic;
}/* โโ Dish data โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
const DISHES = [
{
name: 'Grilled Salmon Fillet',
cal: 420,
protein: 46,
carbs: 8,
fat: 22,
satfat: 5,
sodium: 390,
fiber: 1,
sugar: 2,
cholesterol: 115,
vitamins: { vitd: 16.4, calcium: 28, iron: 1.8 },
allergens: ['fish', 'soy', 'sesame']
},
{
name: 'Truffle Pasta Carbonara',
cal: 680,
protein: 24,
carbs: 78,
fat: 29,
satfat: 12,
sodium: 720,
fiber: 3,
sugar: 4,
cholesterol: 185,
vitamins: { vitd: 1.2, calcium: 142, iron: 2.9 },
allergens: ['gluten', 'eggs', 'dairy', 'sulphites']
},
{
name: 'Wagyu Beef Burger',
cal: 820,
protein: 52,
carbs: 48,
fat: 44,
satfat: 18,
sodium: 980,
fiber: 2,
sugar: 8,
cholesterol: 220,
vitamins: { vitd: 0.8, calcium: 210, iron: 5.4 },
allergens: ['gluten', 'dairy', 'mustard', 'eggs']
},
{
name: 'Garden Harvest Bowl',
cal: 340,
protein: 14,
carbs: 52,
fat: 10,
satfat: 1.5,
sodium: 310,
fiber: 9,
sugar: 11,
cholesterol: 0,
vitamins: { vitd: 0.4, calcium: 96, iron: 4.2 },
allergens: ['tree nuts', 'sesame']
}
];
/* โโ Allergen emoji map โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
const ALLERGEN_EMOJI = {
'gluten': '๐พ',
'dairy': '๐ง',
'eggs': '๐ฅ',
'fish': '๐',
'soy': '๐ซ',
'tree nuts': '๐ฐ',
'sesame': '๐ฟ',
'mustard': '๐ผ',
'sulphites': '๐ท'
};
/* โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
let currentDish = DISHES[0];
let servings = 1;
/* โโ DOM refs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
const dishSelect = document.getElementById('dish-select');
const btnMinus = document.getElementById('btn-minus');
const btnPlus = document.getElementById('btn-plus');
const servingDisplay = document.getElementById('serving-display');
const calDisplay = document.getElementById('cal-display');
const calSub = document.getElementById('cal-sub');
const barProtein = document.getElementById('bar-protein');
const barCarbs = document.getElementById('bar-carbs');
const barFat = document.getElementById('bar-fat');
const gramsProtein = document.getElementById('grams-protein');
const gramsCarbs = document.getElementById('grams-carbs');
const gramsFat = document.getElementById('grams-fat');
const allergenChips = document.getElementById('allergen-chips');
const ntFat = document.getElementById('nt-fat');
const ntSatFat = document.getElementById('nt-satfat');
const ntChol = document.getElementById('nt-chol');
const ntSodium = document.getElementById('nt-sodium');
const ntCarbs = document.getElementById('nt-carbs');
const ntFiber = document.getElementById('nt-fiber');
const ntSugar = document.getElementById('nt-sugar');
const ntProtein = document.getElementById('nt-protein');
const ntVitD = document.getElementById('nt-vitd');
const ntCalcium = document.getElementById('nt-calcium');
const ntIron = document.getElementById('nt-iron');
/* โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
function scale(val) {
return Math.round(val * servings * 10) / 10;
}
function scaleInt(val) {
return Math.round(val * servings);
}
/* โโ Render โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
function render() {
const d = currentDish;
/* Serving display */
servingDisplay.textContent = servings;
btnMinus.disabled = servings <= 1;
btnPlus.disabled = servings >= 4;
btnMinus.style.opacity = servings <= 1 ? '0.35' : '1';
btnPlus.style.opacity = servings >= 4 ? '0.35' : '1';
/* Calories */
calDisplay.textContent = scaleInt(d.cal);
calSub.textContent = servings === 1
? `per 1 serving (${d.name})`
: `for ${servings} servings (${d.name})`;
/* Macro bars โ percentages relative to total macro grams */
const totalMacroG = d.protein + d.carbs + d.fat;
const pctProtein = totalMacroG > 0 ? (d.protein / totalMacroG) * 100 : 0;
const pctCarbs = totalMacroG > 0 ? (d.carbs / totalMacroG) * 100 : 0;
const pctFat = totalMacroG > 0 ? (d.fat / totalMacroG) * 100 : 0;
barProtein.style.width = pctProtein.toFixed(1) + '%';
barCarbs.style.width = pctCarbs.toFixed(1) + '%';
barFat.style.width = pctFat.toFixed(1) + '%';
gramsProtein.textContent = scale(d.protein) + 'g';
gramsCarbs.textContent = scale(d.carbs) + 'g';
gramsFat.textContent = scale(d.fat) + 'g';
/* Nutrient table */
ntFat.textContent = scale(d.fat) + 'g';
ntSatFat.textContent = scale(d.satfat) + 'g';
ntChol.textContent = scaleInt(d.cholesterol) + 'mg';
ntSodium.textContent = scaleInt(d.sodium) + 'mg';
ntCarbs.textContent = scale(d.carbs) + 'g';
ntFiber.textContent = scale(d.fiber) + 'g';
ntSugar.textContent = scale(d.sugar) + 'g';
ntProtein.textContent = scale(d.protein) + 'g';
ntVitD.textContent = (Math.round(d.vitamins.vitd * servings * 10) / 10) + 'mcg';
ntCalcium.textContent = scaleInt(d.vitamins.calcium) + 'mg';
ntIron.textContent = (Math.round(d.vitamins.iron * servings * 10) / 10) + 'mg';
/* Allergen chips */
if (d.allergens.length === 0) {
allergenChips.innerHTML = '<span class="allergen-none">None declared</span>';
} else {
allergenChips.innerHTML = d.allergens.map(a => {
const emoji = ALLERGEN_EMOJI[a] || 'โ ๏ธ';
const label = a.charAt(0).toUpperCase() + a.slice(1);
return `<span class="allergen-chip">${emoji} ${label}</span>`;
}).join('');
}
}
/* โโ Event listeners โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
dishSelect.addEventListener('change', () => {
currentDish = DISHES[+dishSelect.value];
servings = 1;
render();
});
btnMinus.addEventListener('click', () => {
if (servings > 1) { servings--; render(); }
});
btnPlus.addEventListener('click', () => {
if (servings < 4) { servings++; render(); }
});
/* โโ Initial render โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
render();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dish Nutrition Label</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;800&family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page-wrapper">
<div class="card">
<!-- Header -->
<div class="card-header">
<span class="card-eyebrow">Nutrition Facts</span>
<h1 class="card-title">Dish Label</h1>
</div>
<!-- Dish selector -->
<div class="field-row">
<label class="field-label" for="dish-select">Select dish</label>
<div class="select-wrapper">
<select id="dish-select" class="dish-select">
<option value="0">Grilled Salmon Fillet</option>
<option value="1">Truffle Pasta Carbonara</option>
<option value="2">Wagyu Beef Burger</option>
<option value="3">Garden Harvest Bowl</option>
</select>
<span class="select-arrow">⌄</span>
</div>
</div>
<!-- Serving stepper -->
<div class="stepper-row">
<span class="stepper-label">Servings</span>
<div class="stepper">
<button class="stepper-btn" id="btn-minus" aria-label="Decrease servings">โ</button>
<span class="stepper-value" id="serving-display">1</span>
<button class="stepper-btn" id="btn-plus" aria-label="Increase servings">+</button>
</div>
</div>
<div class="divider"></div>
<!-- Calories -->
<div class="calories-block">
<div class="calories-number" id="cal-display">0</div>
<div class="calories-unit">kcal</div>
<div class="calories-sub" id="cal-sub">per serving</div>
</div>
<div class="divider"></div>
<!-- Macro bars -->
<div class="macros-section">
<div class="section-heading">Macronutrients</div>
<div class="macro-row">
<div class="macro-label">
<span class="macro-dot dot-protein"></span>
Protein
</div>
<div class="macro-bar-track">
<div class="macro-bar bar-protein" id="bar-protein"></div>
</div>
<div class="macro-grams" id="grams-protein">0g</div>
</div>
<div class="macro-row">
<div class="macro-label">
<span class="macro-dot dot-carbs"></span>
Carbs
</div>
<div class="macro-bar-track">
<div class="macro-bar bar-carbs" id="bar-carbs"></div>
</div>
<div class="macro-grams" id="grams-carbs">0g</div>
</div>
<div class="macro-row">
<div class="macro-label">
<span class="macro-dot dot-fat"></span>
Fat
</div>
<div class="macro-bar-track">
<div class="macro-bar bar-fat" id="bar-fat"></div>
</div>
<div class="macro-grams" id="grams-fat">0g</div>
</div>
</div>
<div class="divider"></div>
<!-- Nutrient table -->
<div class="nutrient-section">
<div class="section-heading">Full Nutritional Breakdown</div>
<table class="nutrient-table" id="nutrient-table">
<tbody>
<tr class="row-alt">
<td class="nut-name">Total Fat</td>
<td class="nut-value" id="nt-fat">0g</td>
</tr>
<tr>
<td class="nut-name nut-indent">Saturated Fat</td>
<td class="nut-value" id="nt-satfat">0g</td>
</tr>
<tr class="row-alt">
<td class="nut-name">Cholesterol</td>
<td class="nut-value" id="nt-chol">0mg</td>
</tr>
<tr>
<td class="nut-name">Sodium</td>
<td class="nut-value" id="nt-sodium">0mg</td>
</tr>
<tr class="row-alt">
<td class="nut-name">Total Carbohydrates</td>
<td class="nut-value" id="nt-carbs">0g</td>
</tr>
<tr>
<td class="nut-name nut-indent">Dietary Fiber</td>
<td class="nut-value" id="nt-fiber">0g</td>
</tr>
<tr class="row-alt">
<td class="nut-name nut-indent">Sugars</td>
<td class="nut-value" id="nt-sugar">0g</td>
</tr>
<tr>
<td class="nut-name">Protein</td>
<td class="nut-value" id="nt-protein">0g</td>
</tr>
<tr class="row-alt">
<td class="nut-name">Vitamin D</td>
<td class="nut-value" id="nt-vitd">0mcg</td>
</tr>
<tr>
<td class="nut-name">Calcium</td>
<td class="nut-value" id="nt-calcium">0mg</td>
</tr>
<tr class="row-alt">
<td class="nut-name">Iron</td>
<td class="nut-value" id="nt-iron">0mg</td>
</tr>
</tbody>
</table>
</div>
<div class="divider"></div>
<!-- Allergens -->
<div class="allergen-section">
<div class="section-heading">Contains Allergens</div>
<div class="allergen-chips" id="allergen-chips">
<!-- chips injected by JS -->
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Dish Nutrition Label
Nutrition panel in the restaurantโs warm aesthetic (not the clinical government style). Shows: calories large, macro bars (protein/carbs/fat as %-of-serving colored bar), full nutrient table (sodium, fiber, sugar, cholesterol, vitamins), allergen badge row, and a serving-size stepper (ร1 to ร4) that scales all displayed values proportionally. Dish selector dropdown at top switches between 4 dishes.