open regard. Module 01 / Display / Runnable examples

03 / React + vanilla

Two bindings.
One controller.

The React widget in this panel shares the controller that was started before paint. Change it here or in the vanilla control below; both update together.

Use Display inside a React app

Optional binding · Shared state · Site-owned CSS

React binding

Subscribe, then render.

DisplayWidget uses the same native markup and public CSS hooks as the vanilla widget. Pass className to scope your design; keep the controller stable between renders.

<DisplayWidget
  display={regard}
  className="demo-widget"
/>

Vanilla binding

Same profile. No bridge.

This control is mounted directly into the DOM. It shares the existing controller with React.

A shared configuration

Presets work in both bindings.

Composition happens in config.js, outside React. Vision adds text-spacing tokens; Vision + High Contrast adds paired colors and borders. Both widgets inherit the page’s new presentation.

Source code

These are the files behind this example. The tabs support arrow keys, Home and End.

react-demo.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { DisplayWidget } from "openregard/display/react";
import type { DisplayController } from "openregard/display";
const regard = (window as unknown as { regard: DisplayController }).regard;
createRoot(document.getElementById("react-controls")!).render(
  <StrictMode>
    <DisplayWidget display={regard} className="demo-widget" />
  </StrictMode>,
);
(
  window as unknown as {
    OpenRegardDisplay: {
      mountDisplayWidget: (
        el: HTMLElement,
        r: DisplayController,
        options?: { className: string },
      ) => void;
    };
  }
).OpenRegardDisplay.mountDisplayWidget(
  document.getElementById("controls")!,
  regard,
  {
    className: "demo-widget",
  },
);
react.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>React binding — Open Regard Display</title>
    <!-- Self-host synchronous scripts so the saved profile applies before paint. -->
    <script src="../dist/display.browser.js"></script>
    <script src="../dist/presets.browser.js"></script>
    <script src="./config.js"></script>
    <link rel="stylesheet" href="../dist/style.css" />
    <link rel="stylesheet" href="./demo.css" />
    <link rel="icon" href="./mark.svg" type="image/svg+xml" />
  </head>
  <body>
    <a class="skip" href="#main">Skip to content</a>
    <header class="masthead shell">
      <a class="brand" href="https://openregard.org/"
        ><img src="./mark.svg" alt="" />open regard.</a
      >
      <span class="meta">Module 01 / Display / Runnable examples</span>
    </header>
    <nav class="example-nav shell" aria-label="Example navigation">
      <a href="./">Vanilla HTML</a><a href="./docs.html">Static docs</a
      ><a href="./react.html" aria-current="page">React</a
      ><a href="https://openregard.org/docs/">Library documentation ↗</a>
    </nav>
    <main id="main" class="shell">
      <section class="hero">
        <div>
          <p class="eyebrow">03 / React + vanilla</p>
          <h1>Two bindings.<br /><em>One controller.</em></h1>
          <p class="intro">
            The React widget in this panel shares the controller that was
            started before paint. Change it here or in the vanilla control
            below; both update together.
          </p>
        </div>
        <aside class="control-card" aria-label="Display settings">
          <p class="eyebrow">Try a display profile</p>
          <div id="react-controls"></div>
        </aside>
      </section>

      <div class="section-head">
        <h2>Use Display inside a React app</h2>
        <p>Optional binding · Shared state · Site-owned CSS</p>
      </div>
      <div class="grid">
        <article class="card">
          <p class="eyebrow">React binding</p>
          <h2>Subscribe, then render.</h2>
          <p>
            <code>DisplayWidget</code> uses the same native markup and public
            CSS hooks as the vanilla widget. Pass <code>className</code> to
            scope your design; keep the controller stable between renders.
          </p>
          <pre class="code-note"><code>&lt;DisplayWidget
  display={regard}
  className="demo-widget"
/&gt;</code></pre>
        </article>
        <article class="card">
          <p class="eyebrow">Vanilla binding</p>
          <h2>Same profile. No bridge.</h2>
          <p>
            This control is mounted directly into the DOM. It shares the
            existing controller with React.
          </p>
          <div id="controls"></div>
        </article>
      </div>
      <section class="sync-row">
        <div>
          <p class="eyebrow">A shared configuration</p>
          <h2>Presets work in both bindings.</h2>
        </div>
        <p>
          Composition happens in <strong>config.js</strong>, outside React.
          Vision adds text-spacing tokens; Vision + High Contrast adds paired
          colors and borders. Both widgets inherit the page’s new presentation.
        </p>
      </section>

      
    </main>
    <footer class="shell">
      <p>
        Open Regard / Display. Profiles and page styles are configured by the
        site. Test your integration with your content and users.
      </p>
      <a href="https://github.com/inferal-oss/openregard">View the project ↗</a>
    </footer>
    <script src="../dist/react-demo.js" defer></script>
  </body>
