react-insight-overlay
v1.0.0
Published
A lightweight React utility for opt-in contextual explanation overlays
Maintainers
Readme
react-insight-overlay
Add on-demand explanations to any element in your React app — one attribute, zero wrappers, no performance cost when off.
RIO (React Insight Overlay) lets you embed developer-authored context directly into your UI. Toggle insight mode with Alt+I, hover any annotated element, and a smart tooltip appears with rich JSX content, auto-positioned and portal-rendered above everything.
npm install react-insight-overlayWhy RIO
- Attribute-based. Annotate any element with
data-rioand you're done. No component imports, no prop drilling, no refactors. - Zero cost when off. When insight mode is disabled, RIO registers zero listeners and renders no DOM. Your app is completely unaffected.
- Portal above everything. Overlay renders via
ReactDOM.createPortaltodocument.body. Works above AG Grid, Mantine modals, and any z-index stack. - Pure CSS theming. Override CSS variables to match your brand. No theme provider, no CSS-in-JS, no runtime cost.
- Smart tooltip placement. Automatically picks right → left → below → above based on viewport constraints. Supports
data-rio-placementhints. - Keyboard first.
Alt+Ito enter,Escto exit, focus-based navigation for accessible use. - Rich content. Tooltip content can be plain strings, JSX, or async functions that fetch docs on demand.
Quick start
1. Wrap your app
import { RioProvider } from 'react-insight-overlay'
import 'react-insight-overlay/rio.css'
export default function App() {
return (
<RioProvider>
<YourApp />
</RioProvider>
)
}2. Annotate elements
The simplest form — pass a string directly:
<button data-rio="Executes trade at current market price">
Buy
</button>Or reference a key in a contentMap for rich JSX:
<div data-rio="utilization" data-rio-placement="above" />3. Toggle it
Press Alt+I anywhere in your app. Hover any annotated element to see its explanation. Press Esc to exit.
Rich content
Pass a contentMap to RioProvider for rich JSX content, with full support for async loaders:
<RioProvider
contentMap={{
utilization: (
<div>
<strong>Utilization</strong> = capital deployed / total available
<p>Above 80% may limit new positions.</p>
</div>
),
riskScore: async () => {
const docs = await fetchRiskDocs()
return <MarkdownRenderer source={docs} />
},
}}
>
<YourApp />
</RioProvider>Co-locate content with the feature
Use useRioContent to register tooltip content directly from the component that owns it — no central map required:
import { useRioContent } from 'react-insight-overlay'
function PortfolioPanel() {
useRioContent({
'portfolio-total': (
<div>
<strong>Total portfolio value</strong>
<p>Sum of all holdings at current market price.</p>
</div>
),
})
return <div data-rio="portfolio-total">$84,210</div>
}Custom activation
Default is Alt+I, but you can rebind or use alt-click:
// Different hotkey
<RioProvider activation={{ type: 'hotkey', combo: { ctrl: true, key: '?' } }}>
// Hold Alt and click anywhere to toggle
<RioProvider activation={{ type: 'alt-click' }}>Programmatic toggle
Expose a button anywhere in your UI with the headless RioToggle:
import { RioToggle } from 'react-insight-overlay'
<RioToggle>
{({ enabled, toggle }) => (
<button onClick={toggle}>
{enabled ? 'Exit help' : '? Help'}
</button>
)}
</RioToggle>Or access the context directly:
import { useRio } from 'react-insight-overlay'
function MyComponent() {
const { enabled, toggle } = useRio()
// ...
}Theming
Override any CSS variable to match your product's identity:
:root {
--rio-accent: #d4af37;
--rio-tooltip-bg: #0d0900;
--rio-tooltip-fg: #fef3c7;
--rio-tooltip-border: rgba(212, 175, 55, 0.4);
--rio-mask: rgba(0, 0, 0, 0.5);
}Or use one of the built-in themes via overlayClassName:
<RioProvider overlayClassName="rio-theme-gold">
<RioProvider overlayClassName="rio-theme-copper">
<RioProvider overlayClassName="rio-theme-light">Available CSS variables
--rio-mask
--rio-accent
--rio-accent-glow
--rio-highlight-radius
--rio-highlight-outline
--rio-highlight-glow
--rio-tooltip-bg
--rio-tooltip-fg
--rio-tooltip-muted
--rio-tooltip-border
--rio-tooltip-shadow
--rio-tooltip-radius
--rio-tooltip-hint-color
--rio-font-size
--rio-arrow-sizeAPI
<RioProvider>
| Prop | Type | Default | Description |
| ------------------ | --------------------------------- | -------------------------------------- | -------------------------------------------------------- |
| children | ReactNode | — | Your app. |
| contentMap | RioContentMap | {} | Map of data-rio keys to JSX / async loaders. |
| activation | RioActivation | { type: 'hotkey', combo: Alt+I } | How insight mode is toggled. |
| dim | boolean | true | Dim the background when active. |
| overlayClassName | string | — | Class applied to the overlay root for custom theming. |
| portalTarget | HTMLElement | document.body | Where the overlay is portaled. |
data-rio attributes
| Attribute | Value | Description |
| --------------------- | --------------------------------- | ----------------------------------------------------------------- |
| data-rio | string key or inline text | Lookup key into contentMap, or fallback inline text. |
| data-rio-placement | right \| left \| above \| below | Optional placement hint. Smart placement overrides if off-screen. |
Hooks
useRio()— returns{ enabled, enable, disable, toggle }.useRioContent(map)— register tooltip content from a component, auto-cleaned on unmount.
Components
<RioToggle>— headless render-prop toggle.
Do's and don'ts
Do
- Annotate the things users actually ask about. Interactive controls, metrics, and dense data displays earn their keep. Labels, headings, and decorative chrome usually don't.
- Answer "what is this and why does it exist?" — not how to click it. UI affordances should already cover the how.
- Co-locate content with the component that owns it. Reach for
useRioContentfirst; reservecontentMapfor page-level chrome (nav, headers, layout shells). - Nest
RioProviders for self-contained sub-apps or demos. Each one keeps its own scope, and a parent's toggle cascades down — so a single page-level "show insight" button lights everything up at once. - Set
data-rio-placementnear viewport edges. It's a hint, not a hard rule — smart placement still fires if your hint won't fit. - Gate it on a flag if RIO is only meant for staff, dev, or beta audiences. Tree-shake the import too if you want zero bytes shipped to the wrong build.
Don't
- Don't use RIO for tours, onboarding, or marketing pop-ups. It's an opt-in inspector for power users — not always-on UX. If your tooltip needs to fire without user intent, you want a different tool.
- Don't blanket-annotate. Sparse, deliberate annotations are how RIO stays useful; a
data-rioon everydivturns insight mode into noise. - Don't put expensive work in async
contentMaploaders without caching. They run on every hover. Memoize, dedupe, or cache the fetch yourself. - Don't expect a parent provider to highlight elements inside a nested provider's subtree. Each provider owns its own scope — the outer overlay deliberately bails when the cursor is inside a child. Move the annotation, or register it via the inner provider, if you need it picked up.
- Don't rely on
Escto fully exit a cascaded child.Esconly flips the provider whose key listener fired — if a parent is still enabled, the child stays lit by inheritance. Toggle the parent to exit cleanly. - Don't reuse
data-riokeys as test selectors or style hooks. They're a content-lookup mechanism; collisions are isolated per scope, not unique across the page.
Philosophy
- Infrastructure > UI library — RIO is a developer-authored context layer, not a tour or tooltip library.
- CSS > JS theming — pure CSS variables, no runtime overhead.
- Explicit > magical — one attribute, one overlay, no side effects when disabled.
- Small surface area — no step engine, no analytics, no CMS, no dependencies.
Monorepo layout
This repository contains both the published library and its marketing/landing site.
rio/
├── src/ # library source (published as react-insight-overlay)
├── landing/ # marketing site (landing page with live demo)
├── index.html # landing entry
├── vite.config.ts # landing page build
└── vite.lib.config.ts # library buildDevelopment
# Install deps
npm install
# Run landing page dev server
npm run dev
# Build the library (→ dist/)
npm run build:lib
# Build the landing page (→ dist-landing/)
npm run build:landing
# Build both
npm run build
# Typecheck
npm run typecheckPeer dependencies
react >= 17react-dom >= 17
License
MIT
