Skip to content
ShineUI
Install
ShineUI / Bundlers

Bundlers and app frameworks

Each setup below was built for production and run in a browser with a button, a select and a dialog.

Two builds of the same elements

@shineui/core ships the elements twice. The loader registers every element at once and fetches an element's code the first time the page uses it. The components build has one module per element, so a bundler takes only the elements you import, and you register each one yourself. The React, Vue and Angular packages use the components build and register their elements for you.

// The loader: every element, fetched on first use import { defineCustomElements } from "@shineui/core/loader"; defineCustomElements(); // The components build: only what you import, registered by you import { defineCustomElement as defineButton } from "@shineui/core/components/sui-button.js"; defineButton();

Importing a module from the components build registers nothing until you call its defineCustomElement. Use one build per page: an app that runs the loader beside a binding ships both. Either way, import the token sheet, @shineui/core/shineui.css, once.

Vite

Vite needs no configuration. Import the sheets and register the elements in your entry module, as Get started shows for plain HTML, React, Vue and Angular. Both builds work in the dev server and in vite build, and so do the React and Vue templates of create vite, type check included.

webpack 5

webpack needs a rule for CSS, because the sheets are imported from JavaScript, and a rule for the font file the optional fonts sheet points to.

npm i @shineui/core npm i -D webpack webpack-cli css-loader mini-css-extract-plugin html-webpack-plugin
// webpack.config.js const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { entry: "./src/main.js", module: { rules: [ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }, { test: /\.woff2$/, type: "asset/resource" }, ], }, plugins: [new MiniCssExtractPlugin(), new HtmlWebpackPlugin({ template: "./src/index.html" })], };
// src/main.js import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; import "@shineui/core/shineui-fonts.css"; // optional: DM Sans defineCustomElements();

With the loader, webpack gives every element a chunk of its own, about two hundred small files, and the browser fetches only those the page uses. To ship the elements inside your bundle instead, import each one from the components build and call its defineCustomElement.

Next.js

With the App Router, import the sheets in the root layout and use the components from @shineui/react. They carry "use client", so a Server Component can render them and pass them server data as props. A function cannot cross from a Server Component to a client one, so handlers and state live in your own client components: put "use client" at the top of the file that holds onClick, onSuiChange or useState.

npx create-next-app@latest my-app cd my-app npm i @shineui/react @shineui/core
// app/layout.tsx import type { ReactNode } from "react"; import "@shineui/core/shineui.css"; import "@shineui/core/shineui-fonts.css"; // optional: DM Sans export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ); }
// app/page.tsx, a Server Component import { SuiCard } from "@shineui/react"; import { RolePicker } from "./role-picker"; export default async function Page() { const roles = await loadRoles(); return ( <SuiCard heading="Invite"> <RolePicker roles={roles} /> </SuiCard> ); }
// app/role-picker.tsx "use client"; import { useState } from "react"; import { SuiSelect } from "@shineui/react"; export function RolePicker({ roles }: { roles: string[] }) { const [role, setRole] = useState(""); return ( <> <SuiSelect label="Role" options={roles} onSuiChange={(e) => setRole(String(e.detail))} /> <p>{role}</p> </> ); }

On the server and while the page hydrates, the components render nothing, and the elements draw in the render after, so the page hydrates with no mismatch. Until then a ref to one is null. Write the components, not raw sui- tags: a raw tag reaches the server HTML, draws before React hydrates and ends in React error 418. The React tab of Frameworks explains why. Turbopack, the default, and next build --webpack both work.

Nuxt

Add the sheets to nuxt.config and use the components from @shineui/vue in any page, with server rendering on. A component renders nothing on the server and while the page hydrates. The element draws, with the content of its slots, once the page has mounted, so hydration reports no mismatch.

npm i @shineui/vue @shineui/core
// nuxt.config.ts export default defineNuxtConfig({ css: ["@shineui/core/shineui.css", "@shineui/core/shineui-fonts.css"], });
<!-- pages/team.vue, server-rendered --> <script setup lang="ts"> import { ref } from "vue"; import { SuiButton, SuiDialog, SuiSelect } from "@shineui/vue"; const roles = ["Viewer", "Editor", "Admin"]; const role = ref(""); const paying = ref(false); </script> <template> <h1>Team</h1> <SuiSelect label="Role" :options="roles" v-model="role" /> <SuiButton label="Record payment" @click="paying = true" /> <SuiDialog heading="Record payment" :open="paying" @sui-close="paying = false"> <p>Paid in full.</p> </SuiDialog> </template>

Why the elements wait for the browser: they draw into their own light DOM as soon as their code runs. Markup they drew on the server would sit among the nodes Vue rendered, and Vue would report Hydration completed but contains mismatches and replace the content of each slot. The page’s own markup still comes from the server. Where the space the elements take matters, reserve it with a min-height on their container. <ClientOnly>, a .client.vue component and routeRules with ssr: false still work, and none is needed. v-model, props and events work as in any Vue app; the Vue tab of Frameworks has them.

Angular CLI

The Angular CLI's application builder, which runs on esbuild and is the default for a new project, needs no configuration. Import the sheets in styles.css and the directives in each standalone component, the value accessor beside the element it serves.

ng new my-app cd my-app npm i @shineui/angular @shineui/core
/* src/styles.css */ @import "@shineui/core/shineui.css"; @import "@shineui/core/shineui-fonts.css"; /* optional: DM Sans */
import { Component } from "@angular/core"; import { FormsModule } from "@angular/forms"; import { SuiButton, SuiDialog, SuiSelect, SelectValueAccessor } from "@shineui/angular"; @Component({ selector: "app-root", imports: [FormsModule, SuiButton, SuiDialog, SuiSelect, SelectValueAccessor], template: ` <sui-button variant="accent" label="Record payment" (click)="paying = true"></sui-button> <sui-select label="Role" [options]="roles" [(ngModel)]="role"></sui-select> <sui-dialog heading="Record payment" [(open)]="paying"> <p>Paid in full.</p> </sui-dialog> `, }) export class App { roles = ["Viewer", "Editor", "Admin"]; role = ""; paying = false; }

ng serve and ng build both work, zoneless as a new project starts. The Angular tab of Frameworks covers the seven value accessors, two-way binding and hydration.

No bundler

A page with no build step can load the lazy build from a CDN with three tags, as Get started shows. To load a few elements instead, map the components build with an import map and register each element yourself. Each module in that build imports only its own files, by relative path, so one entry in the map is all the browser needs.

<link rel="stylesheet" href="https://unpkg.com/@shineui/core@1/dist/shineui/shineui.css"> <script type="importmap"> { "imports": { "@shineui/core/components/": "https://unpkg.com/@shineui/core@1/dist/components/" } } </script> <sui-button variant="accent" label="Pay bills"></sui-button> <script type="module"> import { defineCustomElement } from "@shineui/core/components/sui-button.js"; defineCustomElement(); </script>

Under a content security policy, the import map and an inline module each need the page's nonce, or their hash, in script-src. A module loaded from a file of your own needs neither.

Checked with

Every setup above was built for production, served, and opened in Chrome 148, where a button drew in the theme, a select took a pick that the app heard, and a dialog opened, took focus and closed on Escape. The versions: Vite 8.3 (vanilla, React 19 and React 18, Vue 3.5), webpack 5.111, Next.js 16.4 with React 19.3, Nuxt 3.21 and 4.5, and the Angular CLI 21.2. An Angular app on each of Angular 20, 21 and 22 is also built and run on every commit.