Skip to content

Framework7 Complete Project — Building a Mobile App from Scratch

DodaTech Updated 2026-06-28 10 min read

In this tutorial, you will learn about Framework7 Complete Project. We cover key concepts, practical examples, and best practices to help you master this topic.

Build a complete Framework7 mobile application from scratch — plan the architecture, implement views and routing, add tab navigation, manage state with the Store, implement PWA features, theme the app, and prepare for production deployment.

What You'll Learn

  • Planning a Framework7 app architecture
  • Implementing views, routing, and tabs
  • Managing state with Store
  • Adding PWA features
  • Theming and customization
  • Production deployment

Why It Matters

Building a complete app ties together all Framework7 concepts — views, pages, router, Store, PWA, theming — into a working mobile application. This lesson walks through the entire Process.

Real-World Use

A task management mobile PWA with tab navigation (Tasks, Calendar, Settings), full CRUD for tasks, offline support via service worker, push notifications for due dates, and a custom theme with dark mode.

Application Architecture

flowchart TD
    A[Task Manager App] --> B[Tabbar]
    B --> C[Tasks Tab]
    B --> D[Calendar Tab]
    B --> E[Settings Tab]
    C --> F[Task List View]
    C --> G[Task Detail View]
    C --> H[Add Task Form]
    E --> I[Theme Settings]
    E --> J[Notification Settings]
    A --> K[Store]
    A --> L[Service Worker]
    A --> M[Router]
    style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px

Project Structure

task-manager/
  index.html
  manifest.json
  sw.js
  pages/
    home.html
    tasks.html
    task-detail.html
    add-task.html
    calendar.html
    settings.html
    offline.html
    about.html
  css/
    app.css
    theme.css
  js/
    app.js
    store.js
    sw-register.js
  images/
    icons/
    splash/

App Initialization

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover" />
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="theme-color" content="#1a237e" />
  <link rel="manifest" href="manifest.json" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/framework7@8/framework7-bundle.min.css" />
  <link rel="stylesheet" href="css/app.css" />
  <link rel="stylesheet" href="css/theme.css" />
</head>
<body>
  <div id="app">
    <!-- Views with tabs -->
    <div class="view view-main view-init" data-url="/">
      <!-- Pages load via router -->
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/framework7@8/framework7-bundle.min.js"></script>
  <script src="js/store.js"></script>
  <script src="js/app.js"></script>
</body>
</html>

Expected output: The HTML shell includes Framework7 CSS/JS, manifest link, meta tags for PWA, and the app root element.

Store Setup

// js/store.js
var store = app.store.create({
  state: {
    tasks: [],
    currentTask: null,
    filter: 'all', // 'all', 'active', 'completed'
    searchQuery: '',
    loading: false
  },
  getters: {
    filteredTasks: function(state) {
      var tasks = state.tasks;
      if (state.searchQuery) {
        tasks = tasks.filter(function(t) {
          return t.title.toLowerCase().includes(state.searchQuery.toLowerCase());
        });
      }
      switch (state.filter) {
        case 'active': return tasks.filter(function(t) { return !t.completed; });
        case 'completed': return tasks.filter(function(t) { return t.completed; });
        default: return tasks;
      }
    },
    taskCount: function(state) {
      return state.tasks.length;
    },
    activeCount: function(state) {
      return state.tasks.filter(function(t) { return !t.completed; }).length;
    },
    completedCount: function(state) {
      return state.tasks.filter(function(t) { return t.completed; }).length;
    }
  },
  actions: {
    addTask: function(context, task) {
      task.id = Date.now();
      task.createdAt = new Date().toISOString();
      task.completed = false;
      context.state.tasks.push(task);
      saveToLocalStorage(context.state.tasks);
    },
    updateTask: function(context, task) {
      var index = context.state.tasks.findIndex(function(t) { return t.id === task.id; });
      if (index > -1) {
        context.state.tasks[index] = task;
        saveToLocalStorage(context.state.tasks);
      }
    },
    toggleTask: function(context, taskId) {
      var task = context.state.tasks.find(function(t) { return t.id === taskId; });
      if (task) {
        task.completed = !task.completed;
        saveToLocalStorage(context.state.tasks);
      }
    },
    deleteTask: function(context, taskId) {
      context.state.tasks = context.state.tasks.filter(function(t) { return t.id !== taskId; });
      saveToLocalStorage(context.state.tasks);
    },
    setFilter: function(context, filter) {
      context.state.filter = filter;
    },
    setSearch: function(context, query) {
      context.state.searchQuery = query;
    },
    loadTasks: function(context) {
      var saved = localStorage.getItem('tasks-data');
      if (saved) {
        context.state.tasks = JSON.parse(saved);
      }
    }
  }
});

