Examples
CSS owns layout. React Viewport exposes geometry to logic.
Rendering budget
A search overlay or lightweight virtualizer can use the visible height to choose how many optional items React creates. CSS can size a container; JavaScript chooses which items exist. A single consumer could use VisualViewport directly; multiple React consumers can share this store.
Measuring viewport…
Example policy: reserve 320px for search controls, allow 48px per row, cap at 8. This is a viewport-derived budget, not a measurement of this card.
'use client'
import { formatGeometry } from './format-geometry'
import { useViewport } from '@nipe-solutions/react-viewport'
// Example application policy, not library defaults. Reserve space for the
// search overlay's controls, and limit how many local results React creates.
const RESERVED_SPACE = 320
const ROW_HEIGHT = 48
const MAX_RESULTS = 8
const results = [
'Document outline',
'Selection anchor',
'Search overlay',
'Visible-area panel',
'Canvas controls',
'Virtualized results',
'Zoom-aware overlay',
'Browser diagnostics',
]
export function ResultBudget() {
const { ready, visual } = useViewport()
const limit =
ready && visual
? Math.min(
MAX_RESULTS,
Math.max(0, Math.floor((visual.height - RESERVED_SPACE) / ROW_HEIGHT)),
)
: null
return (
<section className="result-budget" aria-label="JavaScript result budget">
<p role="status">
{limit === null
? 'Measuring viewport…'
: `Render budget: ${limit} results · visible height ${formatGeometry(visual?.height ?? 0)} px`}
</p>
<p>
Example policy: reserve {RESERVED_SPACE}px for search controls, allow {ROW_HEIGHT}px per
row, cap at {MAX_RESULTS}. This is a viewport-derived budget, not a measurement of this
card.
</p>
{limit === 0 && <p>No result rows fit the current budget.</p>}
{limit !== null && limit > 0 && (
<ul>
{results.slice(0, limit).map((result) => (
<li key={result}>{result}</li>
))}
</ul>
)}
</section>
)
}
Coordinate visibility and scroll correction
A canvas annotation or editor selection is application data. Compare it with the visible document bounds. The optional scroll algorithm re-evaluates after keyboard or viewport changes. Coordinate system assumptions →
Target visible? Place a target
A selected 20 × 20 document-coordinate target. Place it near the visible bottom, then open the keyboard, scroll or zoom. Its document coordinates stay fixed.
Keep the target selected while the actual keyboard changes the viewport. This field is not stored or sent.
Keyboard-aware scroll correction
The application recalculates a vertical scroll correction when viewport geometry changes. CSS cannot call a selection algorithm. Try the default observation first; opt in only to test correction.
Suggested vertical correction: Pending
Correction runs after selection, viewport-size, scale or keyboard-state changes, not ordinary scrolling. Opt-in page scrolling can be clamped at document boundaries. It does not manage focus, defeat native browser panning, or guarantee visibility inside nested scrollers.
'use client'
import { formatGeometry } from './format-geometry'
import { useEffect, useRef, useState, useId } from 'react'
import { createPortal } from 'react-dom'
import { useViewport } from '@nipe-solutions/react-viewport'
import {
correctionToReveal,
intersectsDocumentViewport,
type DocumentTarget,
} from './geometry-logic'
export function CoordinateVisibility() {
const { ready, visual, keyboard } = useViewport()
const [target, setTarget] = useState<DocumentTarget | null>(null)
const [correctScroll, setCorrectScroll] = useState(false)
const lastCorrectionTrigger = useRef('')
const inputId = useId()
const visible = target && visual ? intersectsDocumentViewport(target, visual) : null
const correction = target && visual ? correctionToReveal(target, visual) : null
// Explicit opt-in application policy: re-evaluate a selected document target
// after geometry changes. Never move focus or change the measured snapshot.
useEffect(() => {
if (!visual) return
// Page scrolling/panning alone must not pull the reader back to the target.
const trigger = JSON.stringify([
correctScroll,
target,
visual.width,
visual.height,
visual.scale,
keyboard.open,
])
if (trigger === lastCorrectionTrigger.current) return
lastCorrectionTrigger.current = trigger
if (!correctScroll || !target) return
const delta = correctionToReveal(target, visual)
if (Math.abs(delta) > 1) window.scrollBy({ top: delta, behavior: 'instant' })
}, [correctScroll, target, visual, keyboard.open])
function placeTarget() {
if (!visual) return
setTarget({
left: visual.pageLeft + Math.max(0, (visual.width - 20) / 2),
top: visual.pageTop + Math.max(0, visual.height - 48),
width: 20,
height: 20,
})
}
return (
<section className="logic-demo" aria-label="Coordinate visibility">
<p role="status" data-testid="target-visible">
Target visible?{' '}
<strong>{visible === null ? 'Place a target' : visible ? 'YES' : 'NO'}</strong>
</p>
<p>
A selected 20 × 20 document-coordinate target. Place it near the visible bottom, then open
the keyboard, scroll or zoom. Its document coordinates stay fixed.
</p>
<button type="button" onClick={placeTarget} disabled={!ready || !visual}>
Place target near visible bottom
</button>
<div className="geometry-input coordinate-input">
<label htmlFor={inputId}>Focus to test target visibility</label>
<input id={inputId} placeholder="Open the keyboard near this test…" autoComplete="off" />
<p>
Keep the target selected while the actual keyboard changes the viewport. This field is not
stored or sent.
</p>
</div>
{target && (
<>
<p data-testid="target-coordinates">
Document target: x {formatGeometry(target.left)}, y {formatGeometry(target.top)} CSS px.
</p>
<p>
Any positive rectangle overlap counts as visible. Other elements, clipping and floating
keyboards can still cover it.
</p>
<button
type="button"
onClick={() => {
setCorrectScroll(false)
setTarget(null)
}}
>
Clear target
</button>
</>
)}
<details>
<summary>Keyboard-aware scroll correction</summary>
<p>
The application recalculates a vertical scroll correction when viewport geometry changes.
CSS cannot call a selection algorithm. Try the default observation first; opt in only to
test correction.
</p>
<p data-testid="scroll-correction">
Suggested vertical correction:{' '}
{correction === null ? 'Pending' : `${formatGeometry(correction)} px`}
</p>
<label className="lab-scroll-mode">
<input
type="checkbox"
checked={correctScroll}
onChange={(event) => setCorrectScroll(event.target.checked)}
/>
Automatically reveal the selected target
</label>
<p>
Correction runs after selection, viewport-size, scale or keyboard-state changes, not
ordinary scrolling. Opt-in page scrolling can be clamped at document boundaries. It does
not manage focus, defeat native browser panning, or guarantee visibility inside nested
scrollers.
</p>
</details>
{ready &&
target &&
createPortal(
<div
className="document-target"
data-testid="document-target"
aria-hidden="true"
style={{
left: target.left,
top: target.top,
width: target.width,
height: target.height,
}}
/>,
document.body,
)}
</section>
)
}
import type { VisualViewportState } from '@nipe-solutions/react-viewport'
// Application helpers, not package API. Both rectangles use document CSS pixels.
export interface DocumentTarget {
readonly left: number
readonly top: number
readonly width: number
readonly height: number
}
export function intersectsDocumentViewport(target: DocumentTarget, visual: VisualViewportState) {
return (
visual.width > 0 &&
visual.height > 0 &&
target.width > 0 &&
target.height > 0 &&
target.left + target.width > visual.pageLeft &&
target.left < visual.pageLeft + visual.width &&
target.top + target.height > visual.pageTop &&
target.top < visual.pageTop + visual.height
)
}
export function correctionToReveal(target: DocumentTarget, visual: VisualViewportState) {
if (target.top < visual.pageTop) return target.top - visual.pageTop
return Math.max(0, target.top + target.height - (visual.pageTop + visual.height))
}
// Optional canvas hit-testing tolerance in document CSS pixels. This is not
// devicePixelRatio, physical pixels, or a responsive UI breakpoint.
export function zoomTolerance(scale: number) {
return scale > 0 && Number.isFinite(scale) ? 12 / scale : null
}
Zoom-aware tools and safe-area data
A custom rendering engine needs numerical inputs for hit testing and protected drawing regions. Ordinary CSS padding and responsive styling need no library.
Optional annotation hit tolerance: Pending
A canvas tool can convert a 12 CSS-pixel tolerance at unit scale using 12 / visual.scale. This is an algorithm input, not a smaller button or text size.
Pinch zoom is user viewport geometry, not responsive design. Essential controls remain available at every scale.
Safe-area input for a custom drawing surface: Pending.
Use these raw protected-edge values only after mapping the drawing surface into the same coordinate system. For ordinary padding, use env() directly.
'use client'
import { formatGeometry } from './format-geometry'
import { useViewport } from '@nipe-solutions/react-viewport'
import { zoomTolerance } from './geometry-logic'
export function ZoomLogic() {
const { visual, safeArea, ready } = useViewport()
const tolerance = visual ? zoomTolerance(visual.scale) : null
return (
<section className="logic-demo" aria-label="Zoom-aware tool">
<p data-testid="zoom-tolerance">
Optional annotation hit tolerance:{' '}
<strong>
{tolerance === null ? 'Pending' : `${formatGeometry(tolerance)} document CSS px`}
</strong>
</p>
<p>
A canvas tool can convert a 12 CSS-pixel tolerance at unit scale using{' '}
<code>12 / visual.scale</code>. This is an algorithm input, not a smaller button or text
size.
</p>
<p>
Pinch zoom is user viewport geometry, not responsive design. Essential controls remain
available at every scale.
</p>
<p>
Safe-area input for a custom drawing surface:{' '}
{ready
? `top ${formatGeometry(safeArea.top)}, right ${formatGeometry(safeArea.right)}, bottom ${formatGeometry(safeArea.bottom)}, left ${formatGeometry(safeArea.left)} CSS px`
: 'Pending'}
.
</p>
<p>
Use these raw protected-edge values only after mapping the drawing surface into the same
coordinate system. For ordinary padding, use <code>env()</code> directly.
</p>
</section>
)
}
CSS integration
Use useViewportCssVariables() when JS logic and CSS need the same normalized geometry. Measure once in the shared store, then let styles consume the result. Copying env() into JS and back to CSS adds little value for CSS-only padding.
import { useViewportCssVariables } from '@nipe-solutions/react-viewport'
export function GeometryCssBridge() {
useViewportCssVariables()
return null
}Secondary recipe: bottom constraints
If an application already needs measured geometry, an overlap-aware bottom constraint can consume it. This is an educational recipe, not a universal keyboard-layout fix; visual offsets and browser timing can require a different application policy.
.composer {
position: fixed;
right: max(1rem, var(--react-viewport-safe-area-right, 0px));
--bottom-inset: max(
var(--react-viewport-keyboard-height, 0px),
var(--react-viewport-safe-area-bottom, 0px)
);
bottom: calc(var(--bottom-inset) + 1rem);
left: max(1rem, var(--react-viewport-safe-area-left, 0px));
}When you don’t need React Viewport
CSS-only composer
Use normal grid or flex layout, 100dvh and safe-area padding. Request interactive-widget=resizes-content where supported. If browser-native layout meets your requirements, stop here.
CSS-only modal footer
Let the form body scroll while its action row remains in normal grid flow. A modal implementation must separately own focus and accessibility.
.modal {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
max-height: 90dvh;
}
.modal-body { overflow: auto; }
.modal-actions {
padding-bottom: max(1rem, env(safe-area-inset-bottom, 0px));
}.footer {
padding-bottom: max(1rem, env(safe-area-inset-bottom));
}Try actual browser changes
Live Geometry Lab → Observe the input data, rendering budget and coordinate test on your device. No simulated keyboard.