Skip to main content
This guide covers best practices for displaying SmartMenu search results, match badges, and nutrition information in your UI.

Match Groups

Search results are grouped into three categories based on how well dishes match the guest’s preferences.

Match Status Overview

StatusBadgeColorWhen to Show
MATCHMatchGreenAlways prominent
ALMOST_MATCHAlmostYellow/OrangeWith warnings
NOT_MATCHNot a MatchGreyOptional, dimmed

Visual Hierarchy

Match Badges

Badge Components

function MatchBadge({ status }) {
  const config = {
    MATCH: { label: 'Match', color: 'green', icon: '✓' },
    ALMOST_MATCH: { label: 'Almost', color: 'yellow', icon: '⚠' },
    NOT_MATCH: { label: 'Not a Match', color: 'grey', icon: '✗' }
  };

  const { label, color, icon } = config[status];

  return (
    <span className={`badge badge-${color}`}>
      {icon} {label}
    </span>
  );
}

CSS Styling

.badge {
  display: inline-flex;
  align-items: center;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 600;
}

.badge-green {
  background: #d1fae5;
  color: #065f46;
}

.badge-yellow {
  background: #fef3c7;
  color: #92400e;
}

.badge-grey {
  background: #f3f4f6;
  color: #6b7280;
}

Match Reasons

For ALMOST_MATCH and NOT_MATCH dishes, always display the reasons:
function DishCard({ dish, matchStatus, matchReasons }) {
  return (
    <div className={`dish-card ${matchStatus.toLowerCase()}`}>
      <img src={dish.imageUrl} alt={dish.name} />
      <h3>{dish.name}</h3>
      <MatchBadge status={matchStatus} />

      {matchReasons && matchReasons.length > 0 && (
        <div className="match-reasons">
          {matchReasons.map((reason, i) => (
            <span key={i} className="reason">{reason}</span>
          ))}
        </div>
      )}

      <div className="nutrition-summary">
        {dish.nutrition.calories} cal
      </div>
    </div>
  );
}

Compact Dish View

For list layouts, show key information inline:
function DishListItem({ dish, matchStatus }) {
  return (
    <div className="dish-list-item">
      <img src={dish.imageUrl} alt={dish.name} />
      <div className="dish-info">
        <div className="dish-header">
          <h3>{dish.name}</h3>
          <MatchBadge status={matchStatus} />
        </div>
        <div className="dish-meta">
          {dish.nutrition.calories} cal • {dish.nutrition.protein}g protein
          {dish.diets.map(d => <span key={d.type}>{d.displayName}</span>)}
        </div>
        {dish.allergens.length > 0 && (
          <div className="allergens">
            Contains: {dish.allergens.map(a => a.displayName).join(', ')}
          </div>
        )}
      </div>
      <button className="order-btn">Order</button>
    </div>
  );
}

Nutrition Panel

Full Nutrition Facts

Display the complete nutrition panel in dish detail views:
function NutritionPanel({ nutrition }) {
  return (
    <div className="nutrition-panel">
      <h4>Nutrition Facts</h4>

      <NutritionRow label="Calories" value={nutrition.calories} bold />

      <div className="divider" />

      <NutritionRow label="Total Fat" value={`${nutrition.fatTotal}g`} bold />
      <NutritionRow label="Saturated Fat" value={`${nutrition.fatSaturated}g`} indent />
      <NutritionRow label="Trans Fat" value={`${nutrition.fatTrans}g`} indent />
      <NutritionRow label="Cholesterol" value={`${nutrition.cholesterol}mg`} bold />
      <NutritionRow label="Sodium" value={`${nutrition.sodium}mg`} bold />
      <NutritionRow label="Total Carbohydrates" value={`${nutrition.carbohydrates}g`} bold />
      <NutritionRow label="Dietary Fiber" value={`${nutrition.dietaryFiber}g`} indent />
      <NutritionRow label="Sugars" value={`${nutrition.sugar}g`} indent />
      <NutritionRow label="Protein" value={`${nutrition.protein}g`} bold />
    </div>
  );
}

Allergen Badges

Allergen information is critical for guest safety. Make allergen badges highly visible and never hide them.
function AllergenBadges({ allergens }) {
  const icons = {
    DAIRY: '🥛',
    EGG: '🥚',
    FISH: '🐟',
    SHELLFISH: '🦐',
    TREE_NUT: '🌰',
    PEANUT: '🥜',
    WHEAT: '🌾',
    SOY: '🫛',
    SESAME: '⚪'
  };

  return (
    <div className="allergen-badges" role="alert">
      <span className="allergen-label">Contains:</span>
      {allergens.map(allergen => (
        <span key={allergen.type} className="allergen-badge" title={allergen.source}>
          {icons[allergen.type]} {allergen.displayName}
        </span>
      ))}
    </div>
  );
}

Diet Tags

function DietTags({ diets }) {
  return (
    <div className="diet-tags">
      {diets.map(diet => (
        <span key={diet.type} className="diet-tag">
          {diet.displayName}
        </span>
      ))}
    </div>
  );
}

Empty States

Handle cases where no dishes match:
function EmptyMatchesState({ preferences }) {
  return (
    <div className="empty-state">
      <h3>No exact matches found</h3>
      <p>
        We couldn't find dishes that match all your preferences.
        Try adjusting your filters or check out the "Almost Matches" below.
      </p>
      <button onClick={() => clearFilters()}>
        Clear Filters
      </button>
    </div>
  );
}

Accessibility

Screen Readers

Use aria-label on badges: “Match status: Almost match, contains dairy”

Color Contrast

Ensure badge colors meet WCAG AA contrast ratios

Keyboard Navigation

All interactive elements should be keyboard accessible

Focus Indicators

Visible focus states on dish cards and buttons
// Accessible badge example
<span
  className="badge badge-yellow"
  role="status"
  aria-label={`Match status: Almost match. ${matchReasons.join('. ')}`}
>
  ⚠ Almost
</span>