function saveToLocalStorage(tasks) {
  localStorage.setItem('tasks-data', JSON.stringify(tasks));
}

Expected output: The Store manages tasks with CRUD actions, computed getters for filtered views, and localStorage persistence.

Main App Setup

// js/app.js
var app = new Framework7({
  root: '#app',
  name: 'Task Manager',
  id: 'com.dodatech.taskmanager',
  theme: 'auto',
  serviceWorker: {
    path: '/sw.js'
  },
  routes: [
    {
      path: '/',
      url: 'pages/tasks.html',
      tabs: [
        { path: '/tasks/', id: 'tasks', url: 'pages/tasks.html' },
        { path: '/calendar/', id: 'calendar', url: 'pages/calendar.html' },
        { path: '/settings/', id: 'settings', url: 'pages/settings.html' }
      ]
    },
    { path: '/task/:id/', component: 'pages/task-detail.html' },
    { path: '/add/', component: 'pages/add-task.html' },
    { path: '(.*)', url: 'pages/offline.html' }
  ],
  on: {
    init: function() {
      // Load data
      store.dispatch('loadTasks');
      // Restore theme
      var theme = localStorage.getItem('app-theme') || 'light';
      applyTheme(theme);
    },
    pageInit: function(page) {
      if (page.name === 'tasks') {
        updateTaskUI();
      }
    }
  }
});

Expected output: The app initializes with routes for task list (with tabs), task detail, add task form, and a catch-all route.

Task List Page with Tabbar

<!-- pages/tasks.html -->
<div class="page" data-name="tasks">
  <div class="navbar">
    <div class="navbar-inner">
      <div class="title">Tasks</div>
      <div class="right">
        <a href="/add/" class="link icon-only">
          <i class="icon f7-icons">plus</i>
        </a>
      </div>
    </div>
  </div>

  <div class="toolbar tabbar tabbar-labels">
    <div class="toolbar-inner">
      <a href="#tab-all" class="tab-link tab-link-active">All</a>
      <a href="#tab-active" class="tab-link">Active</a>
      <a href="#tab-completed" class="tab-link">Completed</a>
    </div>
  </div>

  <div class="page-content">
    <form class="searchbar" id="task-search">
      <div class="searchbar-input">
        <input type="text" placeholder="Search tasks" />
        <i class="searchbar-icon"></i>
      </div>
    </form>

    <div class="tabs">
      <div class="tab tab-active" id="tab-all">
        <div class="list media-list" id="task-list"></div>
      </div>
      <div class="tab" id="tab-active">
        <div class="list media-list" id="task-list-active"></div>
      </div>
      <div class="tab" id="tab-completed">
        <div class="list media-list" id="task-list-completed"></div>
      </div>
    </div>
  </div>
</div>
// Render tasks in the list
function updateTaskUI() {
  renderTaskList('#task-list', store.state.tasks);
  renderTaskList('#task-list-active',
    store.state.tasks.filter(function(t) { return !t.completed; }));
  renderTaskList('#task-list-completed',
    store.state.tasks.filter(function(t) { return t.completed; }));
}

function renderTaskList(selector, tasks) {
  var list = $$(selector);
  if (!tasks.length) {
    list.html('<div class="block text-align-center" style="padding:40px"><p>No tasks found</p></div>');
    return;
  }
  list.html('<ul>' + tasks.map(function(task) {
    return '<li>' +
      '<label class="item-content item-checkbox">' +
        '<input type="checkbox" ' + (task.completed ? 'checked' : '') +
          ' data-id="' + task.id + '" class="task-toggle" />' +
        '<i class="icon icon-checkbox"></i>' +
        '<div class="item-inner">' +
          '<div class="item-title" style="' + (task.completed ? 'text-decoration:line-through;color:#999' : '') + '">' +
            task.title +
          '</div>' +
          '<div class="item-after">' + task.priority + '</div>' +
        '</div>' +
      '</label>' +
    '</li>';
  }).join('') + '</ul>');
}

Expected output: The tasks page shows a search bar, filter tabs (All, Active, Completed), and a list of tasks with checkboxes and priority indicators.

Add Task Form

