diff --git a/packages/client/package.json b/packages/client/package.json
index 53814020d7..fefdacd888 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -27,6 +27,10 @@
   "engines": {
     "node": ">=18.0.0"
   },
+  "scripts": {
+    "build": "vite build",
+    "dev": "vite build --watch"
+  },
   "dependencies": {
     "@antfu/utils": "^0.7.10",
     "@iconify-json/carbon": "^1.1.36",
diff --git a/packages/client/uno.ts b/packages/client/uno.ts
new file mode 100644
index 0000000000..b959ab4424
--- /dev/null
+++ b/packages/client/uno.ts
@@ -0,0 +1,5 @@
+import '@unocss/reset/tailwind.css'
+import 'uno:preflights.css'
+import 'uno:typography.css'
+import 'uno:shortcuts.css'
+import 'uno.css'
diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts
new file mode 100644
index 0000000000..0b88012e59
--- /dev/null
+++ b/packages/client/vite.config.ts
@@ -0,0 +1,114 @@
+import { fileURLToPath } from 'node:url'
+import { basename } from 'node:path'
+import { defineConfig, normalizePath } from 'vite'
+import UnoCSS from 'unocss/vite'
+import type { ResolvedSlidevOptions, SlidevPluginOptions } from '@slidev/types'
+import IconsResolver from 'unplugin-icons/resolver'
+import Components from 'unplugin-vue-components/vite'
+import fg from 'fast-glob'
+import { createInspectPlugin } from '../slidev/node/vite/inspect'
+import { createIconsPlugin } from '../slidev/node/vite/icons'
+import { createVuePlugin } from '../slidev/node/vite/vue'
+
+const absolute = (path: string) => normalizePath(fileURLToPath(new URL(path, import.meta.url)))
+const clientRoot = absolute('.')
+
+const options = {
+  clientRoot,
+  roots: [],
+  mode: 'build',
+  inspect: true,
+} as unknown as ResolvedSlidevOptions
+
+const pluginOptions = {
+  components: {
+    dts: false,
+  },
+} as SlidevPluginOptions
+
+const defines = [
+  '__DEV__',
+  '__SLIDEV_CLIENT_ROOT__',
+  '__SLIDEV_HASH_ROUTE__',
+  '__SLIDEV_FEATURE_DRAWINGS__',
+  '__SLIDEV_FEATURE_EDITOR__',
+  '__SLIDEV_FEATURE_DRAWINGS_PERSIST__',
+  '__SLIDEV_FEATURE_RECORD__',
+  '__SLIDEV_FEATURE_PRESENTER__',
+  '__SLIDEV_FEATURE_PRINT__',
+  '__SLIDEV_FEATURE_WAKE_LOCK__',
+  '__SLIDEV_HAS_SERVER__',
+]
+
+const builtinComponents = Object.fromEntries(fg.sync('*', {
+  cwd: absolute('./builtin'),
+  absolute: true,
+}).map(i => [`components/${basename(i).replace(/\..*$/, '')}`, i]))
+
+const layoutComponents = Object.fromEntries(fg.sync('*', {
+  cwd: absolute('./layouts'),
+  absolute: true,
+}).map(i => [`layouts/${basename(i).replace(/\..*$/, '')}`, i]))
+
+const externals = [
+  '#slidev/',
+  '/@slidev/',
+  '@slidev/',
+  'server-reactive:',
+  'vue',
+  'vue-router',
+  'monaco-editor',
+  'typescript',
+  'mermaid',
+  '~icons/',
+]
+
+export default defineConfig({
+  plugins: [
+    createInspectPlugin(options, pluginOptions),
+    UnoCSS(),
+    createVuePlugin(options, pluginOptions),
+    Components({
+      extensions: ['vue', 'ts'],
+      dirs: [absolute('./builtin')],
+      resolvers: [
+        IconsResolver({
+          prefix: '',
+          customCollections: Object.keys(pluginOptions.icons?.customCollections || []),
+        }),
+      ],
+      dts: false,
+    }),
+    createIconsPlugin(options, pluginOptions),
+    {
+      name: 'slidev:flags',
+      enforce: 'pre',
+      transform(code, id) {
+        if (id.match(/\.vue($|\?)/)) {
+          const original = code
+          defines.forEach((name) => {
+            code = code.replaceAll(`_ctx.${name}`, name)
+          })
+          if (original !== code)
+            return code
+        }
+      },
+    },
+  ],
+  build: {
+    lib: {
+      entry: {
+        'main': absolute('./main.ts'),
+        'index': absolute('./index.ts'),
+        'uno.css': absolute('./uno.ts'),
+        ...builtinComponents,
+        ...layoutComponents,
+      },
+      formats: ['es'],
+    },
+    target: 'esnext',
+    rollupOptions: {
+      external: source => externals.some(i => source.startsWith(i)),
+    },
+  },
+})
diff --git a/packages/slidev/node/commands/shared.ts b/packages/slidev/node/commands/shared.ts
index c7f21f6c9c..69efd0ca13 100644
--- a/packages/slidev/node/commands/shared.ts
+++ b/packages/slidev/node/commands/shared.ts
@@ -66,7 +66,7 @@ export async function getIndexHtml({ mode, entry, clientRoot, roots, data }: Res
     head += `\n`
 
   main = main
-    .replace('__ENTRY__', toAtFS(join(clientRoot, 'main.ts')))
+    .replace('__ENTRY__', toAtFS(join(clientRoot, 'dist/main.js')))
     .replace('', head)
     .replace('', body)
 
diff --git a/packages/slidev/node/options.ts b/packages/slidev/node/options.ts
index a76e19635e..0c1e9ccca6 100644
--- a/packages/slidev/node/options.ts
+++ b/packages/slidev/node/options.ts
@@ -84,8 +84,8 @@ export async function createDataUtils(data: SlidevData, clientRoot: string, root
 
       const layouts: Record = {}
 
-      for (const root of [clientRoot, ...roots]) {
-        const layoutPaths = fg.sync('layouts/**/*.{vue,ts}', {
+      for (const root of [path.join(clientRoot, 'dist'), ...roots]) {
+        const layoutPaths = fg.sync('layouts/**/*.{vue,ts,js}', {
           cwd: root,
           absolute: true,
           suppressErrors: true,
diff --git a/packages/slidev/node/virtual/styles.ts b/packages/slidev/node/virtual/styles.ts
index 5cdc262927..c9c3fefe4c 100644
--- a/packages/slidev/node/virtual/styles.ts
+++ b/packages/slidev/node/virtual/styles.ts
@@ -17,6 +17,7 @@ export const templateStyle: VirtualModuleTemplate = {
       `import "${resolveUrlOfClient('styles/code.css')}"`,
       `import "${resolveUrlOfClient('styles/katex.css')}"`,
       `import "${resolveUrlOfClient('styles/transitions.css')}"`,
+      `import "${resolveUrlOfClient('dist/style.css')}"`,
     ]
 
     for (const root of roots) {
diff --git a/packages/slidev/node/vite/compilerFlagsVue.ts b/packages/slidev/node/vite/compilerFlagsVue.ts
index 7d70a38afe..26b4174789 100644
--- a/packages/slidev/node/vite/compilerFlagsVue.ts
+++ b/packages/slidev/node/vite/compilerFlagsVue.ts
@@ -15,10 +15,10 @@ export function createVueCompilerFlagsPlugin(
       name: 'slidev:flags',
       enforce: 'pre',
       transform(code, id) {
-        if (id.match(/\.vue($|\?)/)) {
+        if (id.match(/\.vue($|\?)/) || id.includes('client/dist')) {
           const original = code
           define.forEach(([from, to]) => {
-            code = code.replace(new RegExp(from, 'g'), to)
+            code = code.replaceAll(from, to)
           })
           if (original !== code)
             return code
diff --git a/packages/slidev/node/vite/components.ts b/packages/slidev/node/vite/components.ts
index b7dfe2abdb..2ce8b01fc2 100644
--- a/packages/slidev/node/vite/components.ts
+++ b/packages/slidev/node/vite/components.ts
@@ -11,7 +11,7 @@ export function createComponentsPlugin(
     extensions: ['vue', 'md', 'js', 'ts', 'jsx', 'tsx'],
 
     dirs: [
-      join(clientRoot, 'builtin'),
+      join(clientRoot, 'dist/components'),
       ...roots.map(i => join(i, 'components')),
     ],
 
diff --git a/packages/slidev/node/vite/extendConfig.ts b/packages/slidev/node/vite/extendConfig.ts
index 8a02be1c09..a5067565c8 100644
--- a/packages/slidev/node/vite/extendConfig.ts
+++ b/packages/slidev/node/vite/extendConfig.ts
@@ -68,7 +68,7 @@ export function createConfigPlugin(options: ResolvedSlidevOptions): Plugin {
   })
   return {
     name: 'slidev:config',
-    async config(config) {
+    async config() {
       const injection: InlineConfig = {
         define: getDefine(options),
         resolve: {
@@ -188,7 +188,7 @@ export function createConfigPlugin(options: ResolvedSlidevOptions): Plugin {
         injection.root = options.cliRoot
       }
 
-      return mergeConfig(injection, config)
+      return injection
     },
     configureServer(server) {
       // serve our index.html after vite history fallback