Keyboard & text input

Rive files that take focus, answer the arrow keys and accept typing.

What it is

State machine listeners aren't only for the pointer. A Rive file can mark elements as focusable, react to key presses, and contain real text input fields with a caret, selection and copy and paste, all designed in the editor. The app makes the canvas focusable with tabIndex; Rive moves focus between its own elements. The newer, experimental semantics mode mirrors those elements as hidden DOM nodes, so screen readers and the Tab key reach inside the canvas too.

When to use it

  • Interactive Rive UI that must work without a mouse
  • Animated forms and sign-in screens
  • Game-like menus driven by the arrow keys
Official docs ↗

1. Focus moved by the arrow keys

Loading Rive file…

Click the grid or Tab into it, then use the arrow keys to move between the buttons; Enter presses one.

2. Keyboard and text listeners

Loading Rive file…

Click a box and press a. "Both" has a key listener (counts presses of A) and a text listener (counts characters typed); "KeysOnly" has just the key listener. No app code counts anything: it all happens in the file.

3. A sign-in form with semantics

Loading Rive file…

Click the email field and type; Tab moves to the password field (obscured) and then out of the canvas. With semantics on, the fields also exist as hidden inputs, so a screen reader announces them and the browser's Tab order includes them.

All three files are MIT-licensed test files from Rive's React runtime repository (see public/rive/CREDITS.md). Rive's semantics support is marked experimental, so its API may still change.

Show code · src/app/rive-input/RiveInput.tsx
"use client";

import "@/lib/rive-runtime";
import { Fit, Layout, SemanticMode, useRive } from "@rive-app/react-canvas";
import { useState } from "react";
import { Stage } from "@/components/ui";

// A Rive file that handles keyboard and text input itself. The file defines
// focusable elements, keyboard listeners and text fields; the app's part is
// small: give the canvas a tabIndex so it can take focus, and (optionally)
// turn on semantics so assistive technology can see what's inside.
export function RiveInput({
  src, artboard, label, semantics = false, className = "h-90",
}: {
  src: string; artboard?: string; label: string; semantics?: boolean; className?: string;
}) {
  const [focused, setFocused] = useState(false);
  const { RiveComponent } = useRive(
    {
      src,
      artboard,
      stateMachine: "State Machine 1",
      autoplay: true,
      // Binds the file's default view model, which these files use to show
      // what was typed and how many keys were pressed.
      autoBind: true,
      // Lets the canvas be focused with Tab or a click; Rive then routes keys
      // to the focused element inside the file.
      tabIndex: 0,
      // Fonts are embedded in these files; never fetch assets from Rive's CDN
      // (the CSP would block it anyway).
      enableRiveAssetCDN: false,
      // Experimental: mirrors the file's buttons and text fields as hidden
      // DOM elements, so screen readers and the browser's Tab order see them.
      ...(semantics ? { semanticsMode: SemanticMode.Enabled } : {}),
      layout: new Layout({ fit: Fit.Contain }),
    },
    { shouldUseIntersectionObserver: true },
  );

  return (
    // position: relative on the container is what the semantics overlay needs.
    <Stage className={className} onFocus={() => setFocused(true)} onBlur={() => setFocused(false)}>
      <RiveComponent aria-label={label} className="size-full" />
      <span className={`pointer-events-none absolute top-2 right-2 rounded-full px-2 py-0.5 text-xs ${focused ? "bg-accent text-white" : "bg-code text-muted"}`}>
        {focused ? "focused: keys go to Rive" : "click or Tab in"}
      </span>
    </Stage>
  );
}