</html>
config.js
// Optional token recipes. Profile names, composition and site styling are yours.
const { wcagPresets, htmlPresets } = OpenRegardPresets;
window.regardConfig = {
  version: 1,
  defaultProfile: "standard",
  storageKey: "openregard-example",
  controlLabel: "Reading and display preferences",
  profiles: [
    {
      id: "standard",
      label: "Standard",
      description: "The original site presentation.",
    },
    {
      id: "roomy",
      label: "Vision",
      description: "Larger text with the WCAG text-spacing override values.",
      tokens: {
        ...wcagPresets.textSpacing,
        ...htmlPresets.nativeControls,
        "--regard-font-size": "125%",
      },
    },
    {
      id: "vision-contrast",
      label: "Vision + High Contrast",
      description:
        "Larger, spaced text with a restrained palette and stronger borders.",
      tokens: {
        ...wcagPresets.textSpacing,
        ...wcagPresets.contrastLight,
        ...htmlPresets.nativeControls,
        "--regard-font-size": "125%",
      },
    },
    {
      id: "strong",
      label: "Max Contrast",
      description:
        "Larger text, generous spacing and black-and-white surfaces.",
      tokens: {
        ...wcagPresets.textSpacing,
        ...htmlPresets.nativeControls,
        "--regard-font-size": "150%",
        "--regard-page-ink": "#000",
        "--regard-page-paper": "#fff",
        "--regard-panel-ink": "#fff",
        "--regard-panel-paper": "#000",
        "--regard-border-color": "#000",
      },
    },
  ],
};
window.regard = OpenRegardDisplay.createDisplay(window.regardConfig);
window.stopRegard = window.regard.start();
demo.css
:root {
  font-size: var(--regard-font-size, 100%);
  --ink: var(--regard-page-ink, #203b32);
  --paper: var(--regard-page-paper, #f4f5ee);
  --muted: var(--regard-page-ink, #52645b);
  --line: var(--regard-border-color, #c5cec0);
  --panel-paper: var(--regard-panel-paper, #203b32);
  --panel-ink: var(--regard-panel-ink, #f4f7ee);
  --surface: var(--regard-page-paper, #fffef8);
  --accent: var(--regard-page-paper, #d8ebbb);
  color: var(--ink);
  background: var(--paper);
  font-family:
    ui-sans-serif,
    system-ui,
    -apple-system,
    sans-serif;
  line-height: var(--regard-line-height, 1.65);
  letter-spacing: var(--regard-letter-spacing, normal);
  word-spacing: var(--regard-word-spacing, normal);
}
* {
  box-sizing: border-box;
}
body {
  margin: 0;
  overflow-wrap: anywhere;
}
.shell {
  width: min(100% - 3rem, 76rem);
  margin-inline: auto;
}
a {
  color: inherit;
  text-underline-offset: 0.25em;
}
:focus-visible {
  outline: 3px solid currentColor;
  outline-offset: 4px;
}
.skip {
  position: absolute;
  transform: translateY(-150%);
  background: Canvas;
  color: CanvasText;
  padding: 0.7em;
  z-index: 5;
}
.skip:focus {
  transform: none;
}
.masthead {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  justify-content: space-between;
  align-items: center;
  padding-block: 1.5rem;
}
.brand {
  display: inline-flex;
  gap: 0.65em;
  align-items: center;
  font-weight: 700;
  font-size: 1.3rem;
  letter-spacing: -0.04em;
  text-decoration: none;
}
.brand img {
  width: 2rem;
  height: 2rem;
}
.masthead .meta {
  font-size: 0.75rem;
  color: var(--muted);
}
.example-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1.5rem;
  padding-block: 0.8rem;
  border-block: 1px solid var(--line);
  font-size: 0.85rem;
}
.example-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75em;
  text-decoration: none;
}
.example-nav [aria-current] {
  font-weight: 700;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.55em;
}
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 5rem);
  padding-block: 3.5rem;
  align-items: start;
}
.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.7rem;
  font-weight: 700;
}
h1 {
  font-size: clamp(2.4rem, 5vw, 4.4rem);
  line-height: 1.1;
  letter-spacing: -0.055em;
  margin: 0.8rem 0 1.5rem;
  font-weight: 600;
}
h1 em {
  font-family: Georgia, serif;
  font-weight: 400;
}
h2 {
  font-size: clamp(1.5rem, 3vw, 2.2rem);
  line-height: 1.2;
  letter-spacing: -0.035em;
  font-weight: 600;
  margin-top: 0;
}
h3 {
  line-height: 1.3;
  font-size: 1.1rem;
}
p {
  margin-block: 0 1em;
  margin-block-end: var(--regard-paragraph-spacing, 1em);
}
.intro {
  font-size: 1.05rem;
  color: var(--muted);
  max-width: 39rem;
}
.control-card {
  padding: 1.5rem;
  border: 1px solid var(--line);
  border-radius: 0.8rem;
  background: var(--surface);
  box-shadow: 0 8px 0 var(--accent);
  min-width: 0;
}
.control-card .eyebrow {
  margin-bottom: 1rem;
}
/* The same public styling hooks work on React and vanilla widgets. */
.demo-widget {
  --openregard-control-radius: 0.4rem;
  --openregard-control-padding: 0.75em;
  --openregard-gap: 0.6rem;
  --openregard-focus-color: var(--ink);
}
.demo-widget .openregard-label {
  flex: 1;
  font-size: 0.8rem;
  font-weight: 600;
}
.demo-widget .openregard-select {
  width: 100%;
}
.demo-widget .openregard-description {
  font-size: 0.78rem;
  margin-top: 0.8rem;
  color: var(--muted);
}
.section-head {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  margin-block: 1rem 1.5rem;
}
.section-head h2 {
  margin: 0;
}
.section-head p {
  font-size: 0.8rem;
  color: var(--muted);
  margin: 0;
}
.grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.25rem;
}
.card {
  border: 1px solid var(--line);
  border-radius: 0.7rem;
  padding: clamp(1.1rem, 3vw, 2rem);
  background: var(--surface);
  min-width: 0;
}
.card .eyebrow {
  margin-bottom: 1.4rem;
}
.dark {
  background: var(--panel-paper);
  color: var(--panel-ink);
  border-color: currentColor;
}
.dark p {
  color: inherit;
}
.large-number {
  font-family: Georgia, serif;
  font-size: 3.5rem;
  line-height: 1.2;
}
.tag {
  display: inline-block;
  border: 1px solid currentColor;
  border-radius: 2rem;
  padding: 0.2em 0.7em;
  font-size: 0.7rem;
}
label:not(.openregard) {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
}
input,
button {
  box-sizing: border-box;
  font: inherit;
  max-width: 100%;
  min-width: 0;
}
input {
  display: block;
  width: 100%;
  margin-top: 0.5em;
  padding: 0.75em;
  background: var(--paper);
  color: var(--ink);
  border: 1px solid var(--ink);
  border-radius: 0.35rem;
}
button {
  cursor: pointer;
  padding: 0.65em 0.9em;
  background: Canvas;
  color: CanvasText;
  border: 1px solid currentColor;
  border-radius: 0.35rem;
}
.sync-row {
  margin-block: 1.5rem 3rem;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 2rem;
  align-items: center;
  padding: 2rem;
  background: var(--accent);
  border: 1px solid var(--line);
  border-radius: 0.7rem;
}
.sync-row p {
  font-size: 0.85rem;
}
.doc-layout {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: 3rem;
}
.doc-index {
  border-top: 1px solid var(--line);
  padding-top: 1rem;
  font-size: 0.8rem;
}
.doc-index a {
  display: block;
  padding-block: 0.6em;
}
.doc-article {
  min-width: 0;
  max-width: 48rem;
}
.doc-article section {
  padding-block: 1rem 2rem;
}
.steps {
  padding-inline-start: 1.5rem;
}
.steps li {
  padding-inline-start: 0.4em;
  margin-block: 1em;
}
code {
  font-size: 0.88em;
  overflow-wrap: anywhere;
}
.code-note {
  padding: 1.5rem;
  background: var(--panel-paper);
  color: var(--panel-ink);
  border-radius: 0.5rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: 1.7;
}
.recipe-note {
  border-left: 3px solid currentColor;
  padding-inline-start: 1rem;
  font-size: 0.85rem;
}
footer {
  border-top: 1px solid var(--line);
  padding-block: 2rem;
  margin-top: 3rem;
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  justify-content: space-between;
  font-size: 0.8rem;
  color: var(--muted);
}
footer p {
  margin: 0;
  max-width: 40rem;
}
/* Spacing overrides also reach typography with authored heading/tracking values. */
:root:not([data-regard-profile="standard"])
  :is(h1, h2, h3, p, a, label, span, select, button, li) {
  line-height: var(--regard-line-height, 1.65);
  letter-spacing: var(--regard-letter-spacing, normal);
  word-spacing: var(--regard-word-spacing, normal);
}
@media (max-width: 850px) {
  .hero,
  .doc-layout {
    grid-template-columns: 1fr;
    gap: 2rem;
  }
  .hero {
    padding-block: 2rem 3rem;
  }
  .doc-index {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1rem;
  }
}
@media (max-width: 600px) {
  .shell {
    width: calc(100% - 2rem);
  }
  .grid,
  .sync-row {
    grid-template-columns: 1fr;
  }
  .sync-row {
    padding: 1.25rem;
    gap: 1rem;
  }
  .masthead {
    padding-block: 1rem;
  }
}
@media (forced-colors: active) {
  .dark,
  .code-note {
    background: Canvas;
    color: CanvasText;
  }
  .control-card {
    box-shadow: none;
  }
}