<!-- pages/add-task.html -->
<div class="page" data-name="add-task">
  <div class="navbar">
    <div class="navbar-inner">
      <div class="left">
        <a href="#" class="link back">
          <i class="icon icon-back"></i>
          <span>Back</span>
        </a>
      </div>
      <div class="title">New Task</div>
    </div>
  </div>
  <div class="page-content">
    <div class="list" style="margin:0">
      <ul>
        <li>
          <div class="item-content item-input">
            <div class="item-inner">
              <div class="item-title item-label">Title</div>
              <div class="item-input-wrap">
                <input type="text" id="task-title" placeholder="Task title" />
              </div>
            </div>
          </div>
        </li>
        <li>
          <div class="item-content item-input">
            <div class="item-inner">
              <div class="item-title item-label">Description</div>
              <div class="item-input-wrap">
                <textarea id="task-desc" placeholder="Task description"></textarea>
              </div>
            </div>
          </div>
        </li>
        <li>
          <div class="item-content item-input">
            <div class="item-inner">
              <div class="item-title item-label">Priority</div>
              <div class="item-input-wrap">
                <select id="task-priority">
                  <option value="low">Low</option>
                  <option value="medium" selected>Medium</option>
                  <option value="high">High</option>
                </select>
              </div>
            </div>
          </div>
        </li>
        <li>
          <div class="item-content item-input">
            <div class="item-inner">
              <div class="item-title item-label">Due Date</div>
              <div class="item-input-wrap">
                <input type="text" id="task-due" placeholder="Select date" readonly />
              </div>
            </div>
          </div>
        </li>
      </ul>
    </div>
    <div class="block">
      <button class="button button-fill button-large" id="save-task">Save Task</button>
    </div>
  </div>
</div>
// Save task handler
$$('#save-task').on('click', function() {
  var title = $$('#task-title').val();
  if (!title.trim()) {
    app.dialog.alert('Please enter a task title');
    return;
  }
  store.dispatch('addTask', {
    title: title,
    description: $$('#task-desc').val(),
    priority: $$('#task-priority').val(),
    dueDate: $$('#task-due').val()
  });
  app.dialog.alert('Task added!');
  app.views.main.router.back();
});

Expected output: The add task form collects title, description, priority, and due date. Saving dispatches the addTask action, persists to localStorage, and navigates back to the task list.

Settings Page

<!-- pages/settings.html -->
<div class="page" data-name="settings">
  <div class="navbar">
    <div class="navbar-inner">
      <div class="title">Settings</div>
    </div>
  </div>
  <div class="page-content">
    <div class="list">
      <ul>
        <li class="item-divider">Appearance</li>
        <li>
          <div class="item-content">
            <div class="item-inner">
              <div class="item-title">Dark Mode</div>
              <div class="item-after">
                <label class="toggle">
                  <input type="checkbox" id="dark-mode-toggle" />
                  <span class="toggle-icon"></span>
                </label>
              </div>
            </div>
          </div>
        </li>
        <li>
          <div class="item-content item-input">
            <div class="item-inner">
              <div class="item-title item-label">Font Size</div>
              <div class="item-input-wrap">
                <select id="font-size">
                  <option value="14">Small</option>
                  <option value="16" selected>Normal</option>
                  <option value="18">Large</option>
                  <option value="20">X-Large</option>
                </select>
              </div>
            </div>
          </div>
        </li>
        <li class="item-divider">Notifications</li>
        <li>
          <div class="item-content">
            <div class="item-inner">
              <div class="item-title">Push Notifications</div>
              <div class="item-after">
                <label class="toggle">
                  <input type="checkbox" id="push-toggle" />
                  <span class="toggle-icon"></span>
                </label>
              </div>
            </div>
          </div>
        </li>
        <li>
          <div class="item-content">
            <div class="item-inner">
              <div class="item-title">Due Date Reminders</div>
              <div class="item-after">
                <label class="toggle">
                  <input type="checkbox" id="reminder-toggle" checked />
                  <span class="toggle-icon"></span>
                </label>
              </div>
            </div>
          </div>
        </li>
        <li class="item-divider">Data</li>
        <li>
          <a href="#" class="item-link item-content" id="clear-data">
            <div class="item-inner">
              <div class="item-title" style="color:#f44336">Clear All Data</div>
            </div>
          </a>
        </li>
        <li>
          <div class="item-content">
            <div class="item-inner">
              <div class="item-title">Tasks Stored</div>
              <div class="item-after" id="stored-count">0</div>
            </div>
          </div>
        </li>
        <li class="item-divider">About</li>
        <li>
          <a href="/about/" class="item-link item-content">
            <div class="item-inner">
              <div class="item-title">About Task Manager</div>
              <div class="item-after"><i class="icon f7-icons">chevron-right</i></div>
            </div>
          </a>
        </li>
      </ul>
    </div>
  </div>
</div>
// Dark mode toggle
$$('#dark-mode-toggle').on('change', function() {
  var isDark = this.checked;
  applyTheme(isDark ? 'dark' : 'light');
  localStorage.setItem('app-theme', isDark ? 'dark' : 'light');
});

