Skip to content

About

End-to-end design tool for AI coding agents — iOS/macOS/web guided builds, custom CDP engine, visual validation

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

505 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

╦╔╗ ╦═╗
║╠╩╗╠╦╝
╩╚═╝╩╚═

Interface Built Right

UI/UX design and building tool for AI coding agents.
Plan, design, and build interfaces for iOS, macOS, and web. Audit, validate, and refine them with deterministic evidence.

npm license node


IBR primarily helps AI coding agents design and build UI/UX. It turns user intent into an implementation-ready design contract, routes the work through web, iOS, or macOS guidance, and builds the interface with Calm Precision principles and reusable component patterns.

Auditing, visual validation, interaction testing, and iterative editing support that primary workflow. They measure the rendered result, expose gaps, and guide focused corrections after the design direction and build target are clear.

Built on a custom CDP engine — no Playwright. Works from terminal, Codex, Claude Code slash commands, or code. Zero config.

Surfaces

IBR ships three ways to drive it: a CLI (npx ibr ..., see skills/cli-reference/SKILL.md), a programmatic API (@tyroneross/interface-built-right's exports map), and an MCP server. The CLI and the API are the supported, auto-wired surfaces. The MCP server is dormant/opt-in — it still builds, ships, and passes its tests, but Claude Code no longer auto-discovers it. See optional-mcp/README.md to re-enable it.

Design Workflow

/ibr:build <topic> orchestrates the full design-to-validation flow:

  1. Preamble — Platform, scope, design mode, archetype hints, UI template, references, density
  2. Optional imagegen concepts — Generates visual concepts only when useful, then requires approval before any concept becomes a visual-target
  3. Design Director — Produces design-intent.json, specialist planning passes, target roles, and validation criteria
  4. Brainstorm & Plan — Guided exploration with platform-specific design rules and a concrete implementation plan
  5. Implement — Build with design guidance: Calm Precision, web/iOS/macOS routers, component patterns, data-viz guidance when needed
  6. Validate — Scan, match wireframe/visual targets, test interactions, iterate until passing

Design Director

The design-director skill is the primary planning layer for page, flow, app, dashboard, and reference-heavy UI work. It selects guidance in a deterministic order, resolves Mockup Gallery references into roles (wireframe-target, visual-target, inspiration, data-reference), and writes specialist planning artifacts under .ibr/builds/<topic>/specialists/.

Specialist passes are used for flow, visual system, interaction states, content states, Mockup Gallery targets, data visualization, and validation. IBR does not create separate tiny agents for buttons, fonts, or individual visual atoms; those stay inside component patterns and design tokens.

Imagegen Concepts

IBR can use imagegen as an upstream concept pass for mood, style, hero/product imagery, and hi-fi variants. Generated concepts are saved under .ibr/builds/<topic>/references/imagegen/ when they are project-bound. They remain inspiration until the user explicitly approves one as a visual-target.

Imagegen does not replace wireframes, accessibility semantics, interaction requirements, or validation. IBR still validates the rendered implementation with scan, interaction, match, and native tools.

Web Design Routing

The web-design-router skill classifies web interfaces into archetypes: SaaS dashboard, data/research tool, editor/workbench, AI agent chat, commerce/checkout, content/publication, and internal admin. Each archetype sets defaults for navigation, density, primary content, mobile behavior, and validation focus.

iOS Design System

The ios-design-router skill classifies apps into 6 archetypes, each with pre-set defaults for navigation, color, typography, motion, and more. Domain reference files provide comprehensive option catalogs:

Domain Reference Covers
Navigation 1_navigation_structure.md Tab bars, transitions, sheets, page hierarchy
Content 2_lists_cards_content.md Lists, cards, swipe actions, content resilience
Interactions 3_buttons_touch_interactions.md Buttons, haptics, toggles, forms
Visual 4_color_surface_typography.md Color, gradients, dark mode, typography
Motion & States 5_motion_states_identity.md Loading, onboarding, celebrations, profiles
Task Economy 6_task_economy.md Step counting, flow validation

Two-Tier Architecture

IBR scans return structured data, not just raw element dumps. Two layers run on every scan:

Tier 1 — Deterministic Rule Engine (no LLM)

Runs pure algorithms against the runtime data. Zero tokens. Returns structured verdicts with evidence.

Rule Preset What It Checks Algorithm
wcag-contrast Text contrast ratios, AA and AAA WCAG 2.1 relative luminance. Parses oklch, oklab, lch, lab, hsl, color(), hex 3/4/6/8, named, and rgb/rgba — a color it cannot decode is reported as wcag-aa-contrast-unmeasurable, never skipped silently. Large-text thresholds are 24px, or 18.66px bold
touch-targets Interactive element sizing 44px mobile (WCAG 2.5.5), 24px desktop (WCAG 2.5.8). Measures the real activation rect and honours WCAG's inline exception — see below
calm-precision Gestalt, Signal-to-Noise, Fitts, Hick, Content-Chrome, Cognitive Load Principle-based checks

What the touch-target rules measure

The size rules grade the rect a pointer can actually land on, not always the element's own layout box, and they skip the one target class WCAG exempts. Both landed after the rule reached 0% precision on a real page: every surviving finding was unactionable by construction.

Target Graded as Why
<a> inline in a sentence not graded WCAG 2.5.8 exempts a target "in a sentence or whose size is otherwise constrained by the line-height of non-target text". Growing it to 44px would reflow the paragraph
Hidden <input> with a visible <label> the label's rect Clicking an associated label activates the control, so the label is the target. An sr-only checkbox measures 1x1; its label measures 44x44
Hidden <input> whose label is hidden at this viewport not graded A sm:hidden nav toggle above its breakpoint has no pointer affordance at that width, so there is nothing to size

Deliberately still graded, because suppressing a real finding is worse than the defect being fixed: an inline-block / inline-flex control in prose (a box, resizable without reflowing text), a |-separated inline nav (separators are not prose), a paragraph whose only content is a link, a control whose <label> is itself undersized (the finding reports the label's bounds so the fix targets the right element), and any control large enough to point at whatever its label is doing.

Nothing is dropped silently: ibr ask ... 'is the touch-target compliant' reports what it skipped under meta.exempted, keyed by reason.

{ "verdict": "PASS", "findings": [], "meta": { "exempted": { "wcag-inline": 3, "label-hit-area": 1 } } }

Enable via .ibr/rules.json:

{
  "extends": ["wcag-contrast", "touch-targets", "calm-precision"]
}

ibr scan runs touch-targets, wcag-contrast, and calm-precision by default — no flag needed. Contrast is graded on body copy and headings, not just on controls, and text on a transparent background is measured by compositing through its ancestors rather than skipped.

ibr scan http://localhost:3000                            # default rules
ibr scan http://localhost:3000 --rules wcag-contrast      # only these
ibr scan http://localhost:3000 --rules none               # no preset rules (also: --no-rules)

Precedence: --rules beats .ibr/rules.json, which beats the built-in defaults.

Output in scanResult.issues with ruleId, severity, message, element, and fix fields. scanResult.rulesApplied names the presets that ran and where they came from; scanResult.contrastCoverage reports how much text was actually measured, so "no findings" can be read against a real measurement count rather than mistaken for a clean page when nothing ran.

ibr ask — focused verdicts (v3 thesis M1)

Where scan returns the whole tree, ask returns one verdict + minimal evidence. ~600 bytes vs ~19KB for the same page.

ibr ask http://localhost:3000 'is the touch-target compliant'
ibr -v mobile ask http://localhost:3000 'do status indicators follow signal-to-noise'
ibr ask http://localhost:3000 'is design-system token compliance okay'

Closed question vocabulary today (3 questions); unknown questions return verdict: UNCERTAIN with the supported list. Returns {verdict, findings[], meta} with bounds and a fix per finding. Designed for LLM agents — the agent reads a verdict, optionally drills into one finding's evidence, instead of parsing the full DOM.

Tier 2 — Sensor Layer (structured summaries)

Pre-computed summaries that let the model focus on judgment instead of re-discovering patterns:

Sensor What It Produces Token Saving
visualPatterns Groups elements by style fingerprint per category (button, link, input, heading) Replaces N element dumps with M pattern groups
componentCensus Tag/role counts + orphan cursor:pointer elements with no handler Replaces grep across pages
interactionMap Handler coverage — which interactive-looking elements actually have handlers Pre-computed from handler detection
contrast WCAG pass/fail grouped, only failures listed Model skips passing elements
navigation Link structure with depth and counts Replaces reading Sidebar.tsx
semanticState Wraps existing semantic classifier: page intent, states, available actions Summary form
oneLiners 5-second scannable summary lines Read first, then drill down

Access in scanResult.sensors. Get summaries-only (cuts ~60% tokens):

ibr scan http://localhost:3000 --output summary

Obsidian Plugin Views

Obsidian plugin views only exist inside Obsidian, so they normally cannot be audited: there is no URL to scan. scan_obsidian mounts a view in a real browser and runs the standard scan against it — computed styles, real cascade and layout, box geometry, pseudo-elements, touch targets, contrast, and accessibility.

ibr scan-obsidian ~/Vault/.obsidian/plugins/my-plugin \
  --view-class MyPluginView \
  --viewport iphone-14 \
  --view-state fixture.json \
  --post-mount "view.openEstimatePicker(30, function(){}, document.body)"

Use this, not scan_static, for any Obsidian view. scan_static is a regex parser: it cannot resolve var() or calc(), cannot compute layout or box geometry, and cannot see ::before/::after or descendant selectors. A view styled with CSS variables, grid, and a pseudo-element checkbox reports as essentially empty under it.

How it works:

  1. Obsidian stub — patches Obsidian's DOM extensions (createEl, createDiv, createSpan, setText, empty, addClass, setAttr, detach, …) onto real HTMLElement.prototype, and supplies the obsidian module (ItemView, Platform, Notice, setIcon, Setting, Menu, …). Everything is IIFE-scoped: a shim declaring a top-level class ItemView collides with the bundle's own const { ItemView } = require("obsidian") and kills it silently.
  2. Harness generator — emits a self-contained HTML page (bundle + CSS inlined), mounts the named view class into Obsidian's containerEl.children[1] content-area shape, applies view_state as the fixture, then runs post_mount.
  3. Scan — served on an ephemeral loopback port and handed to the normal scan().
Option Purpose
--view-class Name of the view exported from the bundle (required)
--viewport Preset; default iphone-14 (390px)
--mobile / --desktop Force Platform.isMobile; default inferred from viewport width
--view-state <path> JSON assigned onto the view before render — the fixture
--post-mount <js> JS run after mount (view, root in scope) to open sheets/modals
--harness-out <path> Write the harness HTML for inspection; default a temp dir

Fail-loud contract. A failed mount leaves a blank page, and a blank page has no collisions, no contrast failures, and no undersized targets — it would grade as a serene PASS. So mount failure is fatal by category, not by error count. Any unmodeled obsidian export throws when used (naming itself), and a mount script that never runs at all is caught by an absent mount marker. Known limit: instanceof against an unmodeled class returns false rather than throwing.

Current caveat. Verdicts on Obsidian views are dominated by a pre-existing IBR defect: handler detection reads only inline onclick, so every addEventListener-driven button is reported "has no click handler". Read the geometry, style, and touch-target findings; treat the verdict with suspicion until that is fixed.

Hydration Waiting

SPAs (Next.js, React, Vue) often render after networkidle fires. IBR's waitForHydration() runs after network idle:

  1. Fast-path marker detection: window.__NEXT_DATA__, React DevTools hook, #__next / #root population
  2. AX tree fingerprint polling until stable for 500ms with at least one interactive element
  3. Settle time (200ms default) to absorb async effects

This eliminates the common "0 elements" result on modern SPAs.

Live Sessions with Auto-Capture

Sessions now auto-capture pre/post interaction by default:

ibr session start http://localhost:3000
ibr session interact --action click --target "Submit"
# Automatically: pre-scan baseline -> action -> URL change detection -> hydration wait -> post-scan -> surface console errors
ibr session close

Each ActionRecord includes:

  • navigated: boolean — did the click cause a URL change?
  • urlBefore / urlAfter
  • actionErrors: string[] — console errors that appeared during this action

Opt out with --no-auto-capture on session start.

Architecture

IBR runs on a custom CDP browser engine — direct Chrome DevTools Protocol over WebSocket. No Playwright, no Puppeteer, no heavyweight browser automation dependencies.

LLM-native features built into the engine:

Feature What it does
queryAXTree-first resolution Find elements by semantic name+role (not fragile CSS selectors). 4-tier: CDP-native search → Jaro-Winkler fuzzy → vision fallback
DOM chunking Filter to interactive/leaf elements, chunk for LLM context windows. 60-70% fewer tokens
Adaptive modality Scores AX tree quality. High → use text data. Low → include screenshot. Vision only when needed
Resolution cache Caches intent→element mappings. Same query twice = instant. Clears on navigation
observe() Preview available actions without executing. Returns serializable descriptors
extract() Pull structured data from AX tree using schemas

What's New in v1.4.0

Feature Command / Usage What it does
Native macOS layout-fill / gap analysis scanMacOS returns layoutFill[]; surfaced as layout-fill: WARNINGs in issues[] Per-container largest empty horizontal AND vertical band as pixels + % of container extent. Catches the centered-narrow-content bug class (e.g. terminal at 440px centered in 1074px → 317px / 29.5% leading band) that passes screenshot + a11y + touch-target checks. Threshold default 0.12; configurable via MacOSScanOptions.layoutFill.threshold
Swift extractor --analyze-layout ibr-ax-extract --pid N --analyze-layout Same algorithm in-Swift; emits LAYOUT_FINDINGS:<json> on stderr. Stdout JSON contract unchanged
Drop-in Swift templates assets/native/swift-templates/LayoutProbe.swift + RenderSwiftUI.swift In-process layout dump + wedge-proof cacheDisplay PNG when AX/screencapture are wedged. Off-screen SwiftUI → PNG renderer. Pure AppKit/SwiftUI, zero deps

What's New in v0.7.0

Feature Command What it does
Interact (MCP + CLI) ibr interact / MCP interact Click, type, fill elements by accessible name. LLM-native interaction
Observe (MCP + CLI) ibr observe / MCP observe Preview all clickable/fillable elements before interacting
Extract (MCP + CLI) ibr extract / MCP extract Read page headings, buttons, inputs, links as structured data
Interact & Verify (MCP) MCP interact_and_verify Act + capture before/after state diff (elements added/removed)
Interaction assertions ibr test-interact Click, type, verify — act→verify→screenshot pipeline
Mockup matching ibr match Compare design mockup PNG against live page (SSIM)
Design verification ibr record-change / ibr verify-changes Capture design intent, verify against reality
Test generation ibr generate-test Auto-generate .ibr-test.json from page observation
Test runner ibr test Run declarative test files
Python scripting ibr run-script Execute Python test scripts with sandboxed resources
Fix-and-iterate ibr iterate Convergence detection for test-fix cycles
Safari support --browser safari Cross-browser via safaridriver + macOS AX API
Cross-browser diff ibr compare-browsers Side-by-side Chrome vs Safari comparison
AX tree coverage Built into ibr scan Reports AX tree capture %, shadow DOM piercing
Flow testing ibr test-search/form/login Built-in flows exposed as CLI commands
Playwright removed -- Zero Playwright dependency, custom CDP engine only

See docs/QUICK-START.md for full usage guide.

What's New in v1.0.0

Feature Command / Usage What it does
End-to-end design tool Positioning update From "visual testing platform" to design + build + validate
Design Director /ibr:build Primary design-agent layer with design intent, specialist planning passes, target roles, and validation criteria
Web design router skill: web-design-router 7 web archetypes covering dashboards, research tools, workbenches, AI chat, checkout, content, and admin
Data visualization guidance skill: data-visualization Chart-worthiness gate, chart routing, attribution, accessibility, and validation rules
iOS design system /ibr:build with platform=iOS 6-archetype router, 7 domain reference files covering navigation, lists, buttons, color, motion, task economy
apple-platform skill Loaded during iOS builds Architecture, SwiftData, concurrency, CI/CD, TestFlight — integrated from standalone apple-dev
Deterministic rule engine ibr scan (on by default) WCAG AA/AAA contrast on text and controls, touch target sizes, Calm Precision. No LLM tokens
Sensor layer scanResult.sensors Visual patterns, component census, interaction map, contrast report, navigation, one-liners
Summary output mode ibr scan --output summary Returns sensors + verdict, cuts ~60% of tokens
Hydration wait Built into ibr scan Fixes "0 elements" on SPAs; polls AX tree stability + detects Next.js/React markers
Auto-capture sessions Default on Pre/post scan with URL-change detection and console error surfacing

What's New in v0.9.0-alpha

Feature Command / Skill What it does
Build command /ibr:build <topic> Guided UI build — brainstorm, plan, implement, verify in one flow
Capture command /ibr:capture <url> Capture a named baseline snapshot for any URL
UI guidance skill: ui-guidance-library On-demand design guidance using IBR scan data
UI brainstorm preamble skill: ui-brainstorm-preamble Pre-build exploration — explore directions before implementing
UI guidance library skill: ui-guidance-library Reusable UI guidance patterns and decision aids
Mockup gallery bridge skill: mockup-gallery-bridge Bridge mockup gallery reviews to IBR scan verification
Mobile web UI skill: mobile-web-ui Mobile web patterns — responsive design, touch targets, viewport handling
iOS Design skill: ios-design iOS-specific patterns — SwiftUI conventions, safe areas, haptics
macOS UI skill: macos-ui macOS-specific patterns — AppKit/SwiftUI, menu bar, window chrome

The Problem

UI work fails when intent, platform patterns, implementation choices, and proof live in separate tools. A generic coding agent can produce a functional screen without resolving hierarchy, states, interaction behavior, or visual direction.

  • IBR design contract — defines the user goal, hierarchy, flows, states, and platform rules
  • IBR build guidance — applies archetype, component, and Calm Precision patterns during implementation
  • IBR validation — measures the rendered result and directs focused corrections

How It Works

User intent → /ibr:build → design contract → implementation → scan and refine

The supporting scan returns structured data per element:

  • computedStyles — backgroundColor, fontSize, fontFamily, padding, grid, flexbox, etc.
  • bounds — exact x, y, width, height
  • interactive — hasOnClick, hasHref, hasReactHandler, isDisabled
  • a11y — role, ariaLabel, ariaDescribedBy
  • page-level — pageIntent, auth state, loading state, console errors

For regression, capture before and compare after:

npx ibr start http://localhost:3000    # baseline before changes
# ... edit your code ...
npx ibr check                          # see what changed

Verdicts: MATCH, EXPECTED_CHANGE, UNEXPECTED_CHANGE, LAYOUT_BROKEN

Measured design specifications

IBR can create an authored design spec without Figma, capture a local HTML prototype as a measured draft, and check a rendered page against exact, bounded, and free rules. Review generated drafts before treating them as a build contract.

ibr spec:new --title 'Report' --view report-desktop --route /report --width 1200 --height 800 --out design-spec.json
ibr spec:capture --url http://localhost:3000/report --title 'Report' --view report-desktop --route /report --width 1200 --height 800 --out captured-spec.json
ibr spec:check design-spec.json report-desktop --url http://localhost:3000/report --json

Figma import remains optional. Its draft keeps text, geometry, image references, and prototype destinations; bind source nodes to semantic page elements before verification.

ibr spec:from-figma figma-file.json --frame '12:34' --route /report --out design-spec.json

See design specifications for the schema, build workflow, and measurement limits.

See terminal output
IBR terminal output

Quick Start

npm install @tyroneross/interface-built-right

That's it. .ibr/ is auto-added to your .gitignore on install.

Design and build UI/UX (primary workflow)

From a supported agent runtime:

/ibr:build account settings with profile, security, and billing states

Audit the rendered UI (supporting workflow)

npx ibr scan http://localhost:3000 --json    # measure the rendered result

Regression check:

npx ibr start http://localhost:3000    # capture baseline
# ... make changes ...
npx ibr check                          # compare

Sandbox-friendly browser attach

Use connect mode when the agent can reach a Chrome DevTools endpoint but should not spawn Chrome itself.

npx ibr scan http://localhost:3000 \
  --browser-mode connect \
  --cdp-url http://127.0.0.1:9222

You can also provide a browser WebSocket directly:

npx ibr scan http://localhost:3000 \
  --browser-mode connect \
  --ws-endpoint ws://127.0.0.1:9222/devtools/browser/<id>

Environment variables are supported for sandboxed agents and wrappers:

IBR_BROWSER_MODE=connect
IBR_CDP_URL=http://127.0.0.1:9222
IBR_WS_ENDPOINT=ws://127.0.0.1:9222/devtools/browser/<id>
IBR_CHROME_PATH=/path/to/chrome
IBR_SESSION_IDLE_MS=3600000  # persistent-session idle reaper; default 1 hour, 0 disables

IBR closes one-shot browsers when their command finishes, closes persistent sessions after one hour without an IBR session command, and reaps orphaned IBR-owned Chrome processes before the next local launch. The reaper requires an IBR profile plus a CDP port and never targets the user's normal Chrome profile.

One shared Chrome for headless commands

Headless commands share one Chrome per user instead of launching their own. Each command gets a private browser context (its own cookies, storage and cache), so commands cannot see each other's state. A watchdog stops the shared Chrome after 10 idle minutes. Commands that need their own process keep it: a visible window (--headed), a fixed profile or port, mockup normalization, or an explicit connect endpoint. If the shared Chrome cannot start, the command launches its own and says so.

IBR_BROWSER_MODE=local              # turn sharing off
IBR_SHARED_CHROME_MAX_PAGES=6       # commands using it at once; later ones wait up to 2 minutes
IBR_SHARED_CHROME_IDLE_MS=600000    # idle time before the watchdog stops it
IBR_SHARED_CHROME_DIR=~/.ibr/shared-chrome   # state, leases and profile

--headed is now the preferred flag for a visible browser window. --sandbox remains as a deprecated alias for backwards compatibility.

Setup as Agent Plugins

IBR works standalone and ships plugin metadata for both Claude Code and Codex. Claude Code gets slash commands, hooks, and the design-validator agent; Codex gets compact routing skills plus MCP/session tools for the same scan, navigation, search, and validation contract.

Claude Code

1. Add the marketplace (one-time):

/plugin marketplace add tyroneross/interface-built-right

2. Install the plugin:

/plugin install ibr@interface-built-right

3. Use in conversation:

Command What it does
/ibr:build <topic> Guided UI build: preamble → brainstorm → plan → implement → validate
skill: design-validation Full page scan with sensor summaries and optional rule checks
/ibr:snapshot / /ibr:compare Before/after regression check
skill: interactive-testing Click, type, fill by accessible name; assert an interaction
/ibr:match Compare rendered UI against a mockup (SSIM)
skill: native-testing Scan iOS/watchOS/macOS apps
/ibr:ui Open the web dashboard at localhost:4200

Example:

"Build a daily focus timer for iOS" -> /ibr:build classifies it as a Utility archetype -> routes to iOS design references -> implements with apple-platform patterns -> scans the result, reports any WCAG / touch target / hydration issues.

The plugin hooks run automatic pre/post scans around UI file edits and surface console errors immediately when interactions trigger them.

Codex

Codex consumes .codex-plugin/plugin.json and compact skills under .codex-plugin/skills/. Those skills drive the ibr CLI directly. The Codex MCP server is dormant by default — its config lives at optional-mcp/codex-mcp.json and is not wired into the manifest; see optional-mcp/README.md to opt in. Claude-style files under commands/, hooks/, and agents/ are retained for Claude Code and should not be treated as Codex-loaded routing.

Install this checkout as a local Codex plugin:

npm run build
npm run plugin:install-codex

This syncs a slim Codex bundle to ~/plugins/ibr and registers it in ~/.agents/plugins/marketplace.json. Restart Codex after installing. See Codex Plugin Setup for validation steps and failure modes.

Starter prompts:

$ibr Plan this UI with IBR.
$ibr Scan this UI with IBR.
$ibr Validate this design intent.

Use MCP sessions when the agent needs to navigate the current UI state:

{
  "tool": "flow_search",
  "sessionId": "<session id>",
  "query": "pricing plan",
  "userIntent": "Find the plan a buyer would choose",
  "aiValidation": true
}

What IBR Does For You (Plugin Hooks)

When installed as a Claude Code plugin, IBR provides:

  • Design validation reminders — after UI file edits, nudges to run npx ibr scan to verify against user intent
  • Scan + screenshot guidance — suggests also running IBR scan for precise property data alongside visual checks
  • Session end check — reminds if UI work was done but not validated
  • Bash safety — blocks destructive commands (rm -rf /, git push --force, etc.)
  • Sensitive path protection — prevents writes to ~/.ssh, ~/.aws, /etc/

Hooks are command-based and live in hooks/hooks.json. They call the repo-local shell scripts under hooks/, which are designed to fail softly when no UI work, no config, or no reachable dev server is present.

What IBR Scan Returns

Element Data (per interactive element)

selector:        Unique CSS path
tagName:         button, a, input, etc.
text:            Visible text content
bounds:          { x, y, width, height } — exact position and size
computedStyles:  backgroundColor, color, fontSize, fontFamily, fontWeight,
                 padding, margin, borderRadius, display, gap, flexDirection,
                 alignItems, justifyContent, gridTemplateColumns, etc.
interactive:     { hasOnClick, hasHref, hasReactHandler, isDisabled, cursor }
a11y:            { role, ariaLabel, ariaDescribedBy, ariaHidden }

Page-Level Data

pageIntent:      auth | form | listing | detail | dashboard | error | landing
state.auth:      { authenticated, username, confidence }
state.loading:   { loading, type: spinner|skeleton|progress }
state.errors:    { hasErrors, errors[], severity }
console:         { errors[], warnings[] }
verdict:         PASS | ISSUES | FAIL

Scan Data vs Screenshots

Each approach catches things the other misses. The best validation uses both.

Where IBR scan wins

Question Screenshot IBR Scan
Is this exactly #3b82f6? Guess from pixels backgroundColor: "rgb(59, 130, 246)"
Is the font 16px Inter? "Looks about right" fontSize: "16px", fontFamily: "Inter"
Is the button wired up? Can't tell hasOnClick: true, hasReactHandler: true
Are ARIA labels present? Can't see ariaLabel: "Submit form", role: "button"
Any console errors? Can't see console.errors: []

Where screenshots win

Question IBR Scan Screenshot
Does the page look right? Can't judge Visual coherence at a glance
Any rendering glitches? Computed styles can be correct but render wrong Sees clipping, overlap, z-index issues
Canvas/SVG/WebGL content? Not in the DOM Sees everything rendered
Font rendering quality? Reports font-family, not rendering Sees anti-aliasing, kerning
Unexpected visual artifacts? Only checks what you ask Catches things you didn't think to check

Best practice: combine both

npx ibr scan http://localhost:3000 --json    # precise property verification
# + screenshot for visual confirmation when needed

For AI agents, scan data is best for precise verification (exact values, handler detection, a11y). Screenshots are best for holistic visual checks. Together they give more confidence than either alone.

CLI Reference

Core Commands

Command Description
npx ibr scan <url> --json Validate UI — returns structured data
npx ibr start <url> Capture baseline for regression
npx ibr check Compare current state against baseline
npx ibr serve Open web UI at localhost:4200
npx ibr list List all sessions
npx ibr update Accept current as new baseline
npx ibr clean --older-than 7d Clean old sessions

Interactive Sessions

For pages that need clicks, typing, or navigation before validating:

# Start a persistent browser session
npx ibr session:start http://localhost:3000 --name "search-test"

# Interact with it
npx ibr session:type <id> "input[name=search]" "quantum computing"
npx ibr session:click <id> "button[type=submit]"
npx ibr session:wait <id> ".search-results"
npx ibr session:screenshot <id>
All interactive commands
Command Description
session:start <url> Start browser session
session:click <id> <selector> Click an element
session:type <id> <selector> <text> Type into an element
session:press <id> <key> Press keyboard key
session:scroll <id> <direction> Scroll page
session:screenshot <id> Capture screenshot
session:wait <id> <selector> Wait for element
session:navigate <id> <url> Navigate to URL
session:html <id> Get page HTML
session:text <id> <selector> Extract text content
session:close <id|all> Close session

Memory (Design Specs)

Store design preferences that IBR enforces during every scan:

# Remember that buttons should be blue
npx ibr memory add "Primary buttons are blue" --category color --property background-color --value "#3b82f6"

# Store font preference
npx ibr memory add "Body font is Inter 16px" --property font-family --value "Inter"

# List stored specs
npx ibr memory list

# IBR checks these during every scan automatically

Authenticated Pages

npx ibr login http://localhost:3000/login   # opens browser, log in manually
npx ibr scan http://localhost:3000/dashboard --json  # validates with your auth
npx ibr logout                                 # clear saved auth

Verdicts

After ibr check, you get one of four results:

Verdict Meaning Action
MATCH Nothing changed You're done
EXPECTED_CHANGE Changes look intentional Review and continue
UNEXPECTED_CHANGE Something changed that shouldn't have Investigate
LAYOUT_BROKEN Major structural issues Fix before continuing

Programmatic API

import { compare } from '@tyroneross/interface-built-right';

const result = await compare({
  url: 'http://localhost:3000/dashboard',
  baselinePath: './baselines/dashboard.png',
});

console.log(result.verdict);     // "MATCH" | "EXPECTED_CHANGE" | ...
console.log(result.diffPercent); // 2.5
console.log(result.summary);    // "Header background changed. Layout intact."
Engine API (advanced)
import { EngineDriver, CompatPage } from '@tyroneross/interface-built-right/engine';

// Direct CDP engine access
const driver = new EngineDriver();
await driver.launch({ headless: true, viewport: { width: 1920, height: 1080 } });

// Navigate with stability detection
await driver.navigate('http://localhost:3000', { waitFor: 'stable' });

// Discover interactive elements (LLM-optimized)
const elements = await driver.discover({
  filter: 'interactive',
  serialize: true,        // compact format for context windows
  maxTokens: 4000,
});

// Find elements by intent (not CSS selectors)
const button = await driver.find('Submit', { role: 'button' });

// Assess page understanding quality
const understanding = await driver.assessUnderstanding();
if (understanding.needsScreenshot) {
  const screenshot = await driver.screenshot({ fullPage: true });
}

// Extract structured data
const data = await driver.extract({
  title: { role: 'heading', extract: 'text' },
  isLoggedIn: { role: 'button', label: 'logout', extract: 'exists' },
});

await driver.close();
Session-based workflow
import { InterfaceBuiltRight } from '@tyroneross/interface-built-right';

const ibr = new InterfaceBuiltRight({
  baseUrl: 'http://localhost:3000',
  outputDir: './.ibr',
  threshold: 1.0,
});

const { sessionId } = await ibr.startSession('/dashboard', {
  name: 'dashboard-update',
});

// After changes
const report = await ibr.check(sessionId);
console.log(report.analysis.verdict);

await ibr.close();

Configuration

Optional — create .ibrrc.json in your project root:

{
  "baseUrl": "http://localhost:3000",
  "outputDir": "./.ibr",
  "viewport": "desktop",
  "threshold": 1.0,
  "fullPage": true
}

Available viewports: desktop, laptop, tablet, mobile, iphone-14, iphone-14-pro-max

artifactLint — check self-contained HTML on save

Off by default. When enabled, writing or editing an .html file runs scripts/artifact_lint.py against it and prints findings. Advisory only — it never blocks a write, and a clean page prints nothing.

{ "artifactLint": true }
{
  "artifactLint": {
    "enabled": true,
    "minSeverity": "warn",     // error | warn | info  (default: warn)
    "profile": "auto",         // auto | claude-artifact | standalone | markdown
    "disable": "AD204,AD304"   // comma-separated rule ids
  }
}

Leave it off for projects whose .html files are templates or SSR output rather than self-contained pages — rules such as AX004 and AD105 are correct about an artifact and meaningless about a Jinja template. See /ibr:artifact for the manual path, which needs no configuration.

Mobile and device emulation (1.1.0+)

IBR emulates mobile and tablet devices end-to-end via CDP (no Playwright dependency). Three things happen on driver.launch(), in order, BEFORE the first navigate:

  1. Emulation.setUserAgentOverride — so the initial HTML response sees a mobile UA
  2. Emulation.setDeviceMetricsOverride — width, height, DPR, mobile: true
  3. Emulation.setTouchEmulationEnabled — so navigator.maxTouchPoints > 0 and CSS @media (pointer: coarse) matches

--viewport presets

npx ibr scan https://example.com --viewport mobile --json
# viewport in JSON: { name: "mobile", width: 390, height: 844, deviceScaleFactor: 3, mobile: true }
Preset Size DPR Mobile
desktop 1920 x 1080 1 false
desktop-lg 2560 x 1440 1 false
desktop-sm 1440 x 900 1 false
laptop 1366 x 768 1 false
tablet 820 x 1180 (iPad Air baseline) 2 true
tablet-landscape 1180 x 820 2 true
mobile 390 x 844 (iPhone 14 baseline) 3 true
mobile-lg 430 x 932 3 true

Pre-1.1.0 --viewport mobile parsed cleanly but rendered desktop (375 x 667 with no mobile flag and no UA override). If you previously worked around this by passing custom widths, you can switch to the preset.

--device <name> — canonical profiles

--device carries a full CDP profile (viewport + DPR + mobile + UA + touch). It wins over --viewport when both are given.

npx ibr scan http://localhost:3000 --device iphone-14 --json
npx ibr session:start http://localhost:3000 --device pixel-7
Device Size DPR UA
iphone-14 390 x 844 3 iOS 17 Safari
iphone-14-pro-max 430 x 932 3 iOS 17 Safari
pixel-7 412 x 915 2.625 Android 14 Chrome
ipad-air 820 x 1180 2 iPad iOS 17 Safari
ipad-pro-11 834 x 1194 2 iPad iOS 17 Safari
desktop-1440 1440 x 900 1 (default Chrome)

Unknown --device names fail loudly: Error: Unknown --device "iphone-99". Known devices: iphone-14, .... Add more profiles in src/devices.ts.

Library use

import { EngineDriver, resolveDevice, deviceToViewport } from '@tyroneross/interface-built-right'

const profile = resolveDevice('iphone-14')
const driver = new EngineDriver()
await driver.launch({ headless: true, viewport: deviceToViewport(profile) })
await driver.navigate('https://example.com')

Troubleshooting

Problem Fix
Command not found: ibr Use npx ibr --help
Chrome not found Install Google Chrome, or pass chromePath option
Auth state expired npx ibr login <url>
Session not found npx ibr list to see available sessions
Many headless Chrome processes Run npx ibr session:close all; IBR also reaps orphaned IBR-profile processes before its next launch
--viewport mobile renders desktop (pre-1.1.0) Upgrade to >= 1.1.0; mobile presets now set the CDP mobile flag + UA + touch
Unknown --device "..." Run npx ibr scan --help to see the canonical device list, or add a profile to src/devices.ts

Requirements

  • Node.js 22+ (uses built-in WebSocket for CDP)
  • Google Chrome installed (uses system Chrome, no downloads needed)

License

Apache-2.0

About

End-to-end design tool for AI coding agents — iOS/macOS/web guided builds, custom CDP engine, visual validation

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages