For AI agents: the complete documentation index is available at /next/llms.txt, the full documentation bundle is available at /next/llms-full.txt, and this page is available as Markdown at /next/lynxtron/learn/introduction_to_browser.md.
  • English
  • Tutorial: Build Your Own Browser

    This tutorial walks you through building a simple multi-tab browser step by step. It does not assume any prior knowledge of Lynxtron. The techniques you learn here form the foundation for building any Lynxtron desktop application.

    You will learn:

    What Are We Building?

    First, let us look at the final result. We will build a fully functional multi-tab browser that includes:

    • Custom title bar: with traffic-light window control buttons for close, minimize, and maximize
    • Tab management: add, switch, and close multiple tabs
    • Navigation bar: includes forward, back, refresh buttons, and an address bar
    • Web content area: uses <webview> to load and display web pages

    Try the Finished Browser

    The complete Browser demo includes the Lynx UI, Node.js main process, and preload bridge; you can inspect all of them in the source panel below.

    Tutorial Setup

    Please refer to our detailed installation guide, which walks you through creating a new Lynxtron project.

    You may notice that the project uses TypeScript. Although Lynxtron supports both TypeScript and plain JavaScript, we recommend TypeScript for a better development experience, including static type checking and improved editor IntelliSense.

    Install the webview Native Library

    The optional @lynx-js/cef-webview package provides <webview>. Install it from the project root:

    npm
    yarn
    pnpm
    bun
    deno
    npm install @lynx-js/cef-webview

    create-lynxtron already configures pluginLynxtron() for the desktop build. Check the webview tooling requirements before continuing. The matching tooling loads and packages the native library; the app still initializes CEF before creating a window, as shown below.

    If you use pnpm 11 and installation reports blocked build scripts for these packages, merge the following into the project's pnpm-workspace.yaml and run installation again:

    allowBuilds:
      '@lynx-js/cef-webview': true
      '@lynx-js/lynxtron': true
      '@lynx-js/lynxtron-builder': true

    Build a Custom Title Bar with Lynxtron

    Because the browser title bar contains tabs, the default Lynxtron window title bar does not meet our needs, so we need to create a custom one. The first step is to create a frameless window so we can fully customize the browser's appearance, including the title bar and the traffic-light control buttons.

    Step 1: Create a Frameless Window

    When creating LynxWindow, set frame: false to remove the system title bar:

    // src/main/desktop/main.ts
    import { app, LynxWindow } from '@lynx-js/lynxtron';
    import cefWebview from '@lynx-js/cef-webview/lynxtron';
    let mainWindow: LynxWindow | null = null;
    function createWindow() {
      mainWindow = new LynxWindow({
        width: 1200,
        height: 800,
        frame: false, // Key: remove the system frame
      });
      registerWindowControls(mainWindow);
    }
    
    app.whenReady().then(() => {
      cefWebview.initialize();
      createWindow();
    });

    Merge this initialization into the project's existing startup flow and keep its bundle loading code. Initialize CEF before creating any LynxWindow.

    CEF selects its storage directory based on the host application's identity. Multiple instances sharing the same identity may encounter storage conflicts. Multiple tabs in this browser are supported. If initialize() throws, stop startup and inspect the error instead of continuing to create WebViews.

    Remove the title bar

    Step 2: Build the Custom Title Bar

    After removing the system title bar, we need to build the browser title bar ourselves. It mainly consists of three parts:

    1. Window control buttons: used to maximize, minimize, and close the browser
    2. Tabs: used to display the title information of each web page
    3. Add button: used to create a new tab

    We place these components inside a top bar and use Flexbox for layout:

    // src/app/App.tsx
    return (
      <view className="top-bar">
        <view className="traffic-lights">...</view>
        <scroll-view scroll-x className="tabs-container">
          {tabs.map((tab) => (
            <NewTab />
          ))}
          <view className="add-tab-btn" bindtap={handleAddTab}>
            +
          </view>
        </scroll-view>
      </view>
    );
    Custom title bar

    Step 3: Enable Window Dragging

    Because the system title bar is gone, users can no longer drag the window in the default way. We need to enable dragging on the custom top bar (.top-bar).

    Lynxtron supports the CSS property -x-app-region: drag to define draggable regions, similar to Electron's -webkit-app-region: drag.

    /* src/app/App.css */
    .top-bar {
      /* Key: enable window dragging */
      -x-app-region: drag;
    }

    The effect looks like this:

    Implement Window Controls Through Communication Between Node.js and LynxView

    Because we removed the system-native title bar, we need to implement close, minimize, and maximize ourselves. This requires communication between Node.js and LynxView.

    Lynx JS Thread

    // src/app/App.tsx
    const handleClose = () => {
      NativeModules.bridge.call('close', {}, () => {});
    };
    
    const handleMinimize = () => {
      NativeModules.bridge.call('minimize', {}, () => {});
    };
    
    const handleFullScreenTap = () => {
      NativeModules.bridge.call('maximize', {}, () => {});
    };

    Node JS Thread

    // src/main/desktop/main.ts
    // Called from createWindow() above.
    function registerWindowControls(window: LynxWindow) {
      let isFullScreen = false;
      window.on('-lynx-invoke', (event, methodName, params) => {
        console.log('window is invoked', methodName, params);
        if (methodName === 'close') {
          console.log('Closing window...');
          event.sendReply(null);
          window.close();
        }
    
        if (methodName === 'minimize') {
          window.minimize();
          event.sendReply(null);
        }
    
        if (methodName === 'maximize') {
          isFullScreen = !isFullScreen;
          console.log('Toggling full screen to:', isFullScreen);
          window.setFullScreen(isFullScreen);
          event.sendReply(null);
        }
      });
    }

    This allows the browser window to close, minimize, and maximize:

    How to Use <webview> from Lynx Native Libraries

    Manage Tabs

    One of the core capabilities of a browser is multi-tab management. We need to maintain the state of a tab list.

    // src/app/App.tsx
    const [tabs, setTabs] = useState<Tab[]>([
      {
        id: '1',
        title: 'New Tab',
        url: 'https://www.lynxjs.org',
      },
    ]);
    const [selectedTabId, setSelectedTabId] = useState<string>('1');
    
    // Add a new tab
    const handleAddTab = () => {
      const newId = Date.now().toString();
      setTabs([...tabs, { id: newId, title: 'New Tab', url: '' }]);
      setSelectedTabId(newId);
    };
    
    // Close a tab
    const handleRemoveTab = (id: string) => {
      const newTabs = tabs.filter((t) => t.id !== id);
      setTabs(newTabs);
      // If the closed tab is the currently selected one, switch to another tab
      if (id === selectedTabId && newTabs.length > 0) {
        setSelectedTabId(newTabs[newTabs.length - 1].id);
      }
    };

    At this point, we have finished adding and closing tabs:

    Add the <webview> Component

    After installing the native library and initializing CEF as shown above, use <webview> directly in the Lynx UI to load external pages. The UI code does not import the CEF package.

    <view className="content-area">
      {tabs.map((tab) => (
        <view key={tab.id} className="tab-content">
          {tab.url && (
            <webview
              className="webview-container"
              id={`webview-${tab.id}`}
              src={tab.url}
            />
          )}
        </view>
      ))}
    </view>

    After adding this element, we can load external web pages normally:

    Load external web pages

    Tab Switching Strategy

    To preserve page state when switching tabs, meaning the page does not reload, we do not destroy and recreate Webviews during tab switches. Instead, we render all Webviews and use z-index to control which one stays on top.

    // src/app/App.tsx
    <view className="content-area">
      {tabs.map((tab) => (
        <view
          key={tab.id}
          className="webview-wrapper"
          style={{ zIndex: selectedTabId === tab.id ? 1 : 0 }} // Only the selected tab stays on top
        >
          <webview
            className="webview"
            src={tab.url}
            use-osr={true} // Enable off-screen rendering
            enable-debug={true} // Enable debugging
          />
        </view>
      ))}
    </view>

    This stacking strategy ensures a smooth tab-switching experience while preserving page state. The effect looks like this:

    We also need to control Webview behavior, such as going back and refreshing, through buttons in the Lynx UI. To do that, we need a reference to <webview> and call its methods.

    // src/app/App.tsx
    const webviewRefs = useRef<Record<string, any>>({});
    // Bind the ref
    <webview
      id={`webview-${tab.id}`}
      ref={(ref) => (webviewRefs.current[tab.id] = ref)}
    />;
    // Reload the page
    const handleReload = () => {
      const ref = webviewRefs.current[selectedTabId];
      if (ref) {
        ref.invoke({ method: 'reload' }).exec();
      }
    };
    // Go back
    const goBack = () => {
      const ref = webviewRefs.current[selectedTabId];
      if (ref) {
        // Execute JavaScript inside the page
        ref
          .invoke({
            method: 'eval',
            params: { func: 'window.history.back()' },
          })
          .exec();
      }
    };

    This gives the browser refresh and back navigation:

    Summary

    Through this tutorial, you have built a basic multi-tab browser and learned several key capabilities in Lynxtron desktop app development:

    1. Custom browser window: by creating a frameless window, building a custom top bar, and adding draggable regions to it, you established the browser's core visual and interaction structure.
    2. Communication between Lynx and Node.js: by passing messages between the Lynx side and the Node.js side through the JS Bridge, you implemented close, minimize, and fullscreen window controls.
    3. Integration and control of <webview>: by using <webview> to load web pages and calling methods through references, you implemented page loading, refresh, and back navigation.
    Except as otherwise noted, this work is licensed under a Creative Commons Attribution 4.0 International License, and code samples are licensed under the Apache License 2.0.