Skip to content
SiteEmail

Our React starter is a React 19 + Vite + TypeScript project, pre-configured to render correctly in the Gameface Player. It is the quickest way to build a UI within Gameface when your team already works in React.

React uses a Virtual DOM, which adds more runtime overhead than SolidJS. That trade is worth making when your existing React experience saves more time than the overhead costs. The Recommended Tech Stack covers the full performance rationale.

We scaffold the project with create-gameface-app , passing the react template so the CLI skips the framework prompt.

  1. Run the initializer and let it install the dependencies for you:

    Terminal window
    npm create gameface-app -- --template react --default

    The CLI asks you to name your project, then scaffolds it and installs the dependencies.

  2. Move into the new folder and start Vite:

    Terminal window
    cd your-project-name
    npm run dev

    The dev server runs on port 3000.

  3. Point the Gameface Player at http://localhost:3000/. The sample UI loads, and Hot Module Replacement updates it every time you save a file.

The template opens on a sample entry screen that doubles as a working reference for your own views. It contains:

  • A parallax-style animated background.
  • An Achievements panel in the left column.
  • A PlayerStats panel in the top right.
  • An interactive CarouselMenu along the bottom center.
  • A START GAME action button.

The project also ships a gameface-models/Model.json file, which wires up the data-binding checks that the linter runs against your markup.

The template keeps styles flat and cache-friendly, which is what Gameface needs to hold a high frame rate. It layers a few approaches:

LayerApproach
Component layoutSCSS modules, one .module.scss per component
UtilitiesUnoCSS with presetGameface()
Inline stylesKept minimal, for genuinely dynamic values like a progress bar width. Static values become gf-prop--* classes through vite-plugin-gameface-styles plugin
Responsive sizingrem units relative to the html font size, scaled from a 1920x1080 reference by the useResponsiveRootFontSize hook

The plugin and the preset work as a pair. vite-plugin-gameface-styles pulls static inline styles out of your markup and encodes them into gf-prop--* class names. presetGameface() is the UnoCSS preset that decodes those names back into real CSS, so the generated classes resolve to actual styles instead of nothing. The template wires both together in uno.config.ts. Dropping UnoCSS costs you the automatic class generation from inline styles, unless you write a preset of your own to replace it.

Vite is set up with the Sass modern-compiler, base: './', and cssCodeSplit: false so the output loads cleanly in the engine.

The template ships with the standard set of scripts:

CommandDescription
npm run devStart the dev server on port 3000
npm run buildTypecheck and build the production bundle to dist/
npm run lintRun ESLint with eslint-plugin-gameface
npm run previewPreview the production build locally

Data-binding and mocked models are not part of the template, so this section covers wiring them up in React. The footer’s FPS and ping counters serve as the example: right now they read from a hardcoded systemStats object. The steps below move them onto a Gameface model instead. The same pattern covers any model the game exposes later.

The steps below only cover the React-specific mechanics. Mocking Data Models explains what a model is, and Data-Binding Basics covers the full binding syntax.

  1. Data-binding needs the engine global, which comes from cohtml.js. Copy cohtml.js and cohtml.d.ts out of your Gameface package (Player/Samples/uiresources/library) into src/data/, then import the library once in your entry point:

    src/main.tsx
    import 'virtual:uno.css';
    import './styles/global.scss';
    import './data/cohtml';
    import App from './App';
  2. Add one JSON file per model under gameface-models/. The file name becomes the model name, and only the shape matters, so the values are placeholders:

    gameface-models/SystemStats.json
    {
    "fps": 60,
    "ping": 12
    }

    eslint-plugin-gameface reads this directory and checks every binding expression against it. A typo such as {{SystemStats.fpsx}} fails npm run lint instead of silently rendering nothing.

  3. engine.createJSModel exposes the model as a global variable. Declare it next to the type so TypeScript knows it exists:

    src/data/types.ts
    export type SystemStats = {
    fps: number;
    ping: number;
    };
    declare global {
    var SystemStats: SystemStats;
    }
  4. Register the model once the engine is ready. The template already exports a systemStats object, so pass it straight in. The interval stands in for the game pushing new values, and every change needs updateWholeModel followed by synchronizeModels to reach the DOM:

    src/App.tsx
    import { useState } from 'react';
    import { useEffect, useState } from 'react';
    export default function App() {
    const [selectedModeId, setSelectedModeId] = useState('multiplayer');
    useResponsiveRootFontSize();
    useEffect(() => {
    let intervalId: number | undefined;
    let created = false;
    engine.whenReady.then(() => {
    engine.createJSModel('SystemStats', systemStats);
    engine.synchronizeModels();
    created = true;
    intervalId = window.setInterval(() => {
    SystemStats.fps = Math.round(Math.random() * 100);
    SystemStats.ping = Math.round(Math.random() * 100);
    engine.updateWholeModel(SystemStats);
    engine.synchronizeModels();
    }, 1000);
    });
    return () => {
    if (intervalId !== undefined) window.clearInterval(intervalId);
    if (created) engine.unregisterModel(SystemStats);
    };
    }, []);
  5. Replace the JSX interpolation with data-bind-value on the same elements. JSX leaves double curly braces untouched inside a quoted attribute, so the expression reaches the engine exactly as written:

    src/App.tsx
    <footer className={styles['app__footer']}>
    <span className={styles['app__stat']}>FPS: {systemStats.fps}</span>
    <span className={styles['app__stat']}>PING: {systemStats.ping}ms</span>
    <span className={styles['app__stat']}>
    FPS: <span data-bind-value="{{SystemStats.fps}}"></span>
    </span>
    <span className={styles['app__stat']}>
    PING: <span data-bind-value="{{SystemStats.ping}}"></span>ms
    </span>
    </footer>

Run npm run dev and open the Player. The counters now change every second, driven by the model rather than by React.

Build the production bundle when you are ready to ship:

Terminal window
npm run build

Load the generated dist/index.html in the Gameface Player. The template sets base: './', so every asset resolves relative to that file and the build runs from any location.

React 19 works in the Player, with one cosmetic quirk worth knowing about up front.