---
title: "Game UI in HTML and CSS in a C# game"
url: https://recursiveai.org/onegame/cookbook/html-page-screen/
description: "Write a C# game’s screen in HTML and CSS: a title page drawn over the game, a button that sends a command, a test that clicks it by id, and a screenshot reference. Runnable, with onegame."
version: "1.15.2"
---

# Game UI in HTML and CSS in a C# game

Add `RecursiveAI.OneGame.Pages` and show a page with `game.Pages.Show(name, PageScreen.FromHtml(html, options))`: OneRender lays out, styles, animates and hit-tests the HTML and CSS, onegame draws it over the game, and a button’s click handler sends a command. Pages are view code, so showing one changes no game state. The program below is a lantern-lighting title card.

```shell
dotnet tool install -g RecursiveAI.OneGame.Cli --version 1.15.2
onegame new lantern && cd lantern
rm -r art scenarios/*
dotnet add package RecursiveAI.OneGame.Pages --version 1.15.2
```

You need the .NET 10 SDK. Replace `Game.cs` with:

```csharp
using System.IO;
using OneGame.Core;
using OneGame.Host;
using OneGame.Pages;
using OneGame.UI;
using OneRender.Interaction;

namespace Lantern;

[Entity]
public partial class Lamps : Entity
{
    [Synced] public partial int Lit { get; private set; }

    [Command]
    public void Light() => Lit += 1;
}

public sealed class LanternGame : GameDefinition
{
    // Real HTML and CSS: a grid, a gradient, a shadow, a transition and :hover.
    private const string Html = """
        <body style="margin:0;font:18px Body;color:#f4ead8">
          <style>
            .card { position:absolute; left:40px; top:60px; width:360px; padding:24px; border-radius:14px;
                    background:linear-gradient(160deg,#3a2a1c,#1c1612); box-shadow:0 10px 30px rgba(0,0,0,.5);
                    display:grid; grid-template-columns:1fr auto; gap:12px 20px; align-items:center }
            h1 { grid-column:1 / 3; margin:0; font-size:30px; color:#ffcf70 }
            p { margin:0; color:#d8c8b0 }
            button { width:110px; height:40px; border:0; border-radius:8px; background:#b5651d; color:#fff; font:18px Body;
                     transition:background-color 150ms ease-out }
            button:hover { background:#d97d2a }
          </style>
          <div class="card">
            <h1>Lantern</h1>
            <p>Light one for every traveler.</p>
            <button id="light">Light</button>
          </div>
        </body>
        """;

    public override string Name => "lantern";

    public override void RegisterEntities(EntityRegistry registry) =>
        OneGame.Generated.Lantern.EntityTypes.Register(registry);

    public override void Setup(World world) => world.Add(new Lamps());

    public override void DefineScreens(ScreenSet screens) => screens.Add("main", game =>
    {
        var fonts = new PageFonts(game.Fonts);
        fonts.Add(File.ReadAllBytes("fonts/NotoSans-Regular.ttf"), "Body");   // the page says: font: 18px Body

        // Show replaces a page of the same name, so a rebuilt screen simply remakes it.
        game.Pages.Show("title", PageScreen.FromHtml(Html, new PageOptions(fonts)
        {
            // A DOM listener is view code: it sends a command and never writes state.
            Setup = s => s.Interactive!.Events.AddEventListener(
                EventTarget.Of(s.Interactive.FindElement("light")), "click", _ => LampsOf(game).Send.Light()),
        }));

        // The screen's OneGame.UI side, drawn with the page: a label in the corner.
        return Ui.Label(() => $"{LampsOf(game).Lit} lit").Named("count").Padding(Length.Px(8)).TextColor(Color.White);
    });

    private static Lamps LampsOf(GameInstance game) => (Lamps)game.World.Entities[0];
}
```

