Generate multi-file React component bundles with design tokens from natural language descriptions.
You are now acquiring the skill of generating React UI components. After reading this document, you will know how to produce high-quality, multi-file React component bundles from natural language descriptions.
--cg- design
tokens. No hex colors, no rgb(), no named colors, no raw pixel values.
Hardcoded values like #8B6F47 or color: olive break the live theme
switcher. This is a build error, not a suggestion.localStorage, sessionStorage,
IndexedDB, or any Web Storage APIs. The iframe environment may not have
storage access due to origin restrictions. All state lives in React component
state or is passed via props and the SDK.background: blackbackground: #000color: #fffvar(--cg-color-surface*)var(--cg-color-on-surface*)Your UI MUST work from 320px to 1200px+. This is not optional.
width: 800px, width: 600px, or similar. Use max-width with a percentage
or min() instead: max-width: min(100%, 800px).min-width exceeding 320px on any layout container. This prevents
rendering on small screens.flex-wrap: wrap on any flex container with multiple children that
should stack on narrow screens.auto-fit / minmax for multi-column layouts:
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)).clamp() for font sizes that should scale with viewport:
font-size: clamp(1rem, 2vw + 0.5rem, 2rem).A multi-file React component bundle rendered in a sandboxed iframe. The bundle consists of:
App.jsx — the root component that accepts configuration propscomponents/*.jsx — reusable sub-componentsstyles.css — shared styles using CSS custom propertiesComponents use inline styles with CSS custom properties from a design token system. Import resolution between files is handled automatically by the build pipeline.
You MUST save all generated files to the real filesystem in your working
directory ($HOME) by using the execute_bash tool. Do NOT use
system_write_file, because it writes to a virtual filesystem that the
bundler cannot access.
CRITICAL STYLING RULES: NO TAILWIND!
flex, min-h-screen,
p-4, bg-red-500). Tailwind is NOT INSTALLED.className="hero-container").styles.css file using Vanilla CSS.import "./styles.css"; at the top of your App.jsx. If you
don't write and import a CSS file, your design will be completely unstyled.Use bash heredocs to write files. Example:
cat << 'EOF' > styles.css
.hero-container {
display: flex;
padding: var(--cg-sp-4);
}
EOF
cat << 'EOF' > App.jsx
import React from "react";
import "./styles.css";
export default function App() { ... }
EOF
CRITICAL: BUNDLE YOUR CODE After safely writing all files using
execute_bash, you MUST build the bundle by running the following command via
execute_bash: node $HOME/skills/ui-generator/tools/bundler.mjs
If you do not run this exact command, the user will see a blank screen.
Once bundling is successful, call system_objective_fulfilled with a short
confirmation that the UI was generated and bundled. Do NOT output raw source
code in your response.
Before generating sub-components, check library/ for existing components from
previous runs. Each subdirectory is a previous run, containing its App.jsx and
components/*.jsx.
Reuse workflow:
system_read_text_from_file to list library/ and browse available
components.PieChart, Header), just
import it — import PieChart from "./components/PieChart". You do NOT need
to save the file; the build pipeline resolves library components
automatically.When you reuse a component, include a comment at the top:
// Reused from: library/<run-id>/<filename>
App.jsx and
contain a function called App.App
with realistic defaults.components/ should
render standalone with sensible defaults. Document all props with @prop
JSDoc.import React from "react" in
every JSX file. Import sub-components with relative paths (e.g.
import Header from "./components/Header").import "./styles.css" in App.jsx for shared
styles.export default its component
function.When creating a component, think about what data the caller would want to
customize. These become props on App:
| UI Type | Example Props |
|---|---|
| Weather dashboard | location, temperature, condition, forecast (array) |
| User profile | name, avatar, bio, stats (object) |
| Product card | title, price, image, rating, reviews |
| Task manager | tasks (array), categories, user |
| Analytics dashboard | metrics (array), timeRange, chartData |
All props MUST have realistic default values so the component renders standalone with zero configuration.
Reminder: this is a hard rule (see above). Every visual value — colors,
spacing, type, radii, shadows — MUST use --cg- tokens. No exceptions.
| Category | Use | Never |
|---|---|---|
| Colors | var(--cg-color-...) | #hex, rgb(), named colors |
| Spacing | var(--cg-sp-...) | Raw pixel values for padding/margin/gap |
| Font sizes | var(--cg-text-...-size) | 14px, 1rem |
| Border radius | var(--cg-radius-...) or var(--cg-card-radius) | 12px, 24px |
| Shadows | var(--cg-elevation-...) or var(--cg-card-shadow) | Raw box-shadow values |
| Font family | var(--cg-font-sans) or var(--cg-font-mono) | 'Arial', sans-serif |
Colors: --cg-color-surface-dim, --cg-color-surface,
--cg-color-surface-bright, --cg-color-surface-container-lowest,
--cg-color-surface-container-low, --cg-color-surface-container,
--cg-color-surface-container-high, --cg-color-surface-container-highest,
--cg-color-on-surface, --cg-color-on-surface-muted, --cg-color-primary,
--cg-color-primary-container, --cg-color-on-primary,
--cg-color-on-primary-container, --cg-color-secondary,
--cg-color-secondary-container, --cg-color-on-secondary,
--cg-color-on-secondary-container, --cg-color-tertiary,
--cg-color-tertiary-container, --cg-color-on-tertiary,
--cg-color-on-tertiary-container, --cg-color-error,
--cg-color-error-container, --cg-color-on-error,
--cg-color-on-error-container, --cg-color-outline,
--cg-color-outline-variant
Typography: --cg-font-sans, --cg-font-mono,
--cg-text-display-{lg,md,sm}-{size,line-height,weight},
--cg-text-headline-{lg,md,sm}-{size,line-height,weight},
--cg-text-title-{lg,md,sm}-{size,line-height,weight},
--cg-text-body-{lg,md,sm}-{size,line-height,weight},
--cg-text-label-{lg,md,sm}-{size,line-height,weight}
Spacing (4px grid): --cg-sp-0 through --cg-sp-16
Radius: --cg-radius-{xs,sm,md,lg,xl,full}
Elevation: --cg-elevation-{1,2,3}
Motion: --cg-motion-duration-{short,medium,long},
--cg-motion-easing-{standard,decel,accel}
Component tokens: Card: --cg-card-{bg,radius,padding,shadow}, Button:
--cg-button-{radius,padding,bg,color,font-size,font-weight}, Input:
--cg-input-{bg,border,radius,padding,color,placeholder}, Badge:
--cg-badge-{bg,color,radius,padding,font-size}, Divider:
--cg-divider-{color,thickness,style}
Expressive: --cg-border-{style,width},
--cg-heading-{transform,letter-spacing},
--cg-img-{radius,border,shadow,filter}, --cg-hover-{scale,brightness,shadow}
Header,
MetricsGrid, ForecastCard, etc.Google Material Symbols Outlined is available via a web font:
<span className="material-symbols-outlined" style={{ fontSize: "20px" }}>
search
</span>
CRITICAL: DO NOT import third-party icon packages. You do not have
lucide-react, heroicons, or react-icons installed. Using them will break
the build. ONLY use the material-symbols-outlined span.
Components should be interactive where appropriate. Use useState, useEffect
with cleanup. Supported patterns: timers, carousels, accordions, tabs,
checklists, toggles.
Never use Date.now(), Math.random(), or new Date() in default parameters.
Compute once at module level or use useState(() => ...).
When building UI for a journey segment, you are building a multi-view mini-app — one React component per state in the segment's XState machine.
Your mini-app is one segment of a wider orchestrated journey. Between segments, an LLM orchestrator examines the user's data and decides what comes next. This means:
ark.emit() to
hand collected data back to the orchestrator. Without this, the journey
stalls.Each state gets its own component file named after the state:
App.jsx — shell that renders the initial stateviews/InputRequirements.jsx — one view per stateviews/SelectModels.jsxviews/DetailedComparison.jsxviews/DecisionReport.jsxcomponents/*.jsx — shared sub-components (reusable across views)styles.css — shared stylesWithin the segment, views navigate using window.opalSDK.navigateTo. At the
boundary (the segment's final view), use window.opalSDK.emit to send data
back to the orchestrator.
The SDK is available as window.opalSDK. It has three methods:
// Navigate to another view WITHIN this segment.
window.opalSDK.navigateTo("select_models", { teamProfile });
// Send data BACK TO THE ORCHESTRATOR (segment boundary).
// Use on the final view's CTA — this is what connects segments.
window.opalSDK.emit("journey:result", { decision, comparisonSet });
// Read a file from the shared workspace (async, returns text or null).
const data = await window.opalSDK.readFile("groceries.json");
Do not call any other methods on window.opalSDK. There is no
onNavigation, subscribe, or event listener API. Navigation state is managed
internally by your App component (e.g. useState + switch statement), not by
the SDK.
readFileUse readFile to load data files from the shared workspace at runtime instead
of hardcoding data into your component. Paths are relative to the workspace
root. You can read files written by any agent in the workspace — for example, a
menu planner can read files produced by a diet researcher.
import React, { useState, useEffect } from "react";
export default function GroceryList() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
window.opalSDK.readFile("groceries.json").then((text) => {
if (text) {
setItems(JSON.parse(text));
}
setLoading(false);
});
}, []);
if (loading) return <div>Loading…</div>;
return (
<ul>
{items.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
}
Rules for readFile:
Promise<string | null>. null means the file was not found."analysis/results.json",
"diet_research/notes.md").null case gracefully — the file may not exist yet.Each view component receives two props:
data — the journey context relevant to this stateonTransition — callback for state transitions (wired to
window.opalSDK.navigateTo)export default function SelectModels({ data = {}, onTransition }) {
const handleSelect = (item) => {
onTransition("detailed_comparison", {
...data,
shortlist: [...(data.shortlist || []), item],
});
};
// ...
}
components/. Headers, cards, buttons used
across multiple views should be extracted.readFile to load shared workspace data.window.opalSDK.emit("journey:result", data) with the data the orchestrator
needs to decide what happens next.App.jsx, styles.css) to
disk using execute_bash (e.g. cat << 'EOF' > ...) BEFORE running the
bundler.execute_bash tool after
writing your code to disk.execute_bash tool with the command argument set to
node $HOME/skills/ui-generator/tools/bundler.mjs. This generates an
optimized bundle.js and bundle.css in your working directory, ready for
the iframe to render.generate_and_execute_code or
system_write_file. Ensure all operations happen on the real filesystem via
execute_bash.React, useState, useEffect, useRef, useCallback, useMemo,
useContext, useReducer, useLayoutEffect, memo, forwardRef,
createContext, Fragment
Be creative and visually impressive.