This document outlines the UI/UX patterns, design system conventions, and best practices used in this project. Following these guidelines helps keep our interface consistent, accessible, and easy to maintain.
Disclaimer: These are guidelines, not hard rules. Maintainers may request additional design changes or tweaks on a case-by-case basis during PR review.
- Design System Overview
- Component Patterns
- Internationalization (i18n)
- Accessibility (a11y)
- Responsive Design
- Design Resources
Our UI is built on a modern stack:
- Tailwind CSS v4 — Utility-first CSS framework
- DaisyUI v5 — Semantic component classes (e.g.,
btn,card,alert) - SvelteKit — Full-stack Svelte framework
- Iconify (@iconify-svelte) — Icon library (primarily Material Design Icons)
We define custom Light and Dark themes in src/app.css. The palette uses warm, natural tones to reflect our food-focused mission.
| Token | Semantic Role | Light Mode | Dark Mode |
|---|---|---|---|
primary |
Main actions, brand identity | #201a17 (Dark Brown) |
#faf7f5 (Off-White) |
primary-content |
Text on primary background | #faf7f5 |
#201a17 |
secondary |
Supporting actions, navigation | #52443d (Medium Brown) |
#ebc3a8 (Warm Beige) |
secondary-content |
Text on secondary background | #faf7f5 |
#201a17 |
base-100 |
Page / card backgrounds | #faf7f5 |
#201a17 |
base-200 |
Secondary backgrounds | #f3f0ee |
#2d2724 |
base-300 |
Borders, dividers, highlights | #ebe8e6 |
#3a332f |
Note: Always use DaisyUI semantic tokens (
bg-primary,text-base-content) instead of hard-coded hex values. This ensures automatic dark mode support.
The project uses generous rounding for a soft, friendly feel:
- Selectors & Fields:
1rem(set via--radius-selectorand--radius-field) - Inner Boxes:
0.5rem(set via--radius-box)
We follow a strict hierarchy to guide user attention. Always use the lowest-emphasis button that fits the context.
| Priority | DaisyUI Class | When to Use | Real Example |
|---|---|---|---|
| Highest | btn btn-primary |
Main call-to-action ("Add Product", "Save") | Homepage hero, Add Product CTA |
| High | btn btn-secondary |
Supporting action ("Search", "Next Step") | SearchBar "Go" button, Edit form nav |
| Medium | btn btn-outline |
Tertiary action ("Cancel", "Back", "Log out") | Login/Logout, Return to Home |
| Low | btn btn-soft |
Gentle emphasis without strong borders | Search page sort/filter toggles |
| Lowest | btn btn-ghost |
Minimal emphasis ("Close", "Dismiss") | Toast close, Settings link |
Use sizing classes to match the context:
| Size | Class | Usage |
|---|---|---|
| Small | btn-sm |
Inline actions, tags, compact UIs |
| Default | btn |
Standard buttons |
| Large | btn-lg |
Hero sections, prominent CTAs |
For buttons with both an icon and text, use gap-2 to maintain consistent spacing:
<button class="btn btn-primary gap-2">
<IconMdiPencil class="h-5 w-5" />
<span>{$_('action.edit', { default: 'Edit' })}</span>
</button>Use btn-circle or btn-square. Always add aria-label for accessibility:
<button class="btn btn-circle btn-ghost" aria-label={$_('action.close', { default: 'Close' })}>
<IconMdiClose class="h-6 w-6" aria-hidden="true" />
</button>Use the join pattern when buttons are part of a connected group (e.g., a search bar, pagination):
<div class="join">
<input class="input join-item input-bordered" placeholder="Search..." />
<button class="btn btn-secondary join-item">Go</button>
</div>Use card with bg-base-100 and shadow-xl for content containers. Use card-body for padded inner content:
<div class="card bg-base-100 w-full shadow-xl">
<div class="card-body">
<h2 class="card-title">Card Title</h2>
<p>Card content goes here.</p>
<div class="card-actions justify-end">
<button class="btn btn-primary">{$_('action.save', { default: 'Save' })}</button>
</div>
</div>
</div>Use DaisyUI's alert component for notifications. The project uses Svelte's fly transition for smooth entrance/exit:
<!-- Alert types: alert-success, alert-error, alert-warning, alert-info -->
<div class="alert alert-success shadow-lg" role="alert" aria-live="polite">
<IconMdiCheckCircle class="h-5 w-5" aria-hidden="true" />
<span>Operation completed successfully!</span>
</div>For tag-like links (e.g., in the Footer's "Discover our Project" section), use rounded-full:
<a
href="/about"
class="bg-secondary-content text-primary rounded-full px-4 py-2 transition-opacity hover:opacity-80"
>
{$_('footer.discover.about', { default: 'About Us' })}
</a>We use svelte-i18n to support multiple languages.
<script>
import { _ } from '$lib/i18n';
</script>Translation files are located in src/lib/i18n/messages/. The project aims to cover all languages available across the Open Food Facts ecosystem — if you'd like to contribute a translation, add a new locale file there.
-
Always provide a default value. This ensures the UI remains functional even when a translation key is missing or the locale bundle hasn't loaded yet.
<!-- Recommended: always provide a default --> <span>{$_('product.buttons.edit', { default: 'Edit' })}</span> <!-- Avoid: no fallback, may render the raw key --> <span>{$_('product.buttons.edit')}</span>
-
Use dot-separated, nested keys for logical grouping.
# Recommended product.buttons.edit search.placeholder # Avoid edit_button searchPlaceholder -
Adding a new translation key:
- Add the key and English text to
src/lib/i18n/messages/en-US.json. - (Optional) Add translations to other locale files (e.g.,
it-IT.json). - Use the key in your component with a
{ default: '...' }fallback.
- Add the key and English text to
We aim to make Open Food Facts accessible to everyone. Before submitting a PR, review the checklist below.
- Icon-only buttons have an
aria-labeldescribing the action. - Decorative icons use
aria-hidden="true"so screen readers skip them. - Images have a descriptive
altattribute (oralt=""if purely decorative). - Semantic HTML is used correctly:
<button>for actions (submitting forms, toggling state, opening modals).<a>for navigation (changing the URL, linking to external pages).
- Dynamic notifications (Toasts) use
role="alert"andaria-live="polite". - Interactive elements are keyboard-accessible (focusable and operable via
Enter/Space). - External links use
target="_blank"withrel="noopener"for security.
<!-- Footer: social link with aria-label -->
<a href="https://github.com/openfoodfacts" target="_blank" aria-label="GitHub">
<IconMdiGithub class="h-6 w-6" />
</a>
<!-- Toast: role and aria-live for screen reader announcements -->
<div class="alert alert-success" role="alert" aria-live="polite">
<IconMdiCheckCircle class="h-5 w-5" aria-hidden="true" />
<span>Changes saved!</span>
</div>
<!-- SearchBar: accessible input -->
<input
type="text"
class="input join-item input-bordered w-full"
placeholder={$_('search.placeholder')}
aria-label={$_('search.placeholder')}
/>We use a Mobile-First approach. Design for small screens first, then progressively enhance for larger viewports.
DaisyUI / Tailwind CSS breakpoints used in this project:
| Prefix | Min Width | Typical Use |
|---|---|---|
| (default) | 0px | Mobile styles |
sm: |
640px | Small tablets |
md: |
768px | Tablets / small desktops |
lg: |
1024px | Desktops |
2xl: |
1536px | Wide screens |
| Pattern | Classes | What It Does |
|---|---|---|
| Hide on mobile | hidden md:block |
Hidden by default, visible on tablets and up |
| Full width on mobile | w-full sm:w-auto |
Takes full width on small screens, auto on wider |
| Stack → Row | flex-col md:flex-row |
Vertical stack on mobile, horizontal on desktop |
| Responsive padding | px-4 md:px-10 lg:px-36 |
Padding increases with screen size (see Footer) |
| Mobile-only full | max-sm:w-full |
Full width only on very small screens |
- Contributing Guide — Code contribution workflow, branching, and PR guidelines.
- Open Food Facts Design Repo — Brand guidelines, logos, illustrations, and high-level design plan.
- Design Documentation — In-depth design principles and guidelines.
- Figma:
- Explorer Mockups (WIP) — Homepage, Search, Product page, Edit mode, Facets.
- Current Website Design — Reference designs for the legacy website.
This guide is a living document. If you spot something missing or outdated, feel free to open a PR to improve it.