For game developers
How web games run in Rare OS
The sandboxed window each web game gets in Rare OS, the injected Rare Wallet, controller input modes, per-game profiles and the on-screen keyboard.
Rare OS treats a browser game like an installed game. It gets its own fullscreen window, its own storage, controller support, an on-screen keyboard and Rare Wallet, without any change to your code. This page explains what your page sees in that window and how to make the most of it.
The game window
Each web game opens in its own webview window. On Rare OS the engine is WebKitGTK, so a page that works in Safari is the closest match.
Rare's runtime is injected before your page's own scripts run, so the wallet and the controller are ready by the time your code loads.
Chrome fallback
A game that needs something WebKitGTK lacks (WebGPU, some codecs, Chrome-only APIs) can be opened in a Chromium browser instead, as a fullscreen app window with its own profile per game. Every web game's page in the launcher also has Open in Google Chrome. In that mode the game runs in the browser as is: Rare's injected runtime (wallet, controller mapping, keyboard) is not part of it. No listed game needs the fallback today.
Rare Wallet
Rare Wallet registers itself in every game window as a Wallet Standard wallet. Any Sui wallet adapter that discovers Wallet Standard wallets (dapp-kit and others) lists it next to the wallets it already knows.
Features
Each account carries address, publicKey, label, chains: ["sui:mainnet"] and the five sui:* features above.
The wallet works on Sui mainnet only.
Connecting and signing
The game asks to connect
Rare shows a connect sheet with your game's title and its origin. The player picks one of their Rare Wallet accounts and allows or denies. An allowed origin is remembered on that device.
The game asks for a signature
Only an origin the player connected may ask for signatures. Every transaction and message opens Rare's review sheet: the transaction is simulated first, and the sheet shows what leaves the wallet, what arrives and the fee. Nothing is signed until the player approves with their PIN, passkey or hardware wallet.
The game gets the result
The promise resolves with the wallet's result, or rejects with an
Errorwhose message says why.
While a sheet is open the controller drives the sheet, and focus goes back to the game when it closes.
Transactions can be passed as a Transaction (serialized with its toJSON()) or as base64 transaction bytes. If your transaction sets a sender, it must be the connected account.
Error messages your game can receive:
Call connect from a button press rather than on page load, so the player sees the connect sheet when they expect it.
Connect with dapp-kit
With dapp-kit's providers set up for mainnet, Rare Wallet shows up in ConnectButton's list. Signing works through the usual hooks:
import { ConnectButton, useCurrentAccount, useSignAndExecuteTransaction } from "@mysten/dapp-kit";
import { Transaction } from "@mysten/sui/transactions";
export function Claim() {
const account = useCurrentAccount();
const { mutateAsync: signAndExecute } = useSignAndExecuteTransaction();
async function claim() {
const tx = new Transaction();
tx.moveCall({ target: "0xYOUR_PACKAGE::game::claim_reward", arguments: [] });
const { digest } = await signAndExecute({ transaction: tx });
console.log("claimed", digest);
}
return account ? <button onClick={claim}>Claim reward</button> : <ConnectButton />;
}To list Rare Wallet first, pass preferredWallets={["Rare Wallet"]} to WalletProvider.
Connect with Wallet Standard directly
import { getWallets } from "@wallet-standard/app";
const wallets = getWallets();
const findRare = () => wallets.get().find((w) => w.name === "Rare Wallet");
let rare = findRare();
wallets.on("register", () => {
rare ??= findRare();
});
export async function connectAndSign() {
if (!rare) throw new Error("Rare Wallet is not available in this browser");
const { accounts } = await (rare.features["standard:connect"] as any).connect();
const account = accounts[0];
const { bytes, signature } = await (rare.features["sui:signPersonalMessage"] as any).signPersonalMessage({
account,
message: new TextEncoder().encode("Sign in to My Game"),
});
return { address: account.address, bytes, signature };
}Checking for a wallet named Rare Wallet is also how your page can tell it is running in Rare OS.
Controller input
Rare reads the controller natively and hands it to your page in one of three input modes. Real controllers are always hidden from the page, so input never arrives twice. The mode comes from your game's profile (see below); without a profile the game gets keys.
keys (default)
Buttons become keyboard events and the right stick moves an on-screen pointer.
- The right stick moves a pointer that sends
pointermoveandmousemoveevents, with a finer response near the centre. RB (or a right-stick click) clicks withpointerdown,mousedown,pointerup,mouseupandclick. When the pointer is on, RB is the click and does not send its key. - With pointer lock, the stick sends
movementXandmovementYinstead of moving the cursor. - The pointer hides after 4 seconds without movement.
- Key events go to the focused element, or to the first
<canvas>when nothing is focused, and bubble up. They carrykey,code,keyCodeandwhich.
gamepad
For games that read the Gamepad API. Rare exposes one virtual controller and sends no keys.
navigator.getGamepads()returns the virtual pad at index 0, after agamepadconnectedevent.mappingis"standard", with 17 buttons and 4 axes (left x, left y, right x, right y; down is positive). Triggers are analog.- Button order: 0 A, 1 B, 2 X, 3 Y, 4 LB, 5 RB, 6 LT, 7 RT, 8 View, 9 Menu, 10 L3, 11 R3, 12 to 15 d-pad up, down, left, right, 16 Guide.
idisRare Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e). There is no vibration actuator.
Poll getGamepads() every frame, as you would for any standard pad.
focus
For games built from DOM controls: menus, card games, word games. The d-pad moves a focus ring over your page's own buttons, links and inputs, and A presses the one under the ring.
- The ring moves to the nearest usable control in the pressed direction. When there is none, up and down scroll the page.
- It skips controls that are hidden, disabled,
aria-hidden, nearly transparent or covered by something else (a modal above them). - The ring starts on the profile's
focusStartselector, or else on the largest control near the centre of the screen, which is usually your Play button. - B sends Escape and Menu sends Enter, unless the profile maps them to other keys.
Use real <button>, <a href>, role="button" or tabindex elements for anything the player should reach.
Writing input code that works everywhere
- Rare's events are synthetic, so
event.isTrustedisfalse. Don't ignore untrusted events. - Listen on
windowordocumentas well as on your canvas: events bubble from the focused element. - Don't depend on a native default action from a synthetic key. An Enter key event does not submit a form by itself, so handle Enter in a
keydownlistener. - The device's touchscreen sends normal touch events.
Per-game profiles
A profile tells Rare where your game really plays and how the controller maps onto it. Profiles ship with Rare Launcher in launcher/src/games/profiles.json, keyed by your catalog slug (see Get your game listed). A game the player adds by URL picks up a profile through its hosts.
Example profile for a top-down web game played with WASD and the mouse:
{
"games": {
"my-game": {
"playUrl": "https://play.mygame.example/",
"hosts": ["play.mygame.example", "mygame.example"],
"input": "keys",
"pointer": true,
"keys": { "up": "w", "down": "s", "left": "a", "right": "d", "x": "f" },
"controls": "Left stick walks, right stick moves the pointer, RB clicks, X interacts."
}
}
}And one for a word game played with its own on-screen letters:
{
"games": {
"suiordle": {
"playUrl": "https://suiordle.com/",
"hosts": ["suiordle.com", "www.suiordle.com"],
"input": "focus",
"focusStart": ["button[data-key=\"Q\"]"],
"keys": { "x": "Backspace", "start": "Enter", "b": "Escape" },
"controls": "D-pad picks a letter on the game's keyboard, A types it. X deletes, Menu enters the word. View opens Rare's keyboard to type faster."
}
}
}Profiles are maintained by Rare. Tell us the mode and mapping your game needs when you get listed.
On-screen keyboard
Rare draws its own keyboard at the bottom of your page. It has a number row, a QWERTY layout with @ . - _, and Shift, Space, Del and Done keys. A types, X deletes, Y toggles Shift, Menu finishes and B closes it.
When it opens
- A is pressed while a visible text field is focused (keys and gamepad modes).
- A is pressed on a text field under the focus ring (focus mode).
- The pointer clicks a text field, or the player taps one on the touchscreen.
- View is pressed. In focus and gamepad modes View always opens it; in keys mode only when a text field is focused, or when the profile sets
"keyboard": "keys".
A text field is an <input> (not a checkbox, radio, button, range, color, file, hidden, image or reset input), a <textarea> or a contenteditable element. It counts as visible when it is at least 40 by 14 pixels, not visibility: hidden and not nearly transparent. Hidden inputs that games keep focused to capture keys are ignored.
What it sends
- Into a field: each key sets the field's value through the native value setter and fires an
inputevent, so controlled inputs in React and other frameworks update. In acontenteditableelement it inserts text. Done closes the keyboard and sends an Enter key press to the field. - With no field: it sends
keydownandkeyupfor each key to the focused element or the page: letters, digits, Space, Backspace for Del and Enter for Done. This is how word games that listen to the keyboard get typed letters.
Make your text inputs work with it
- Use real
<input>or<textarea>elements, visible and at least 40 by 14 pixels. - Read the value on
inputevents, not only onkeydown. Typing into a field firesinputevents, not key events. - Submit on Enter from a
keydownlistener on the field, since the keyboard's Enter is a synthetic key press. - If your game reads typed letters from key events with no field, use focus mode or ask for
"keyboard": "keys"so View opens the keyboard as a key sender.
