Wix Velo Tutorial for Beginners: Build Your First Custom Feature
Usama MehmoodCo-owner & CEO · 10 min readWix Velo is the JavaScript platform built into Wix and Wix Studio, and the fastest way to learn it is to build something small that your site actually needs. In this tutorial you'll enable coding, write your first page code, build a live search box over a CMS collection, and finish with a secure backend form, all in a single afternoon with no previous Velo experience.
Key takeaways
- Velo has three places code lives: page code (runs in the browser for one page), public files (shared frontend code) and backend files (run on Wix's servers).
$w('#elementId')selects elements you placed in the editor. You can't use jQuery ordocument.querySelector.wix-datareads and writes CMS collections; web modules (.web.js) expose backend functions to the page with explicit permissions.- Anything secret or security-sensitive (API keys, validation, writing to private collections) belongs in the backend.
- Every snippet here is complete and checked against the current Velo API reference. Copy, rename the element IDs, run.
We've been writing code on Wix since 2014, before Velo had its current name. This is the tutorial we wish had existed then.
What you need
- A Wix account and a site in Wix Studio (the Classic Editor works too; menus differ slightly).
- Basic JavaScript: variables, functions,
async/await. Ifawaitis new to you, read the snippets slowly and you'll pick it up. - About two hours.
Step 1: Enable Dev Mode and understand the layout
In the Wix Studio editor, click the Code icon in the left sidebar, then Start Coding. (In the Classic Editor it's Dev Mode → Turn on Dev Mode in the top bar.)
Three things appear:
- A code panel at the bottom of the editor, open on the current page's code file.
- A Code sidebar listing your files: Page Code (one file per page, plus
masterPage.jsfor code that runs on every page), Public (frontend files you can import anywhere), Backend (server-side files) and Packages (npm). - A Properties & Events panel that shows the selected element's ID and lets you attach event handlers.
The most important habit: rename every element you'll touch in code. Click the element, open Properties & Events, change the ID from button12 to searchInput or submitButton. Your code will reference those IDs.
Step 2: Your first page code
Add a text element (ID greetingText) and a button (ID greetingButton) to a page. Then, in that page's code file:
// Page code
// $w.onReady runs once the page's elements have loaded. All page code starts here.
$w.onReady(() => {
$w('#greetingButton').onClick(() => {
const hour = new Date().getHours();
const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
$w('#greetingText').text = `${greeting}! Thanks for visiting.`;
});
});
Click Preview, click the button, and the text changes. Open the Developer Console at the bottom of the preview if anything doesn't work; errors show there with the line number.
That's the whole mental model of frontend Velo: wait for onReady, select elements with $w, read or set their properties, respond to events.
Step 3: Build a live search over a CMS collection
Now something useful. We'll build a search box that filters a list of services as the visitor types. The same pattern works for team members, products, FAQs or any collection.
3a. Create the collection
In the Code sidebar, under Databases (or CMS in the left menu), create a collection called Services with these fields:
| Field name | Field key | Type |
|---|---|---|
| Title | title |
Text |
| Summary | summary |
Text |
| Image | image |
Image |
| Page URL | pageUrl |
URL |
Add five or six items. Set the collection's permissions to "Anyone can read" (this is public content), with create/update/delete restricted to Admin.
3b. Add the elements
On your page, add:
- A text input, ID
searchInput, placeholder "Search services…" - A repeater, ID
servicesRepeater. Inside each item: a text elementserviceTitle, a text elementserviceSummary, an imageserviceImageand a buttonserviceButtonwith the label "Learn more". - A text element
noResultsTextsaying "No services match that search", collapsed by default (Properties & Events → Collapsed on load).
3c. The code
// Page code
import wixData from 'wix-data';
const COLLECTION = 'Services';
let debounceTimer;
$w.onReady(async () => {
// Tell the repeater how to fill each item. This must be set BEFORE
// you assign repeater.data, or it won't run for the new items.
$w('#servicesRepeater').onItemReady(($item, itemData) => {
$item('#serviceTitle').text = itemData.title;
$item('#serviceSummary').text = itemData.summary || '';
if (itemData.image) {
$item('#serviceImage').src = itemData.image;
}
if (itemData.pageUrl) {
$item('#serviceButton').link = itemData.pageUrl;
}
});
// Load everything on first visit
await loadServices('');
// Re-run the search as the visitor types, but wait 300 ms after the last
// keystroke so we don't fire a query on every character
$w('#searchInput').onInput(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
loadServices($w('#searchInput').value);
}, 300);
});
});
async function loadServices(term) {
const search = (term || '').trim();
// Start with the whole collection
let query = wixData.query(COLLECTION);
// If there's a search term, match it against title OR summary
if (search) {
query = query
.contains('title', search)
.or(wixData.query(COLLECTION).contains('summary', search));
}
try {
const results = await query.ascending('title').limit(20).find();
if (results.items.length === 0) {
$w('#servicesRepeater').collapse();
$w('#noResultsText').expand();
return;
}
$w('#noResultsText').collapse();
$w('#servicesRepeater').expand();
$w('#servicesRepeater').data = results.items; // triggers onItemReady for new items
} catch (err) {
console.error('Search failed', err);
$w('#noResultsText').text = 'Something went wrong. Please refresh the page.';
$w('#noResultsText').expand();
}
}
Preview the page. The repeater fills with all services, and typing filters it. A few things worth noticing:
wixData.query()builds a query; nothing runs until.find(). You can chain.eq(),.contains(),.gt(),.ascending(),.limit()and more..or()takes a second query on the same collection and combines them.- Repeater items are keyed by
_id. Every CMS item has one, so you can assignresults.itemsdirectly. collapse()removes an element from the layout;hide()only makes it invisible. For "no results" states, collapse is what you want.
Step 4: A secure form with backend code
The search runs entirely in the browser, which is fine for public data. Now something that shouldn't: a quote-request form that saves to a private collection. The rule is that anything involving validation, secrets or writing to private data goes in the backend.
4a. Create the private collection
Create a collection QuoteRequests with fields name (Text), email (Text), details (Text) and status (Text). Set permissions so that only Admin can read, create, update and delete. Visitors must not be able to read other people's requests, and they shouldn't be able to write directly either; our backend will do it for them.
4b. Backend web module
In the Code sidebar, under Backend, create a new file named quotes.web.js. The .web.js extension is what makes it a web module that page code can call.
// backend/quotes.web.js
import { Permissions, webMethod } from 'wix-web-module';
import wixData from 'wix-data';
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export const submitQuoteRequest = webMethod(
Permissions.Anyone, // visitors can call this; the checks below keep it safe
async (form) => {
// Never trust what the browser sends. Normalize and validate here.
const name = String(form?.name || '').trim();
const email = String(form?.email || '').trim().toLowerCase();
const details = String(form?.details || '').trim();
if (name.length < 2) {
throw new Error('Please enter your name.');
}
if (!EMAIL_PATTERN.test(email)) {
throw new Error('Please enter a valid email address.');
}
if (details.length < 10) {
throw new Error('Tell us a little more about the project (at least 10 characters).');
}
if (details.length > 2000) {
throw new Error('Please keep the details under 2,000 characters.');
}
// suppressAuth lets backend code write to a collection that visitors
// can't write to directly. Only use it after validating the input.
const saved = await wixData.insert(
'QuoteRequests',
{ name, email, details, status: 'new' },
{ suppressAuth: true }
);
// Return only what the page needs. Never return the whole item.
return { id: saved._id };
}
);
4c. Page code for the form
Add inputs nameInput, emailInput, a text box detailsInput, a button submitButton and a text element formMessage (empty by default). Then:
// Page code
import { submitQuoteRequest } from 'backend/quotes.web';
$w.onReady(() => {
$w('#submitButton').onClick(async () => {
$w('#submitButton').disable();
$w('#formMessage').text = 'Sending…';
try {
await submitQuoteRequest({
name: $w('#nameInput').value,
email: $w('#emailInput').value,
details: $w('#detailsInput').value
});
$w('#formMessage').text = 'Thanks! We will reply within one business day.';
$w('#nameInput').value = '';
$w('#emailInput').value = '';
$w('#detailsInput').value = '';
} catch (err) {
// Validation messages thrown in the backend arrive here
$w('#formMessage').text = err.message || 'Something went wrong. Please try again.';
} finally {
$w('#submitButton').enable();
}
});
});
Preview, submit the form, then open the QuoteRequests collection: your entry is there with status: new. Try submitting with a bad email and you'll see the backend's message appear on the page.
You now have the two halves of real Velo development: frontend code that handles the interface, and backend code that enforces the rules.
Where to go next
Three natural extensions, each a short step from what you've built:
- Send an email on submission. Use Wix Automations (dashboard → Automations) triggered by "item added to QuoteRequests", with no code at all. Or, for custom emails, call an email API from the backend with the key in the Secrets Manager.
- Call an external API. The same web-module pattern plus
wix-fetchconnects your site to any REST API: a CRM, a shipping provider, an AI model. Our AI chatbot tutorial is a complete example. - Dynamic pages. Turn the Services collection into one page per service with its own URL, using the CMS's dynamic pages plus a little code for related items.
If you'd rather see what Velo can do at full scale, the Wix Velo development page lists what we build for clients: member portals, custom booking systems, directories, integrations.
Common beginner mistakes
- Setting
repeater.databeforeonItemReady. The items render empty. Always registeronItemReadyfirst. - Calling an API from page code with the key in the file. Anyone can read it. Backend plus Secrets Manager, always.
- Leaving collection permissions open. A collection set to "Anyone can read" that holds customer data is a leak. Set the tightest permission that works and write through the backend.
- Forgetting
await.wixData.query().find()returns a promise. Withoutawait(or.then()),resultsis a promise object, not your data. - Testing only in Preview. Preview uses sandbox data by default. Publish to a test site and check with live data before telling a client it works.
How Wix Engine can help
If you've followed along and hit the point where the feature you need is bigger than an afternoon (a member portal, a multi-step booking flow, a CRM sync, a custom app), that's what our Velo team does all day. We're a Wix Legend Partner, we write code you can read and maintain, and projects start from $500. See Wix Velo development or Book a Free Call.
FAQ
What is Wix Velo?
Wix Velo (Velo by Wix) is the JavaScript development platform built into Wix and Wix Studio. It gives you page code for interacting with site elements, backend code that runs on Wix's servers, a database API for CMS collections, and access to Wix business apps and external APIs, all without separate hosting.
Do I need to know JavaScript to use Velo?
Yes, at a basic level. Velo is standard JavaScript (ES2020+), so variables, functions, promises and async/await are enough to follow this tutorial. You don't need to know React, Node.js frameworks or the DOM, because Velo's $w API replaces direct DOM access.
Is Wix Velo free?
Yes. Velo is included with every Wix and Wix Studio site, including the free plan. Some features you might call from code (payments, bookings, certain apps) require a premium plan, and external APIs you connect to may have their own costs.
Does Velo work in both Wix Studio and the Wix Editor?
Yes. The APIs are the same. Wix Studio adds a better built-in IDE, local development through the Wix CLI and GitHub integration. If you're starting fresh, build in Studio; see Wix Studio vs Wix Editor for the full comparison.
How do I call backend code from a Wix page?
Create a file ending in .web.js under Backend, export functions wrapped in webMethod(Permissions.X, fn) from wix-web-module, then import them in page code from backend/yourfile.web. The page calls the function like a normal async function; Wix runs it on the server and returns the result.

Usama Mehmood
Co-owner & CEO at Wix Engine
Wix Certified designer and developer, senior software and AI engineer with 15 years of experience. Started Wix Engine in 2014.
LinkedIn ↗Keep reading
All articles →
AIHow to Add an AI Chatbot to Your Wix Website
Three ways to add an AI chatbot to Wix: Wix Smart Chat, an App Market bot, or a custom Velo chatbot calling the Claude API. Working code included.
· 11 min read
Pricing & HiringHow Much Does a Wix Website Cost in 2026? Plans + Design Costs
Wix website cost in 2026: plan prices, domain, apps, and what professional design and development cost, from a Wix Legend Partner with 2,000+ projects.
· 9 min read
WixHow to Move from Wix Classic Editor to Wix Studio (Step by Step)
How to migrate Wix to Wix Studio step by step: what transfers, what doesn't, how to keep your domain, plan, CMS data and SEO, and when to get help.
· 10 min read
Want an expert to look at your project?
Book a free 30-minute call with the Wix Engine team. You'll get honest advice and a detailed proposal within 24 hours.