- **`game.Pages.Show(name, PageScreen.FromHtml(html, options))`** shows a page. Sizes are CSS pixels at the UI scale, and the page is transparent wherever it has no background.
- **`PageFonts`** gives the page the faces the game’s text draws with. The font name in the CSS is the one you register.
- **`Setup`** runs every time the page is made, including after a window resize or a UI scale change. The page is made again then, because an HTML page can’t resize in place.
- The click handler is checked like any view. Call `LampsOf(game).Light()` instead of `Send.Light()` and the build fails:

```text
…/lantern/Game.cs(59,83): error OG0008: View code cannot change simulation state: 'Lamps.Light' changes state. Views read state and send commands; use entity.Send.<Command>(...) instead.
```

## Click it from a scenario

`click:`, `ui:` and the other UI steps look in the `OneGame.UI` tree first, then in the shown pages, by name: an element’s `id`, a form control’s `name`, or `#name`. A click sends real pointer events at the element’s center.

Save this as `scenarios/light.yaml`:

```yaml
name: light
description: The page's Light button, found by its HTML id, sends a command; the frame is kept as a reference.
window: 640x360
steps:
  - expect: { ui: { id: light, text: Light } }
  - click: light
  - click: light
  - step: 1
  - tweens: settle
expect:
  - state: { entity: Lamps, property: Lit, equals: 2 }
  - ui: { id: count, text: 2 lit }
  - capture: { reference: refs/light.png, tolerance: 2, max-diff: 0.001 }
```

And the same two commands without the page:

Save this as `scenarios/light-by-command.yaml`:

```yaml
name: light-by-command
description: The same two commands sent directly, with no click.
steps:
  - command: { entity: Lamps, name: Light }
  - command: { entity: Lamps, name: Light }
  - step: 1
expect:
  - state: { entity: Lamps, property: Lit, equals: 2 }
```

```shell
onegame test --scenario scenarios/light.yaml --update-references
onegame test
```

```text
PASS light (579 ms, tick 1, hash 82f3ce76daebd03a)
created reference …/lantern/scenarios/refs/light.png
1 passed, 0 failed
PASS light-by-command (26 ms, tick 1, hash 82f3ce76daebd03a)
PASS light (574 ms, tick 1, hash 82f3ce76daebd03a)
2 passed, 0 failed
```

Both end on the same hash: the page sent the commands and changed nothing else. `refs/light.png` shows “2 lit” in the top-left corner and the card below it: “Lantern” in gold, the line of text wrapped in its grid column, and the orange Light button, on a dark gradient with rounded corners and a soft shadow. Look at a capture before you keep it as a reference.

## Limits and notes

- `File.ReadAllBytes("fonts/...")` reads from the working directory. `onegame run`, `onegame test` and the export’s `run.sh` start in the right folder; the font ships in the export’s `fonts/`. Started from another directory, the exported program stops with `error: Could not find a part of the path '/tmp/fonts/NotoSans-Regular.ttf'.` and exits 4.
- At 1.15.2 the game SDK already brings `OneGame.Pages`: the game built without the package reference too. The reference pins the version.
- Structure is fixed once a page is made. Handlers change text (`SetTextContent`), control state (`SetValue`, `SetChecked`) and classes, not the element tree. For a HUD that changes every frame, a builder page (`PageScreen.FromTree`) edits a `StyledTree` with no allocation.
- Focus, scroll position and form values are lost when a page is made again. Keep what it shows in your own state and have `Setup` apply it.
- A controller works on a focused page like a keyboard: up and down move between controls, and the south button presses.
- Pages ran on Linux (lavapipe) and in headless Chrome upstream. Metal, Windows, phones and real GPUs in a browser haven’t run them yet. Input-method text and clipboard paste don’t reach page fields.

More in [HTML pages](https://recursiveai.org/onegame/docs/pages/). For screens built from the component kit, see [A game UI screen](https://recursiveai.org/onegame/cookbook/game-ui-screen/).
