English | 中文
SoybeanUI is an elegant, modern, accessible and high-quality UI component library with shadcn-like design for Vue 3, built on top of a robust headless foundation. It provides a comprehensive set of accessible, customizable, and performant components.
SoybeanUI is built on a strict two-layer separation model:
┌─────────────────────────────────────────┐
│ @soybeanjs/ui (packages/ui/) │
│ S-prefixed components (SButton…) │
│ UnoCSS classes · @soybeanjs/cva │
│ provideXUi(ui) ──────────────────┐ │
└────────────────────────────────────┼────┘
│ style injection
┌────────────────────────────────────▼────┐
│ @soybeanjs/headless (packages/headless/) │
│ Logic · State · A11y · Keyboard nav │
│ useUiContext() reads injected classes │
│ Zero styles — works with any CSS │
└─────────────────────────────────────────┘
| Package | Role | Components |
|---|---|---|
| @soybeanjs/headless | Logic, state, a11y. Zero styles. | 95 component dirs, 25 composables |
| @soybeanjs/ui | Styled wrappers. UnoCSS + @soybeanjs/cva recipes. |
91 S-prefixed components |
Data flow is strictly one-way: headless → src. The styled layer never imports from headless's internals — it injects style tokens via provideXUi(computedUi) which headless components read through useUiContext().
Some multi-slot headless components also expose Compact aggregators, such as AccordionCompact and TableCompact. They keep item iteration and default content/icon composition inside headless, while the UI layer stays focused on styling and prop forwarding.
Current Compact-style coverage also includes flows such as card, date-field, dialog, editable, hover-card, layout, navigation-menu, pagination, popover, and stepper, when those structures are stable enough to live in headless.
Every multi-slot headless component exposes a provide{Name}Ui function. The styled wrapper computes classes with @soybeanjs/cva recipes and injects them:
// In the styled wrapper (packages/ui/)
const ui = computed(() => accordionVariants({ size: props.size }, props.ui, { root: props.class }));
provideAccordionUi(ui); // headless reads this via useAccordionUi()ThemeColor— 8 semantic colors:primary·destructive·success·warning·info·carbon·secondary·accentThemeSize— 6 sizes:xs·sm·md·lg·xl·2xl(base 16px atmd)ConfigProvider— sets globaldir,locale,nonce, and defaulttooltipconfig for the entire component tree, including RTL layout switching
ConfigProvider supports the following locale bundles:
| Code | Language |
|---|---|
zh-CN |
Simplified Chinese |
zh-TW |
Traditional Chinese |
en |
English |
ar |
Arabic |
ja |
Japanese |
ko |
Korean |
de |
German |
fr |
French |
es |
Spanish |
pt-BR |
Portuguese (Brazil) |
ru |
Russian |
tr |
Turkish |
id |
Indonesian |
Only en and zh-CN are pre-registered by default. registerLocale supports two registration styles:
- Pass a
LocaleRegistryobject. Built-in locale files from@soybeanjs/headless/locale/{code}already export this shape, includingdirmetadata. - Pass a locale key plus
LocaleMessagesfor a lightweight custom locale.
The shorthand registerLocale(key, messages) form uses the key as the locale name and falls back to ltr. Use the object form when you need explicit metadata such as rtl.
import { en, registerLocale } from '@soybeanjs/headless/locale';
import type { LocaleMessages } from '@soybeanjs/headless/locale';
import ar from '@soybeanjs/headless/locale/ar';
registerLocale(ar);
const customMessages: LocaleMessages = {
...en.messages,
pagination: {
...en.messages.pagination,
nextPage: 'Next →',
prevPage: '← Prev'
}
};
registerLocale('custom', customMessages);@soybeanjs/headless ships fine-grained sub-paths:
import { AccordionRoot } from '@soybeanjs/headless'; // all components
import { useControllableState } from '@soybeanjs/headless/composables'; // 25 composables
import { transformPropsToContext } from '@soybeanjs/headless/shared'; // pure TS utils
import { createMonth } from '@soybeanjs/headless/date'; // shared date helpers
import * as Headless from '@soybeanjs/headless/namespaced'; // namespace object
import type { AccordionUiSlot } from '@soybeanjs/headless/accordion'; // per-component
import type { UiClass } from '@soybeanjs/headless/types'; // shared type surface@soybeanjs/ui exports:
import { SButton, SAccordion } from '@soybeanjs/ui'; // all components
import '@soybeanjs/ui/styles.css'; // pre-built UnoCSS stylesheet
// Also: @soybeanjs/ui/nuxt · @soybeanjs/ui/resolverIf you contribute new public components, exports, or API descriptions, keep generated surfaces in sync through the official scripts instead of editing generated files by hand.
pnpm sui headless # sync headless component names and namespaced exports
pnpm sui ui # sync ui component names
pnpm sui api # regenerate docs api json and locale baseline data
pnpm sui api-locales # refresh api locale template data only
pnpm sui changelog # regenerate docs changelog json and locale baseline data
pnpm sui api-translate -- --locale zh-CN
pnpm sui changelog-translate -- --locale zh-CNThe docs site now renders component docs through UsageCode, PlaygroundGallery, and ComponentApi. Component detail pages and /releases also read generated changelog data from apps/docs/src/generated/changelog/ and apps/docs/src/generated/changelog-locales/.
Public API or demo delivery changes should keep docs, playground examples, and generated API data aligned. Changelog mapping, release presentation, and changelog locale template changes should keep generated changelog data aligned as well.
If you want ready-to-use components with a modern design:
pnpm add @soybeanjs/uiIf you want to build your own design system from scratch:
pnpm add @soybeanjs/headless-
Import Styles
Import the CSS file in your main entry file (e.g.,
main.ts):
import '@soybeanjs/ui/styles.css';-
Global Registration (Optional)
You can register components globally or import them on demand.
-
On-demand Import (Recommended)
We recommend using
unplugin-vue-componentsfor auto-importing components.
// vite.config.ts
import Components from 'unplugin-vue-components/vite';
import UiResolver from '@soybeanjs/ui/resolver';
export default defineConfig({
plugins: [
Components({
resolvers: [UiResolver()]
})
]
});- Nuxt Module
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@soybeanjs/ui/nuxt']
});The headless components provide the functionality without the styles.
For data-driven multi-slot patterns, prefer the exported Compact variant when it exists. It is the headless entry point for opinionated composition, while the regular parts remain available for fully manual assembly.
<script setup>
import { AccordionRoot, AccordionItem, AccordionTrigger, AccordionContent } from '@soybeanjs/headless';
</script>
<template>
<AccordionRoot>
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent>
</AccordionItem>
</AccordionRoot>
</template>- Accessible: Follows WAI-ARIA patterns for roles, focus management, and keyboard navigation.
- RTL ready: Switch supported components between LTR and RTL layouts with
ConfigProvider. - Headless-first: Logic and styles are fully separated — use
@soybeanjs/headlessalone to build any design system. - Type Safe: Written in strict TypeScript. All props, emits, slots, and context values are typed.
- Customizable at every level: Override individual slot classes via the
uiprop, or swap the entire style layer. - Lightweight & Tree-shakable: Import only the components you use. Each component is individually tree-shakable.
- Nuxt ready: First-class Nuxt module with auto-registration (
@soybeanjs/ui/nuxt). - unplugin support: Auto-import resolver for
unplugin-vue-components(@soybeanjs/ui/resolver).
We welcome contributions of all kinds! Please read CONTRIBUTING.md for setup instructions, coding conventions, and the pull request process.