React
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.
Install the template
Section titled “Install the template”We scaffold the project with create-gameface-app , passing the react template so the CLI skips the framework prompt.
-
Scaffold the project
Section titled “Scaffold the project”Run the initializer and let it install the dependencies for you:
Terminal window npm create gameface-app -- --template react --defaultThe CLI asks you to name your project, then scaffolds it and installs the dependencies.
-
Start the development server
Section titled “Start the development server”Move into the new folder and start Vite:
Terminal window cd your-project-namenpm run devThe dev server runs on port 3000.
-
Open it in the Player
Section titled “Open it in the Player”Point the Gameface Player at
http://localhost:3000/. The sample UI loads, and Hot Module Replacement updates it every time you save a file.
What’s included
Section titled “What’s included”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
Achievementspanel in the left column. - A
PlayerStatspanel in the top right. - An interactive
CarouselMenualong the bottom center. - A
START GAMEaction 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.
Styling
Section titled “Styling”The template keeps styles flat and cache-friendly, which is what Gameface needs to hold a high frame rate. It layers a few approaches:
| Layer | Approach |
|---|---|
| Component layout | SCSS modules, one .module.scss per component |
| Utilities | UnoCSS with presetGameface() |
| Inline styles | Kept minimal, for genuinely dynamic values like a progress bar width. Static values become gf-prop--* classes through vite-plugin-gameface-styles plugin |
| Responsive sizing | rem 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.
Scripts
Section titled “Scripts”The template ships with the standard set of scripts:
| Command | Description |
|---|---|
npm run dev | Start the dev server on port 3000 |
npm run build | Typecheck and build the production bundle to dist/ |
npm run lint | Run ESLint with eslint-plugin-gameface |
npm run preview | Preview the production build locally |
Data-binding and mocked models
Section titled “Data-binding and mocked models”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.
-
Add the cohtml library
Section titled “Add the cohtml library”Data-binding needs the
engineglobal, which comes fromcohtml.js. Copycohtml.jsandcohtml.d.tsout of your Gameface package (Player/Samples/uiresources/library) intosrc/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'; -
Describe the model
Section titled “Describe the model”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-gamefacereads this directory and checks every binding expression against it. A typo such as{{SystemStats.fpsx}}failsnpm run lintinstead of silently rendering nothing. -
Declare the global
Section titled “Declare the global”engine.createJSModelexposes 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;} -
Create the model
Section titled “Create the model”Register the model once the engine is ready. The template already exports a
systemStatsobject, so pass it straight in. The interval stands in for the game pushing new values, and every change needsupdateWholeModelfollowed bysynchronizeModelsto 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);};}, []); -
Bind the markup
Section titled “Bind the markup”Replace the JSX interpolation with
data-bind-valueon 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 for production
Section titled “Build for production”Build the production bundle when you are ready to ship:
npm run buildLoad 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 and Gameface notes
Section titled “React and Gameface notes”React 19 works in the Player, with one cosmetic quirk worth knowing about up front.
© 2026 Coherent Labs. All rights reserved.