Many developers are impressed by Generative‑UI capabilities of GenUI SDK at first trial. However, they quickly hit a real‑world problem: the pages generated by GenUI look great in demos, yet they clash with styles, color themes and interaction patterns of existing business systems once integrated into your project.
This raises a frequent question: Can GenUI SDK adapt its visual style to match my project’s own component library?
Answer: Yes. Starting from v1.3.0, GenUI SDK has achieved complete material decoupling. The framework no longer ships with built‑in components; instead, arbitrary component libraries are imported via standalone material packages. Official material packages are available for OpenTiny Vue, Element Plus and OpenTiny NG.
- Official Website: https://opentiny.design/genui-sdk
- GitHub Repository: https://github.com/opentiny/genui-sdk
This article walks you through building a custom material library for the Vue‑based Naive UI component library.
Why Build a Custom GenUI Material Library?
The frontend ecosystem hosts a rich variety of component libraries tailored for different project requirements. Many legacy projects adopt alternative libraries such as Naive UI, for which official GenUI materials are not yet available.
Furthermore, different systems prioritize distinct component sets: dashboard projects make heavy use of chart components, while data‑collection scenarios rely mostly on form widgets. In such cases, a trimmed‑down material library containing only components you actually need yields better generation quality and reduces prompt token consumption.
For our Naive‑UI example targeting login‑page generation, only simple form widgets and buttons are required. Limiting the exposed component scope shrinks prompt payload and improves both generation speed and output accuracy — exactly where custom material libraries add value.
Building a GenUI SDK Material Library for Naive UI
Creating a material library requires two core artifacts:
-
materials (Component Registry): Maps
componentNamefrom Schema to real runtime components. - meta (Component Specification / Metadata): Tells the LLM what components exist and what properties each component accepts.
Our minimal Naive‑UI material library will contain:
-
Form components:
NInput,NSelect,NButton -
Form containers:
NForm,NFormItem -
Card container:
NCard(default wrapper component) -
Icons: Encapsulated
NIconSvg(covered in Step 4), plus two icons:SearchOutline,CheckmarkOutline
Though compact, this set covers essential capabilities. Full demo source code is available in GitHub: opentiny/genui-sdk-demos. Below is a hands‑on step‑by‑step tutorial.
Project Structure
genui-materials-naive-ui/
├── src/
│ ├── index.ts # Package entry, exports meta & materials
│ ├── materials/
│ │ ├── index.ts # materials sub‑path entry
│ │ ├── materials.ts # Assemble IMaterials (component table + default‑value map)
│ │ └── components/
│ │ ├── index.ts
│ │ ├── components.ts # componentName → real‑component registry
│ │ └── NIconSvg.vue # Encapsulated icon component (Step 4)
│ └── meta/
│ ├── index.ts # meta sub‑path entry
│ ├── meta.ts # Assemble IMaterialsMeta (protocol + allow‑list)
│ ├── white-list.ts # componentName allow‑list for LLM usage
│ └── bundle.json # Component protocol descriptions (LLM specs)
├── test/ # Local test project
│ ├── main.ts
│ ├── App.vue # GenuiConfigProvider + GenuiRenderer integration test
│ └── fetch-schema-stream.ts # Stream LLM responses and parse Schema
├── index.html # Dev‑server entry page
├── vite.config.ts # Library‑mode build config
├── vite.test.config.ts # Local dev‑server config for testing
├── .env # LLM endpoint & API‑key for local development
└── package.json
Step 1: Initialize Project
mkdir genui-materials-naive-ui && cd genui-materials-naive-ui
npm init -y
npm install @opentiny/genui-sdk-core
npm install vue naive-ui @vicons/ionicons5
npm install -D typescript vite vite-plugin-dts @vitejs/plugin-vue @opentiny/genui-sdk-vue
Note:
@opentiny/genui-sdk-vueis used only for local testing and belongs indevDependencies, not production build artifacts.
package.json key fields (declare sub‑path exports for materials and meta):
{
"name": "genui-materials-naive-ui",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./materials": {
"types": "./dist/materials.d.ts",
"import": "./dist/materials.js"
},
"./meta": {
"types": "./dist/meta.d.ts",
"import": "./dist/meta.js"
}
},
"scripts": {
"build": "vite build",
"dev": "vite --config vite.test.config.ts"
},
"dependencies": {
"@opentiny/genui-sdk-core": "^1.3.0",
"@vicons/ionicons5": "^0.13.0",
"naive-ui": "^2.45.0",
"vue": "^3.5.32"
},
"devDependencies": {
"@opentiny/genui-sdk-vue": "^1.3.0",
"@vitejs/plugin-vue": "^6.0.6",
"typescript": "~5.9.3",
"vite": "^8.0.8",
"vite-plugin-dts": "^5.0.3"
}
}
Step 2: Component Registry (materials)
The renderer looks up real components by componentName via this registry.
src/materials/components/components.ts
import type { Component } from 'vue';
import { NButton, NCard, NForm, NFormItem, NInput, NSelect } from 'naive-ui';
export interface IComponents {
[key: string]: Component;
}
export const components: IComponents = {
NButton,
NCard,
NForm,
NFormItem,
NInput,
NSelect,
};
Icon component
NIconSvgwill be registered later in Step 4.
Assemble into the IMaterials structure expected by the renderer:
src/materials/materials.ts
import { buildMaterialDefaultValueMap, type IMaterials } from '@opentiny/genui-sdk-core';
import { materialsMeta } from '../meta';
import { components } from './components';
const requiredCompleteFieldSelectors: string[] = [];
export { components };
export const materials: IMaterials = {
components,
requiredCompleteFieldSelectors,
defaultPropsMap: buildMaterialDefaultValueMap(materialsMeta),
};
-
requiredCompleteFieldSelectors: Configures buffered fields rendered only after fully resolved. -
buildMaterialDefaultValueMap: Generates default‑property map frombundle.jsonfor graceful fallback during streaming rendering.
Step 3: Component Metadata Specification (meta)
This is the critical part: teaching the LLM how to use each component. Example for NInput in src/meta/bundle.json:
{
"data": {
"framework": "Vue",
"materials": {
"components": [
{
"name": {
"zh_CN": "Input"
},
"component": "NInput",
"description": "Accept character input via mouse or keyboard",
"npm": {
"package": "naive-ui",
"exportName": "NInput",
"destructuring": true
},
"schema": {
"properties": [
{
"name": "0",
"label": {
"zh_CN": "Basic Properties"
},
"content": [
{
"property": "modelValue",
"label": {
"text": {
"zh_CN": "Bound Value"
}
},
"description": {
"zh_CN": "Two‑way bound input value"
},
"required": true,
"type": "string",
"cols": 12
},
{
"property": "placeholder",
"label": {
"text": {
"zh_CN": "Placeholder Text"
}
},
"description": {
"zh_CN": "Input box placeholder hint"
},
"required": false,
"type": "string",
"cols": 12
}
]
}
],
"events": {
"onUpdate:modelValue": {
"label": {
"zh_CN": "Triggered when bound value changes"
},
"description": {
"zh_CN": "Fires on modification of bound input value"
}
}
}
}
}
]
}
}
}
Field explanations:
-
component: Component identifier, must exactly match keys inside component registry. -
name: Human‑readable display name for configuration panels. -
description: Component description consumed by LLM prompt generation. -
npm: Package metadata for import/code generation. -
schema.properties: Grouped property definitions for configuration panels. -
schema.events: Declare events available for LLM‑generated event bindings.
Allow‑list src/meta/white-list.ts
export const whiteList = [
'NInput',
'NSelect',
'NButton',
'NForm',
'NFormItem',
'NCard',
'div',
'span',
'Text',
];
Assemble metadata object src/meta/meta.ts
import type { IMaterialsMeta, IMaterialsProtocol } from '@opentiny/genui-sdk-core';
import bundleJson from './bundle.json' with { type: 'json' };
import { whiteList } from './white-list';
export const materialsMeta: IMaterialsMeta = {
materials: [bundleJson] as unknown as IMaterialsProtocol[],
wrapperComponent: 'NCard',
whiteList,
examples: [],
rules: [],
};
Export entry files:
src/materials/components/index.ts
export * from './components';
src/materials/index.ts
export * from './materials';
src/meta/index.ts
export * from './meta';
src/index.ts
export * from './meta';
export * from './materials';
Step 4: Adding Icon Materials
Icon components follow the same pattern as ordinary widgets, with one extra step: wrap an icon wrapper component mapping name prop to concrete icon assets. Refer to ElIconSvg from the official vue‑element‑plus material package.
- Create icon wrapper component
src/materials/components/NIconSvg.vue
<script setup lang="ts">
import { computed, type Component } from 'vue';
import * as Icons from '@vicons/ionicons5';
const props = withDefaults(defineProps<{ name: string }>(), { name: '' });
const iconComponent = computed(() => {
return (Icons as Record<string, Component | unknown>)[props.name] || null;
});
</script>
<template>
<component :is="iconComponent" v-if="iconComponent" />
</template>
- Register inside component registry
src/materials/components/components.ts
import NIconSvg from './NIconSvg.vue';
export const components: IComponents = {
NButton,
NCard,
NForm,
NFormItem,
NIconSvg,
NInput,
NSelect,
};
- Add component definition inside
bundle.json
{
"name": { "zh_CN": "Icon" },
"component": "NIconSvg",
"description": "Icon component; set name property e.g. SearchOutline, CheckmarkOutline",
"schema": {
"properties": [
{
"name": "0",
"label": { "zh_CN": "Basic Properties" },
"content": [
{
"property": "name",
"label": { "text": { "zh_CN": "Icon Name" } },
"description": { "zh_CN": "Icon identifier such as SearchOutline (search), CheckmarkOutline (checkmark)" },
"required": true,
"type": "string",
"cols": 12,
"widget": { "component": "SelectIconConfigurator", "props": {} }
}
]
}
]
}
}
- Append component name to
white-list.ts
export const whiteList = [
'NInput',
'NSelect',
'NButton',
'NForm',
'NFormItem',
'NCard',
'NIconSvg',
'div',
'span',
'Text',
];
Now the LLM can generate buttons with icons, e.g. place NIconSvg inside button icon slots.
Step 5: Build Configuration
Vite library‑mode build, export separate entry‑points for materials and meta, mark vue / naive‑ui as external dependencies.
vite.config.ts
import path from 'node:path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import vue from '@vitejs/plugin-vue';
import packageJson from './package.json';
export default defineConfig({
plugins: [vue(), dts()],
build: {
lib: {
entry: {
index: path.resolve(__dirname, './src/index.ts'),
materials: path.resolve(__dirname, './src/materials/index.ts'),
meta: path.resolve(__dirname, './src/meta/index.ts'),
},
formats: ['es'],
fileName: (_, entryName) => `${entryName}.js`,
},
sourcemap: true,
rollupOptions: {
external: [...Object.keys(packageJson.dependencies || {})],
},
},
});
Step 6: Local Validation
Before publishing, test locally to verify the LLM can generate valid Naive‑UI interfaces. The test/ directory contains a separate dev‑server setup.
vite.test.config.ts (local test server, port 5175)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
server: {
port: 5175,
open: true,
},
});
Root‑level index.html
<!doctype html>
<html lang="zh‑CN">
<head>
<meta charset="UTF‑8" />
<meta name="viewport" content="width=device‑width, initial‑scale=1.0" />
<title>genui‑materials‑naive‑ui · Test</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/test/main.ts"></script>
</body>
</html>
test/main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
test/App.vue (core integration page: user input → LLM schema → GenuiRenderer renders Naive‑UI UI)
<script setup lang="ts">
import { ref } from 'vue';
import { GenuiRenderer, GenuiConfigProvider } from '@opentiny/genui-sdk-vue';
import { genPrompt } from '@opentiny/genui-sdk-core';
import { components } from '../src/materials';
import { materialsMeta } from '../src/meta';
import { fetchSchemaStream } from './fetch-schema-stream';
const materials = { components };
const inputText = ref('');
const schema = ref<any>({ componentName: 'Page', children: [] });
const rendererKey = ref(0);
const generating = ref(false);
const systemPrompt = genPrompt('Vue', materialsMeta);
console.log('genPrompt output (first 200 chars):', systemPrompt.slice(0, 200));
const handleSend = async () => {
if (!inputText.value.trim() || generating.value) return;
generating.value = true;
schema.value = '';
rendererKey.value++;
const userInput = inputText.value;
inputText.value = '';
try {
await fetchSchemaStream(
import.meta.env.VITE_DEEPSEEK_API_URL,
import.meta.env.VITE_DEEPSEEK_API_KEY,
userInput,
systemPrompt,
(schemaChunk) => { schema.value += schemaChunk; }
);
} catch (error) {
console.error('Request failed:', error);
} finally {
generating.value = false;
}
};
</script>
<template>
<GenuiConfigProvider :materials="materials">
<div class="demo‑container">
<div class="input‑group">
<input v‑model="inputText" placeholder="Enter prompt, e.g.: generate a login form" @keyup.enter="handleSend" />
<button :disabled="generating" @click="handleSend">{{ generating ? 'Generating…' : 'Send' }}</button>
</div>
<GenuiRenderer :content="schema" :key="rendererKey" />
</div>
</GenuiConfigProvider>
</template>
<style scoped>
.demo‑container { padding: 16px; box‑sizing: border‑box; }
.input‑group { display: flex; gap: 8px; margin‑bottom: 16px; }
input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border‑radius: 4px; }
button { padding: 8px 16px; background: #1890ff; color: white; border: none; border‑radius: 4px; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: not‑allowed; }
</style>
test/fetch‑schema‑stream.ts — uses PatternExtractor to parse schemaJson chunks from streamed LLM responses
import { PatternExtractor } from '@opentiny/genui-sdk-core';
export async function fetchSchemaStream(
url: string,
apiKey: string,
userInput: string,
systemPrompt: string,
onSchemaUpdate: (schemaChunk: string) => void
): Promise<void> {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content‑Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userInput },
],
model: 'deepseek‑v4‑flash',
thinking: { type: 'disabled' },
stream: true,
}),
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const reader = response.body!.getReader();
const decoder = new TextDecoder('utf‑8');
let buffer = '';
const patternExtractor = new PatternExtractor({
onNormalWrite: () => {},
onHandledWrite: (value) => onSchemaUpdate(value),
});
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
while (true) {
const lineEndIndex = buffer.indexOf('\n');
if (lineEndIndex === -1) break;
const line = buffer.slice(0, lineEndIndex).trim();
buffer = buffer.slice(lineEndIndex + 1);
if (!line.startsWith('data:')) continue;
const dataStr = line.slice(5).trim();
if (dataStr === '[DONE]') return;
try {
const chunk = JSON.parse(dataStr);
const content = chunk.choices?.[0]?.delta?.content;
if (!content) continue;
patternExtractor.handleContent(content);
} catch (e) {
console.error('Parse backend data failed:', e, dataStr);
}
}
}
} finally {
reader.releaseLock();
}
}
Create .env file for LLM endpoint and credentials
VITE_DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
VITE_DEEPSEEK_API_KEY=sk‑your‑deepseek‑api‑key
Launch local dev server:
npm run dev
Browser opens http://localhost:5175. Input prompt such as generate a login form and observe Naive‑UI components rendered from LLM output.
Step 7: Publish
Once local testing passes you may rename the package and publish to npm registry.
npm run build
npm publish --access public
Integrating the Material Library Into an Application
Two‑step integration after building your custom material package.
Install dependency
npm install genui‑materials‑naive‑ui naive‑ui
Front‑end rendering: inject materials via GenuiConfigProvider
<script setup lang="ts">
import { GenuiChat, GenuiConfigProvider } from '@opentiny/genui-sdk-vue';
import { materials } from 'genui‑materials‑naive‑ui/materials';
</script>
<template>
<GenuiConfigProvider :materials="materials">
<GenuiChat />
</GenuiConfigProvider>
</template>
Server‑side prompt generation: feed materialsMeta into genPrompt
import { genPrompt } from '@opentiny/genui-sdk-core';
import { materialsMeta } from 'genui‑materials‑naive‑ui/meta';
const systemPrompt = genPrompt('Vue', materialsMeta);
You can now instruct the LLM within chat sessions to generate UI following Naive‑UI styling.
Three Practical Tips To Improve Material‑Library Quality
-
Write thorough descriptions: Detailed
descriptionfields for components and properties deliver the highest return on investment for improved LLM output accuracy. -
Supply example schemas: Populate
materialsMeta.exampleswith representative form schemas as reference templates for the LLM. -
Declare buffered fields: Add field‑path rules such as
[componentName=NSelect] > props > optionsinsidematerials.requiredCompleteFieldSelectorsfor more robust streaming rendering.
Accelerating Material‑Library Development With AI
Besides manual implementation you can leverage AI as a shortcut workflow:
- Supply the official TinyVue material‑library repository to your Agent: https://github.com/opentiny/genui-sdk/tree/dev/packages/materials/vue‑opentiny‑vue
- Provide component‑documentation links for your target library (e.g. Naive‑UI Button docs: https://www.naiveui.com/en‑US/light/components/button)
- Tell the Agent which components you intend to include, or describe your business scenarios for automatic component discovery.
- Ask the Agent to generate material metadata by referencing the official TinyVue material specs.
This approach drastically speeds up custom material‑library construction (the demo material‑library metadata was generated with AI assistance).
Summary
Material decoupling opens GenUI‑SDK’s component ecosystem to third‑party contributions. Whether you adopt Element Plus, Ant Design or internal private component libraries, integration follows this three‑part pattern:
Component mapping (
materials) + Component specification metadata (meta) + Runtime injection (ConfigProvider/genPrompt)
Full demo repository: https://github.com/opentiny/genui-sdk-demos/pull/1
If you build useful custom material libraries, feel free to share your work on GitHub or submit PRs for inclusion within official GenUI‑SDK materials.
About OpenTiny NEXT
OpenTiny NEXT is an enterprise‑grade intelligent frontend development solution built on Generative UI and WebMCP. It delivers intelligent upgrades for legacy products including the TinyVue component library and TinyEngine low‑code engine, while launching Agent‑oriented products such as NEXT‑SDKs for frontend, AI Extension, TinyRobot AI Assistant and GenUI. It enables AI to interpret user intentions and autonomously execute tasks, accelerating intelligent transformation of enterprise applications.
Join the OpenTiny Open‑Source Community
WeChat Assistant: opentiny‑official
- Official Website: https://opentiny.design
- GenUI SDK Repository: https://github.com/opentiny/genui-sdk (Star ⭐ appreciated)
If you want to contribute, look for issues tagged good first issue in repositories. Feel free to leave comments for questions and feedback!

Top comments (0)