The gap between design and development often frustrates many teams. Designers use tools like Figma to create beautiful interfaces, but developers find it hard to turn these designs into working code using traditional IDEs. This disconnect sometimes slows down development and can lead to misunderstandings and costly fixes.
Onlook is a new tool that helps bridge this gap by bringing design features directly into your development environment. With Onlook, you can improve your workflow, enhance teamwork between designers and developers, and deliver products faster and more accurately.
In this article, we’ll explore Onlook, a tool that brings design capabilities directly into the development environment. We will look at how it compares to existing tools like Figma and Webflow, and provide a practical guide to setting it up.
Onlook is an open source visual editor designed specifically for React applications. It enables developers to create and modify user interfaces while working with their live React components. Onlook provides a direct manipulation interface similar to Figma, offering features like drag-and-drop editing, real-time preview, and seamless code generation.
With Onlook, developers can visually manipulate components, adjust styles, and see changes in real time, all while working within their application’s actual codebase. Its direct integration simplifies the handoff process between design and development, reducing inconsistencies and streamlining the overall workflow.
Unlike traditional design tools or no-code builders, Onlook allows developers to directly integrate their existing React projects and maintain full development control while gaining the benefits of visual editing. Onlook is relatively new, with over 4.2k Github stars. It has over 40 contributors, which means they regularly ship updates and new features.
Onlook is a standalone app, but to follow along with this tutorial, you will also need:
To get started with Onlook, follow these steps:
Once running, you will be presented with your application’s homepage.
Now that we’ve got our app running, let’s dive into the Onlook interface before designing our first app.
When you first open Onlook, you’ll see an interface that combines visual editing with code features. The interface has several key areas, each with a specific purpose in your development process.
The main workspace has three main sections:
Additionally, there are other important sections:
The Onlook interface connects design and development. Any design change you make updates the underlying React code immediately. Any code changes are also reflected in the design. This two-way relationship is what differentiates Onlook from traditional design tools.
Unlike traditional workflows where you export designs to code, Onlook maintains a live connection between your visual edits and the React code. This allows for:
Let’s break it down to the components. When you’re working on a component, you can:
Let’s create a to-do list app using Onlook to show how the visual editor works with React. This example will highlight Onlook’s main features while building a real application that uses state management, user interactions, and component composition.
When you start designing in Onlook, you have two options: you can either use the visual editor directly, or you can use Onlook’s AI assistant. Beginners or those who want guidance may find the AI assistant helpful. You can describe what you want, and the AI will help you create a basic layout that you can improve:
The visual editor lets you drag and drop elements, change styles, and see quick results. As you make changes, Onlook automatically writes the React code for you. At first, all the code goes into the page.tsx
file, which is a starting point, but not the best setup for a production app.
Structuring the application
While Onlook’s automatic code generation is useful, real-world applications need a clearer structure. To address this, we can use component-based architecture. This approach will separate concerns and make the code easier to maintain.
Here’s how we will organize our project:
app/ ├── page.tsx └── components/ ├── TodoContainer.tsx ├── TodoForm.tsx ├── TodoList.tsx ├── TodoItem.tsx └── types.ts
This structure follows React best practices by breaking the user interface into clear, reusable components. Each component, which we’ll demonstrate below, has a specific function, helping the code stay organized and easier to understand.
Onlook’s Types
component:
// types.ts export interface Todo { id: number; text: string; completed: boolean; }
Onlook’s Page
component:
// page.tsx import TodoContainer from './components/TodoContainer'; export default function Page() { return ( <div className="w-full min-h-screen bg-gradient-to-br from-purple-50 to-white dark:from-gray-900 dark:to-gray-800 p-4"> <div className="max-w-2xl mx-auto"> <div className="text-center mb-8"> <h1 className="text-4xl font-bold text-purple-600 dark:text-purple-400 mb-2"> Todo List </h1> <p className="text-gray-600 dark:text-gray-300"> Stay organized and productive </p> </div> <TodoContainer /> </div> </div> ); }
Onlook’s TodoContainer
component:
// components/TodoContainer.tsx 'use client'; import { useState } from 'react'; import TodoForm from './TodoForm'; import TodoList from './TodoList'; import { Todo } from './types'; export default function TodoContainer() { const [todos, setTodos] = useState<Todo[]>([]); const [newTodo, setNewTodo] = useState(''); const addTodo = (e: React.FormEvent) => { e.preventDefault(); if (newTodo.trim()) { setTodos([ ...todos, { id: Date.now(), text: newTodo.trim(), completed: false, }, ]); setNewTodo(''); } }; const toggleTodo = (id: number) => { setTodos( todos.map((todo) => (todo.id === id ? { ...todo, completed: !todo.completed } : todo)), ); }; const deleteTodo = (id: number) => { setTodos(todos.filter((todo) => todo.id !== id)); }; return ( <> <TodoForm newTodo={newTodo} setNewTodo={setNewTodo} addTodo={addTodo} /> <TodoList todos={todos} toggleTodo={toggleTodo} deleteTodo={deleteTodo} /> </> ); }
Onlook’s TodoForm
component:
// components/TodoForm.tsx 'use client'; interface TodoFormProps { newTodo: string; setNewTodo: (value: string) => void; addTodo: (e: React.FormEvent) => void; } export default function TodoForm({ newTodo, setNewTodo, addTodo }: TodoFormProps) { return ( <form onSubmit={addTodo} className="mb-8"> <div className="flex gap-2"> <input type="text" value={newTodo} onChange={(e) => setNewTodo(e.target.value)} placeholder="What needs to be done?" className="flex-1 p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent" /> <button type="submit" className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-colors duration-200" > Add </button> </div> </form> ); }
Onlook’s TodoList
component:
// components/TodoList.tsx 'use client'; import { Todo } from './types'; import TodoItem from './TodoItem'; interface TodoListProps { todos: Todo[]; toggleTodo: (id: number) => void; deleteTodo: (id: number) => void; } export default function TodoList({ todos, toggleTodo, deleteTodo }: TodoListProps) { return ( <div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> {todos.length === 0 ? ( <p className="text-center text-gray-500 dark:text-gray-400 py-8"> No todos yet. Add one above! </p> ) : ( <ul className="space-y-3"> {todos.map((todo) => ( <TodoItem key={todo.id} todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo} /> ))} </ul> )} {todos.length > 0 && ( <div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700"> <div className="flex justify-between text-sm text-gray-600 dark:text-gray-400"> <span>{todos.filter((t) => !t.completed).length} items left</span> <span>{todos.filter((t) => t.completed).length} completed</span> </div> </div> )} </div> ); }
Onlook’s TodoItem
component:
// components/TodoItem.tsx 'use client'; import { Todo } from './types'; interface TodoItemProps { todo: Todo; toggleTodo: (id: number) => void; deleteTodo: (id: number) => void; } export default function TodoItem({ todo, toggleTodo, deleteTodo }: TodoItemProps) { return ( <li className="flex items-center gap-3 bg-gray-50 dark:bg-gray-700/50 p-4 rounded-lg group"> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} className="w-5 h-5 rounded border-gray-300 text-purple-600 focus:ring-purple-500" /> <span className={`flex-1 ${ todo.completed ? 'line-through text-gray-400 dark:text-gray-500' : 'text-gray-700 dark:text-gray-200' }`} > {todo.text} </span> <button onClick={() => deleteTodo(todo.id)} className="opacity-0 group-hover:opacity-100 transition-opacity text-red-500 hover:text-red-600 p-1" > <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" > <path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" /> </svg> </button> </li> ); }
One useful feature of Onlook is its ability to link the visual editor and the code in both directions. This is done using a special data-oid
attribute that Onlook adds to components during development:
<TodoContainer data-oid="zshvwmp"/>
This attribute helps Onlook keep track of changes to components, connect visual elements with the code, allow real-time updates in both directions and maintain component functions while editing visually.
While you build your application, you can use Onlook’s visual editor to improve the design without hurting the quality of your code. For instance, you can change component layouts, adjust spacing, refine colors and fonts, and test how your app responds to different screen sizes:
Web development tools meet different needs and workflows. They offer solutions for both design and development. To understand where Onlook fits in, we’ll compare it to other tools, highlighting its strengths and weaknesses.
Onlook vs. Figma: Comparing traditional design tools
Traditional design tools are strong at illustration, collaboration, prototyping, and design asset management. Onlook offers something different by allowing direct manipulation of React components, real-time interaction with component states and props, production-ready code, seamless integration with development workflows, and active testing of component behavior during design.
Onlook vs. Webflow: Comparing no-code builders
No-code platforms offer visual development and code export, they are strong at rapid prototyping, visual CMS integration, and built-in hosting. Onlook is different by being directly integrated into existing React codebases, maintaining component logic, and being developer-first while allowing visual editing. It integrates with development tools and supports custom components with full React features, and clean React code.
Onlook improves workflow across the React ecosystem. It improves cohesion between design and development teams and allows users to edit React components visually while keeping the code intact. By syncing visual changes with the code in real time, Onlook resolves a variety of common workflow issues. In addition, its AI features and smooth integration with existing React projects make it a useful addition to modern development processes.
Although Onlook is still being improved, it shows great potential to change how React applications are built. As it adds new features, it could become a vital tool for React developers looking to enhance their workflows and create high-quality user experiences more easily. Check out Onlook and try out its features in your projects.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Islands integrates React into legacy codebases, enabling modernization without requiring a complete rewrite.
JavaScript generators offer a powerful and often overlooked way to handle asynchronous operations, manage state, and process data streams.
webpack’s Module Federation allows you to easily share code and dependencies between applications, helpful in micro-frontend architecture.
Whether you’re part of the typed club or not, one function within TypeScript that can make life a lot easier is object destructuring.