function applyTheme(theme) {
  if (theme === 'dark') {
    document.documentElement.style.setProperty('--f7-bars-bg-color', '#1e1e1e');
    document.documentElement.style.setProperty('--f7-bg-color', '#121212');
    document.documentElement.style.setProperty('--f7-text-color', '#e0e0e0');
  } else {
    document.documentElement.style.setProperty('--f7-bars-bg-color', '#1a237e');
    document.documentElement.style.setProperty('--f7-bg-color', '#ffffff');
    document.documentElement.style.setProperty('--f7-text-color', '#212121');
  }
}

// Font size
$$('#font-size').on('change', function() {
  document.documentElement.style.fontSize = this.value + 'px';
  localStorage.setItem('font-size', this.value);
});

// Clear data
$$('#clear-data').on('click', function() {
  app.dialog.confirm('Delete all tasks?', function() {
    localStorage.removeItem('tasks-data');
    store.state.tasks = [];
    app.dialog.alert('All tasks deleted');
  });
});

Expected output: The settings page controls dark mode, font size, notifications, and data management. Changes apply immediately and persist.

PWA Integration

// Service worker registration
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(function(reg) {
      console.log('SW registered');
    });
}

// Install prompt
var deferredPrompt;
window.addEventListener('beforeinstallprompt', function(e) {
  e.preventDefault();
  deferredPrompt = e;
  $$('#install-banner').show();
});

$$('#install-btn').on('click', function() {
  if (deferredPrompt) {
    deferredPrompt.prompt();
    deferredPrompt.userChoice.then(function(result) {
      if (result.outcome === 'accepted') {
        $$('#install-banner').hide();
      }
      deferredPrompt = null;
    });
  }
});

Expected output: The app is installable on the home screen with PWA support. Service worker enables offline access to cached pages.

Production Build

# Build process for production
# 1. Minify HTML pages
# 2. Minify CSS
# 3. Minify JavaScript
# 4. Optimize images (WebP)
# 5. Generate service worker
# 6. Deploy to web server

# Example using basic tools:
# Install dependencies
npm install -g html-minifier uglify-js clean-css-cli

# Minify HTML
html-minifier --collapse-whitespace pages/*.html -o dist/pages/

# Minify CSS
cleancss -o dist/css/app.css css/app.css

# Minify JS
uglifyjs js/app.js -o dist/js/app.js

# Copy manifest and service worker
cp manifest.json dist/
cp sw.js dist/

# Deploy (example)
# rsync -avz dist/ user@server:/var/www/taskapp/

Expected output: The production build produces optimized, minified files ready for deployment. The app runs as a full PWA with offline support.

Common Mistakes

  1. Not handling empty states - Lists and dashboards should show helpful messages when there are no tasks or data.

  2. Forgetting to restore state on app restart - Users expect their data to persist. Always load from localStorage on app init.

  3. Not testing offline mode - PWAs must work offline. Test by disabling network in DevTools and verifying the app still functions.

  4. Overcomplicating the Store - Keep the Store focused on app state. Use app.data for simple persisted data and Store for reactive UI state.

  5. Ignoring Accessibility - Add proper labels, roles, and ARIA attributes to components. Test with screen readers.

Practice Questions

  1. How do you structure a Framework7 app with tab-based navigation?
  2. How does the Store manage CRUD operations for tasks?
  3. How do you implement dark mode with CSS variable overrides?
  4. How do you add PWA install prompt to the app?
  5. How do you prepare a Framework7 app for production deployment?

Challenge: Extend the task manager with: a calendar view showing tasks grouped by due date, drag-and-drop task reordering, task categories with color labels, data export to CSV, biometric authentication for app lock, and multi-language support with i18n.

FAQ

How do I structure a large Framework7 app?

Use separate JS files per module (store, routes, controllers), organize pages in folders by feature, and use the Store for shared state. Consider using Framework7 React/Vue for very large apps.

Can I use the same Store across multiple views?

Yes. Store is global. All views access the same state. Use getters to derive view-specific data from the shared state.

How do I add animations between tab switches?

Set animate: true on tab switch. Framework7 uses CSS transitions. Custom tab animations can be defined via CSS on the .tab elements.

How do I handle form validation in a Framework7 app?

Use HTML5 validation attributes (required, type, pattern) with item-input-error-message elements. For custom validation, use JavaScript before submitting.

How do I deploy a Framework7 app to the App Store?

Use Capacitor or Cordova to wrap the PWA as a native app. Framework7 works seamlessly with both. You can then submit to Apple App Store and Google Play Store.

Mini Project

Build the complete Task Manager app as specified in this lesson: tabbed navigation (Tasks, Calendar, Settings), full CRUD with Store persistence, search and filter functionality, dark mode and font size settings, PWA service worker with offline support, install prompt, date picker for due dates, and production build configuration. This serves as a template you can adapt for any Framework7 mobile app.

What's Next

You have completed the Framework7 guide. Explore other frameworks or continue building real projects with the task manager as your template.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro