[{"data":1,"prerenderedAt":1219},["ShallowReactive",2],{"blog-2026-02-27-json-render-generative-ui-framework":3},{"id":4,"title":5,"author":6,"body":7,"date":1202,"description":1203,"extension":1204,"language":1205,"meta":1206,"navigation":149,"path":1207,"seo":1208,"stem":1209,"tags":1210,"__hash__":1218},"blog/blog/2026-02-27-json-render-generative-ui-framework.md","json-render: The Missing Layer Between AI and UI","NeoAI",{"type":8,"value":9,"toc":1189},"minimark",[10,19,30,35,38,45,64,71,75,82,388,399,402,406,412,660,663,667,674,762,765,769,772,866,885,892,984,987,991,994,1064,1067,1073,1077,1084,1088,1095,1098,1102,1105,1111,1117,1123,1129,1133,1136,1143,1146,1150,1161,1164,1167,1170,1185],[11,12,13,14,18],"p",{},"AI is good at generating content. It turns out it's also pretty good at generating ",[15,16,17],"em",{},"interfaces"," — if you give it the right constraints.",[11,20,21,22,29],{},"That's the bet behind ",[23,24,28],"a",{"href":25,"rel":26},"https://json-render.dev",[27],"nofollow","json-render",", a new open-source framework from Vercel Labs. The pitch is compact: AI generates a JSON structure describing a UI; your components render it. But the architectural decisions underneath that pitch are worth unpacking.",[31,32,34],"h2",{"id":33},"the-problem-with-ai-ui-today","The problem with AI + UI today",[11,36,37],{},"Current AI-UI integrations fall into two camps, and both have real limitations.",[11,39,40,44],{},[41,42,43],"strong",{},"Camp 1: AI fills templates."," The developer builds the layout and AI populates the data. The UI never changes structure. Safe, predictable, boring. You're using AI as a smarter database query.",[11,46,47,50,51,55,56,59,60,63],{},[41,48,49],{},"Camp 2: AI generates code."," The AI produces JSX or HTML directly. Creative, flexible, and completely untrustworthy in production. Nothing stops it from hallucinating a component that doesn't exist, generating a ",[52,53,54],"code",{},"\u003Cscript>"," tag, or producing structurally invalid output that crashes the renderer. The \"AI code generation\" approach collapses the distinction between ",[15,57,58],{},"instructions"," and ",[15,61,62],{},"execution"," — which is exactly the property that makes it dangerous.",[11,65,66,67,70],{},"json-render takes a third path. The AI generates a ",[15,68,69],{},"spec"," — a constrained JSON description of a UI — and your components do the actual rendering. The spec is the boundary. The AI never touches your component implementations. Your components never receive arbitrary AI output.",[31,72,74],{"id":73},"the-catalog-defining-the-contract","The catalog: defining the contract",[11,76,77,78,81],{},"The central concept is the ",[41,79,80],{},"catalog",": a typed declaration of exactly which components, actions, and data bindings AI is allowed to use.",[83,84,89],"pre",{"className":85,"code":86,"language":87,"meta":88,"style":88},"language-typescript shiki shiki-themes github-light github-dark","import { defineCatalog } from \"@json-render/core\";\nimport { schema } from \"@json-render/react/schema\";\nimport { z } from \"zod\";\n\nconst catalog = defineCatalog(schema, {\n  components: {\n    MetricCard: {\n      props: z.object({\n        label: z.string(),\n        value: z.string(),\n        trend: z.enum([\"up\", \"down\", \"flat\"]).nullable(),\n      }),\n      description: \"Displays a single KPI with optional trend indicator\",\n    },\n    DataTable: {\n      props: z.object({\n        columns: z.array(z.string()),\n        caption: z.string().nullable(),\n      }),\n      description: \"A tabular data display\",\n    },\n  },\n  actions: {\n    export_csv: { description: \"Export the current table as CSV\" },\n    refresh: { description: \"Reload data from source\" },\n  },\n});\n","typescript","",[52,90,91,114,129,144,151,171,177,183,195,207,217,251,257,269,275,281,290,307,322,327,337,342,348,354,366,377,382],{"__ignoreMap":88},[92,93,96,100,104,107,111],"span",{"class":94,"line":95},"line",1,[92,97,99],{"class":98},"szBVR","import",[92,101,103],{"class":102},"sVt8B"," { defineCatalog } ",[92,105,106],{"class":98},"from",[92,108,110],{"class":109},"sZZnC"," \"@json-render/core\"",[92,112,113],{"class":102},";\n",[92,115,117,119,122,124,127],{"class":94,"line":116},2,[92,118,99],{"class":98},[92,120,121],{"class":102}," { schema } ",[92,123,106],{"class":98},[92,125,126],{"class":109}," \"@json-render/react/schema\"",[92,128,113],{"class":102},[92,130,132,134,137,139,142],{"class":94,"line":131},3,[92,133,99],{"class":98},[92,135,136],{"class":102}," { z } ",[92,138,106],{"class":98},[92,140,141],{"class":109}," \"zod\"",[92,143,113],{"class":102},[92,145,147],{"class":94,"line":146},4,[92,148,150],{"emptyLinePlaceholder":149},true,"\n",[92,152,154,157,161,164,168],{"class":94,"line":153},5,[92,155,156],{"class":98},"const",[92,158,160],{"class":159},"sj4cs"," catalog",[92,162,163],{"class":98}," =",[92,165,167],{"class":166},"sScJk"," defineCatalog",[92,169,170],{"class":102},"(schema, {\n",[92,172,174],{"class":94,"line":173},6,[92,175,176],{"class":102},"  components: {\n",[92,178,180],{"class":94,"line":179},7,[92,181,182],{"class":102},"    MetricCard: {\n",[92,184,186,189,192],{"class":94,"line":185},8,[92,187,188],{"class":102},"      props: z.",[92,190,191],{"class":166},"object",[92,193,194],{"class":102},"({\n",[92,196,198,201,204],{"class":94,"line":197},9,[92,199,200],{"class":102},"        label: z.",[92,202,203],{"class":166},"string",[92,205,206],{"class":102},"(),\n",[92,208,210,213,215],{"class":94,"line":209},10,[92,211,212],{"class":102},"        value: z.",[92,214,203],{"class":166},[92,216,206],{"class":102},[92,218,220,223,226,229,232,235,238,240,243,246,249],{"class":94,"line":219},11,[92,221,222],{"class":102},"        trend: z.",[92,224,225],{"class":166},"enum",[92,227,228],{"class":102},"([",[92,230,231],{"class":109},"\"up\"",[92,233,234],{"class":102},", ",[92,236,237],{"class":109},"\"down\"",[92,239,234],{"class":102},[92,241,242],{"class":109},"\"flat\"",[92,244,245],{"class":102},"]).",[92,247,248],{"class":166},"nullable",[92,250,206],{"class":102},[92,252,254],{"class":94,"line":253},12,[92,255,256],{"class":102},"      }),\n",[92,258,260,263,266],{"class":94,"line":259},13,[92,261,262],{"class":102},"      description: ",[92,264,265],{"class":109},"\"Displays a single KPI with optional trend indicator\"",[92,267,268],{"class":102},",\n",[92,270,272],{"class":94,"line":271},14,[92,273,274],{"class":102},"    },\n",[92,276,278],{"class":94,"line":277},15,[92,279,280],{"class":102},"    DataTable: {\n",[92,282,284,286,288],{"class":94,"line":283},16,[92,285,188],{"class":102},[92,287,191],{"class":166},[92,289,194],{"class":102},[92,291,293,296,299,302,304],{"class":94,"line":292},17,[92,294,295],{"class":102},"        columns: z.",[92,297,298],{"class":166},"array",[92,300,301],{"class":102},"(z.",[92,303,203],{"class":166},[92,305,306],{"class":102},"()),\n",[92,308,310,313,315,318,320],{"class":94,"line":309},18,[92,311,312],{"class":102},"        caption: z.",[92,314,203],{"class":166},[92,316,317],{"class":102},"().",[92,319,248],{"class":166},[92,321,206],{"class":102},[92,323,325],{"class":94,"line":324},19,[92,326,256],{"class":102},[92,328,330,332,335],{"class":94,"line":329},20,[92,331,262],{"class":102},[92,333,334],{"class":109},"\"A tabular data display\"",[92,336,268],{"class":102},[92,338,340],{"class":94,"line":339},21,[92,341,274],{"class":102},[92,343,345],{"class":94,"line":344},22,[92,346,347],{"class":102},"  },\n",[92,349,351],{"class":94,"line":350},23,[92,352,353],{"class":102},"  actions: {\n",[92,355,357,360,363],{"class":94,"line":356},24,[92,358,359],{"class":102},"    export_csv: { description: ",[92,361,362],{"class":109},"\"Export the current table as CSV\"",[92,364,365],{"class":102}," },\n",[92,367,369,372,375],{"class":94,"line":368},25,[92,370,371],{"class":102},"    refresh: { description: ",[92,373,374],{"class":109},"\"Reload data from source\"",[92,376,365],{"class":102},[92,378,380],{"class":94,"line":379},26,[92,381,347],{"class":102},[92,383,385],{"class":94,"line":384},27,[92,386,387],{"class":102},"});\n",[11,389,390,391,394,395,398],{},"Two things happen here simultaneously: you constrain the solution space — AI cannot hallucinate a ",[52,392,393],{},"SuperChart3D"," component that doesn't exist in your catalog — and the catalog generates the system prompt automatically via ",[52,396,397],{},"catalog.prompt()",", so the AI always knows exactly what's available. The contract is bidirectional: it constrains the AI and informs it at the same time.",[11,400,401],{},"This is structurally similar to how type systems work. A type system doesn't prevent you from writing programs — it constrains the space of valid programs to those that can be reasoned about. json-render applies the same principle to generative interfaces.",[31,403,405],{"id":404},"the-spec-what-ai-actually-produces","The spec: what AI actually produces",[11,407,408,409,411],{},"Given a catalog and a user prompt like \"show me a revenue dashboard for Q4\", the AI outputs a ",[41,410,69],{}," — a flat dictionary of typed elements:",[83,413,417],{"className":414,"code":415,"language":416,"meta":88,"style":88},"language-json shiki shiki-themes github-light github-dark","{\n  \"root\": \"dashboard-1\",\n  \"elements\": {\n    \"dashboard-1\": {\n      \"type\": \"Stack\",\n      \"props\": { \"direction\": \"column\", \"gap\": 4 },\n      \"children\": [\"metric-revenue\", \"metric-orders\", \"table-breakdown\"]\n    },\n    \"metric-revenue\": {\n      \"type\": \"MetricCard\",\n      \"props\": { \"label\": \"Total Revenue\", \"value\": \"$2.4M\", \"trend\": \"up\" },\n      \"children\": []\n    },\n    \"table-breakdown\": {\n      \"type\": \"DataTable\",\n      \"props\": { \"columns\": [\"Region\", \"Revenue\", \"Growth\"], \"caption\": \"Q4 by region\" },\n      \"children\": []\n    }\n  }\n}\n","json",[52,418,419,424,437,445,452,464,492,516,520,527,538,573,580,584,591,602,639,645,650,655],{"__ignoreMap":88},[92,420,421],{"class":94,"line":95},[92,422,423],{"class":102},"{\n",[92,425,426,429,432,435],{"class":94,"line":116},[92,427,428],{"class":159},"  \"root\"",[92,430,431],{"class":102},": ",[92,433,434],{"class":109},"\"dashboard-1\"",[92,436,268],{"class":102},[92,438,439,442],{"class":94,"line":131},[92,440,441],{"class":159},"  \"elements\"",[92,443,444],{"class":102},": {\n",[92,446,447,450],{"class":94,"line":146},[92,448,449],{"class":159},"    \"dashboard-1\"",[92,451,444],{"class":102},[92,453,454,457,459,462],{"class":94,"line":153},[92,455,456],{"class":159},"      \"type\"",[92,458,431],{"class":102},[92,460,461],{"class":109},"\"Stack\"",[92,463,268],{"class":102},[92,465,466,469,472,475,477,480,482,485,487,490],{"class":94,"line":173},[92,467,468],{"class":159},"      \"props\"",[92,470,471],{"class":102},": { ",[92,473,474],{"class":159},"\"direction\"",[92,476,431],{"class":102},[92,478,479],{"class":109},"\"column\"",[92,481,234],{"class":102},[92,483,484],{"class":159},"\"gap\"",[92,486,431],{"class":102},[92,488,489],{"class":159},"4",[92,491,365],{"class":102},[92,493,494,497,500,503,505,508,510,513],{"class":94,"line":179},[92,495,496],{"class":159},"      \"children\"",[92,498,499],{"class":102},": [",[92,501,502],{"class":109},"\"metric-revenue\"",[92,504,234],{"class":102},[92,506,507],{"class":109},"\"metric-orders\"",[92,509,234],{"class":102},[92,511,512],{"class":109},"\"table-breakdown\"",[92,514,515],{"class":102},"]\n",[92,517,518],{"class":94,"line":185},[92,519,274],{"class":102},[92,521,522,525],{"class":94,"line":197},[92,523,524],{"class":159},"    \"metric-revenue\"",[92,526,444],{"class":102},[92,528,529,531,533,536],{"class":94,"line":209},[92,530,456],{"class":159},[92,532,431],{"class":102},[92,534,535],{"class":109},"\"MetricCard\"",[92,537,268],{"class":102},[92,539,540,542,544,547,549,552,554,557,559,562,564,567,569,571],{"class":94,"line":219},[92,541,468],{"class":159},[92,543,471],{"class":102},[92,545,546],{"class":159},"\"label\"",[92,548,431],{"class":102},[92,550,551],{"class":109},"\"Total Revenue\"",[92,553,234],{"class":102},[92,555,556],{"class":159},"\"value\"",[92,558,431],{"class":102},[92,560,561],{"class":109},"\"$2.4M\"",[92,563,234],{"class":102},[92,565,566],{"class":159},"\"trend\"",[92,568,431],{"class":102},[92,570,231],{"class":109},[92,572,365],{"class":102},[92,574,575,577],{"class":94,"line":253},[92,576,496],{"class":159},[92,578,579],{"class":102},": []\n",[92,581,582],{"class":94,"line":259},[92,583,274],{"class":102},[92,585,586,589],{"class":94,"line":271},[92,587,588],{"class":159},"    \"table-breakdown\"",[92,590,444],{"class":102},[92,592,593,595,597,600],{"class":94,"line":277},[92,594,456],{"class":159},[92,596,431],{"class":102},[92,598,599],{"class":109},"\"DataTable\"",[92,601,268],{"class":102},[92,603,604,606,608,611,613,616,618,621,623,626,629,632,634,637],{"class":94,"line":283},[92,605,468],{"class":159},[92,607,471],{"class":102},[92,609,610],{"class":159},"\"columns\"",[92,612,499],{"class":102},[92,614,615],{"class":109},"\"Region\"",[92,617,234],{"class":102},[92,619,620],{"class":109},"\"Revenue\"",[92,622,234],{"class":102},[92,624,625],{"class":109},"\"Growth\"",[92,627,628],{"class":102},"], ",[92,630,631],{"class":159},"\"caption\"",[92,633,431],{"class":102},[92,635,636],{"class":109},"\"Q4 by region\"",[92,638,365],{"class":102},[92,640,641,643],{"class":94,"line":292},[92,642,496],{"class":159},[92,644,579],{"class":102},[92,646,647],{"class":94,"line":309},[92,648,649],{"class":102},"    }\n",[92,651,652],{"class":94,"line":324},[92,653,654],{"class":102},"  }\n",[92,656,657],{"class":94,"line":329},[92,658,659],{"class":102},"}\n",[11,661,662],{},"Notice the flat structure: elements are stored in a dictionary keyed by ID, with children expressed as arrays of IDs rather than nested objects. This is a deliberate streaming decision. When JSON arrives incrementally over a network, a flat dictionary lets you insert any element by ID without knowing its position in the tree. Nested structures force sequential parsing; flat dictionaries allow order-independent updates. The renderer resolves the tree at render time.",[31,664,666],{"id":665},"streaming-structure-that-emerges","Streaming: structure that emerges",[11,668,669,670,673],{},"As the AI responds chunk by chunk, the ",[52,671,672],{},"SpecStreamCompiler"," converts JSONL patches into spec updates:",[83,675,677],{"className":85,"code":676,"language":87,"meta":88,"style":88},"const compiler = createSpecStreamCompiler();\n\nfor await (const chunk of stream) {\n  const { result } = compiler.push(chunk);\n  setSpec(result); // UI updates with each partial result\n}\n",[52,678,679,694,698,720,746,758],{"__ignoreMap":88},[92,680,681,683,686,688,691],{"class":94,"line":95},[92,682,156],{"class":98},[92,684,685],{"class":159}," compiler",[92,687,163],{"class":98},[92,689,690],{"class":166}," createSpecStreamCompiler",[92,692,693],{"class":102},"();\n",[92,695,696],{"class":94,"line":116},[92,697,150],{"emptyLinePlaceholder":149},[92,699,700,703,706,709,711,714,717],{"class":94,"line":131},[92,701,702],{"class":98},"for",[92,704,705],{"class":98}," await",[92,707,708],{"class":102}," (",[92,710,156],{"class":98},[92,712,713],{"class":159}," chunk",[92,715,716],{"class":98}," of",[92,718,719],{"class":102}," stream) {\n",[92,721,722,725,728,731,734,737,740,743],{"class":94,"line":146},[92,723,724],{"class":98},"  const",[92,726,727],{"class":102}," { ",[92,729,730],{"class":159},"result",[92,732,733],{"class":102}," } ",[92,735,736],{"class":98},"=",[92,738,739],{"class":102}," compiler.",[92,741,742],{"class":166},"push",[92,744,745],{"class":102},"(chunk);\n",[92,747,748,751,754],{"class":94,"line":153},[92,749,750],{"class":166},"  setSpec",[92,752,753],{"class":102},"(result); ",[92,755,757],{"class":756},"sJ8bj","// UI updates with each partial result\n",[92,759,760],{"class":94,"line":173},[92,761,659],{"class":102},[11,763,764],{},"Elements appear in the UI as soon as they're described in the stream. The dashboard header renders before the table is fully specified. This isn't just a loading optimization — it changes how the interaction feels. Instead of a blank screen followed by a complete UI, structure emerges progressively. The experience is closer to watching someone design a layout in real time than waiting for a page to load.",[31,766,768],{"id":767},"data-binding-ai-wires-the-structure-you-own-the-logic","Data binding: AI wires the structure, you own the logic",[11,770,771],{},"The spec format includes a declarative expression language for data binding:",[83,773,775],{"className":414,"code":774,"language":416,"meta":88,"style":88},"{\n  \"type\": \"Alert\",\n  \"props\": { \"message\": \"Validation failed\" },\n  \"visible\": [\n    { \"$state\": \"/form/hasError\" },\n    { \"$state\": \"/form/errorDismissed\", \"not\": true }\n  ]\n}\n",[52,776,777,781,793,810,818,833,857,862],{"__ignoreMap":88},[92,778,779],{"class":94,"line":95},[92,780,423],{"class":102},[92,782,783,786,788,791],{"class":94,"line":116},[92,784,785],{"class":159},"  \"type\"",[92,787,431],{"class":102},[92,789,790],{"class":109},"\"Alert\"",[92,792,268],{"class":102},[92,794,795,798,800,803,805,808],{"class":94,"line":131},[92,796,797],{"class":159},"  \"props\"",[92,799,471],{"class":102},[92,801,802],{"class":159},"\"message\"",[92,804,431],{"class":102},[92,806,807],{"class":109},"\"Validation failed\"",[92,809,365],{"class":102},[92,811,812,815],{"class":94,"line":146},[92,813,814],{"class":159},"  \"visible\"",[92,816,817],{"class":102},": [\n",[92,819,820,823,826,828,831],{"class":94,"line":153},[92,821,822],{"class":102},"    { ",[92,824,825],{"class":159},"\"$state\"",[92,827,431],{"class":102},[92,829,830],{"class":109},"\"/form/hasError\"",[92,832,365],{"class":102},[92,834,835,837,839,841,844,846,849,851,854],{"class":94,"line":173},[92,836,822],{"class":102},[92,838,825],{"class":159},[92,840,431],{"class":102},[92,842,843],{"class":109},"\"/form/errorDismissed\"",[92,845,234],{"class":102},[92,847,848],{"class":159},"\"not\"",[92,850,431],{"class":102},[92,852,853],{"class":159},"true",[92,855,856],{"class":102}," }\n",[92,858,859],{"class":94,"line":179},[92,860,861],{"class":102},"  ]\n",[92,863,864],{"class":94,"line":185},[92,865,659],{"class":102},[11,867,868,869,872,873,876,877,880,881,884],{},"Four expression forms cover most real-world cases: ",[52,870,871],{},"$state"," (read from state), ",[52,874,875],{},"$cond"," (conditional branch), ",[52,878,879],{},"$template"," (string interpolation with state values), and ",[52,882,883],{},"$computed"," (call a registered function). The AI can generate conditional visibility, dynamic styling, and reactive text without touching application logic.",[11,886,887,888,891],{},"The ",[52,889,890],{},"watch"," field extends this to side effects. When a Select changes its value, a registered action fires automatically:",[83,893,895],{"className":414,"code":894,"language":416,"meta":88,"style":88},"{\n  \"type\": \"Select\",\n  \"props\": { \"value\": { \"$bindState\": \"/form/country\" } },\n  \"watch\": {\n    \"/form/country\": { \"action\": \"loadCities\", \"params\": { \"country\": { \"$state\": \"/form/country\" } } }\n  }\n}\n",[52,896,897,901,912,933,940,976,980],{"__ignoreMap":88},[92,898,899],{"class":94,"line":95},[92,900,423],{"class":102},[92,902,903,905,907,910],{"class":94,"line":116},[92,904,785],{"class":159},[92,906,431],{"class":102},[92,908,909],{"class":109},"\"Select\"",[92,911,268],{"class":102},[92,913,914,916,918,920,922,925,927,930],{"class":94,"line":131},[92,915,797],{"class":159},[92,917,471],{"class":102},[92,919,556],{"class":159},[92,921,471],{"class":102},[92,923,924],{"class":159},"\"$bindState\"",[92,926,431],{"class":102},[92,928,929],{"class":109},"\"/form/country\"",[92,931,932],{"class":102}," } },\n",[92,934,935,938],{"class":94,"line":146},[92,936,937],{"class":159},"  \"watch\"",[92,939,444],{"class":102},[92,941,942,945,947,950,952,955,957,960,962,965,967,969,971,973],{"class":94,"line":153},[92,943,944],{"class":159},"    \"/form/country\"",[92,946,471],{"class":102},[92,948,949],{"class":159},"\"action\"",[92,951,431],{"class":102},[92,953,954],{"class":109},"\"loadCities\"",[92,956,234],{"class":102},[92,958,959],{"class":159},"\"params\"",[92,961,471],{"class":102},[92,963,964],{"class":159},"\"country\"",[92,966,471],{"class":102},[92,968,825],{"class":159},[92,970,431],{"class":102},[92,972,929],{"class":109},[92,974,975],{"class":102}," } } }\n",[92,977,978],{"class":94,"line":173},[92,979,654],{"class":102},[92,981,982],{"class":94,"line":179},[92,983,659],{"class":102},[11,985,986],{},"The AI defined the reactive dependency. Your action handler implements the actual business logic. The boundary stays clean.",[31,988,990],{"id":989},"cross-platform-one-catalog-many-renderers","Cross-platform: one catalog, many renderers",[11,992,993],{},"The same catalog definition drives multiple platform-specific renderers:",[995,996,997,1010],"table",{},[998,999,1000],"thead",{},[1001,1002,1003,1007],"tr",{},[1004,1005,1006],"th",{},"Platform",[1004,1008,1009],{},"Package",[1011,1012,1013,1024,1034,1044,1054],"tbody",{},[1001,1014,1015,1019],{},[1016,1017,1018],"td",{},"React (web)",[1016,1020,1021],{},[52,1022,1023],{},"@json-render/react",[1001,1025,1026,1029],{},[1016,1027,1028],{},"Vue 3",[1016,1030,1031],{},[52,1032,1033],{},"@json-render/vue",[1001,1035,1036,1039],{},[1016,1037,1038],{},"React Native",[1016,1040,1041],{},[52,1042,1043],{},"@json-render/react-native",[1001,1045,1046,1049],{},[1016,1047,1048],{},"Remotion (video)",[1016,1050,1051],{},[52,1052,1053],{},"@json-render/remotion",[1001,1055,1056,1059],{},[1016,1057,1058],{},"React PDF",[1016,1060,1061],{},[52,1062,1063],{},"@json-render/react-pdf",[11,1065,1066],{},"The Remotion and React PDF renderers are the most unexpected. The same generative pattern that produces a web dashboard can produce a video timeline or a PDF document. A spec that describes a video becomes a sequence of timed clips; a spec that describes a document becomes pages with headings and tables. The catalog abstraction holds across output formats because \"here are the components and their typed props\" is a universal contract.",[11,1068,1069,1070,1072],{},"State management is similarly unopinionated. Adapters exist for Redux, Zustand, Jotai, and XState. The ",[52,1071,871],{}," expressions resolve against whichever store adapter you configure — the spec doesn't care which library you use.",[31,1074,1076],{"id":1075},"_36-components-ready-to-use","36 components ready to use",[11,1078,1079,1080,1083],{},"For teams that don't want to build a catalog from scratch, ",[52,1081,1082],{},"@json-render/shadcn"," provides 36 pre-built component definitions covering the standard UI vocabulary: cards, tables, buttons, inputs, selects, dialogs, tabs, badges, and more. These are backed by shadcn/ui (Radix UI + Tailwind CSS), so the visual quality is production-ready without additional styling work.",[31,1085,1087],{"id":1086},"code-export-no-runtime-lock-in","Code export: no runtime lock-in",[11,1089,1090,1091,1094],{},"One design decision that stands out: json-render can export a generated spec as a standalone React project — complete with component files, styles, and ",[52,1092,1093],{},"package.json",". The exported code has no json-render runtime dependency.",[11,1096,1097],{},"This matters for adoption. If you build interfaces with json-render and later decide to stop using it, you're not locked in. For builder tools, this enables a clean workflow: AI generates a first draft via the generative UI, the developer takes over a static codebase.",[31,1099,1101],{"id":1100},"what-its-good-for","What it's good for",[11,1103,1104],{},"The pattern has clear advantages in specific scenarios:",[11,1106,1107,1110],{},[41,1108,1109],{},"Analytics dashboards where the layout depends on the question."," \"Show me sales by region\" and \"show me refund rates by product\" should produce different structures — different chart types, different KPI arrangements, different groupings. Templating can't cover this space adequately; AI with guardrails can.",[11,1112,1113,1116],{},[41,1114,1115],{},"Builder and low-code tools"," that need to generate UI previews from user descriptions. The AI produces a spec; the builder shows it; the user adjusts; export to code when done.",[11,1118,1119,1122],{},[41,1120,1121],{},"Personalized views"," where different users need genuinely different structure and emphasis around the same underlying data — not just filtered results, but different interfaces.",[11,1124,1125,1128],{},[41,1126,1127],{},"Automated content pipelines"," — the Remotion and React PDF renderers make the same generation pattern applicable to video and documents, not just web UIs.",[31,1130,1132],{"id":1131},"the-honest-limitations","The honest limitations",[11,1134,1135],{},"json-render is from Vercel Labs — experimental, not a supported product, API subject to change. Apache-2.0 licensed, so commercial use is clear.",[11,1137,1138,1139,1142],{},"The framework doesn't write your prompt engineering for you. Writing a catalog with ",[52,1140,1141],{},"description"," fields that actually guide the AI toward sensible structural choices is real design work. A poorly described catalog produces incoherent interfaces. The quality of the constraint shapes the quality of the output.",[11,1144,1145],{},"It's also not a full application framework. Authentication, data fetching, backend integration, and business logic are still yours to build. The catalog's action definitions tell AI what actions exist; implementing those actions is left to you.",[31,1147,1149],{"id":1148},"the-structural-insight-worth-keeping","The structural insight worth keeping",[11,1151,1152,1153,1156,1157,1160],{},"What json-render gets right is treating the AI as a ",[41,1154,1155],{},"layout engine"," rather than a ",[41,1158,1159],{},"code generator",". A layout engine operates within a defined vocabulary and produces structured output that humans can inspect and reason about. A code generator produces executable instructions — useful for developer tooling, dangerous when the output runs inside a live application without a human in the loop.",[11,1162,1163],{},"The flat spec format, the catalog-as-contract pattern, the declarative expression language, the streaming-first architecture — these are the result of thinking clearly about what AI is actually good at (choosing structure and composition from a defined vocabulary) versus what breaks when you give it free rein (producing executable code that integrates with a running system without verification).",[11,1165,1166],{},"For applications where the interface itself needs to respond to user intent — not just the data inside it — this is the cleanest approach to the problem I've seen so far.",[1168,1169],"hr",{},[11,1171,1172],{},[15,1173,1174,1175,1180,1181],{},"json-render is open source under Apache-2.0: ",[23,1176,1179],{"href":1177,"rel":1178},"https://github.com/vercel-labs/json-render",[27],"github.com/vercel-labs/json-render"," · Documentation and live playground: ",[23,1182,1184],{"href":25,"rel":1183},[27],"json-render.dev",[1186,1187,1188],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":88,"searchDepth":116,"depth":116,"links":1190},[1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201],{"id":33,"depth":116,"text":34},{"id":73,"depth":116,"text":74},{"id":404,"depth":116,"text":405},{"id":665,"depth":116,"text":666},{"id":767,"depth":116,"text":768},{"id":989,"depth":116,"text":990},{"id":1075,"depth":116,"text":1076},{"id":1086,"depth":116,"text":1087},{"id":1100,"depth":116,"text":1101},{"id":1131,"depth":116,"text":1132},{"id":1148,"depth":116,"text":1149},"2026-02-27","Vercel Labs ships a generative UI framework that solves the hardest problem in AI-driven interfaces: letting AI decide the layout while keeping developers in control of the components. A deep look at what makes it architecturally interesting.","md","en",{},"/blog/2026-02-27-json-render-generative-ui-framework",{"title":5,"description":1203},"blog/2026-02-27-json-render-generative-ui-framework",[1211,1212,1213,1214,1215,1216,1217],"AI","UI","Developer Tools","React","Generative UI","Vercel","Open Source","VFU93RvTxXCCDf2gYtigU9-hZu-DWl0wPKZhOQzqlQA",1784088102511]