@zoobzio/vue

Vue 3 components for the foundation design system: unstyled interactive components composing reka-ui primitives and semantic HTML, exposed through passthrough and ctx-scoped slots. No Nuxt dependency.
Install
npm install @zoobzio/vue
Latest Version:0.0.6
Vue:3
License:MIT
Last Updated:Oct 26, 2026
Links: GitHub  ·  npm
Maintainer: zoobzio

@zoobzio/vue

Vue 3 components for the foundation design system. Every framework symbol is imported from vue, and the package ships its source (src/) through subpath exports, the same way the layer ships app/.

Each core component composes reka-ui primitives and semantic HTML into one unstyled widget (f-* classes), and exposes its interactive parts through one passthrough system: a part manifest (XPassthrough), the consumer's deep-partial pt prop, and the resolved settings the template binds. A single ctx view model serves both defineExpose and the ctx-scoped slots. The full contract is documented in the components README; select.vue is the reference implementation there and the first component here.

Usage

import Select from "@zoobzio/vue/components/select.vue";
import { defineSelect } from "@zoobzio/vue/definitions/select";
import type { SelectProps } from "@zoobzio/vue/types/components/select";
<script setup lang="ts">
const picker = defineSelect({
  options: [
    { value: "a", label: "Apple" },
    { value: "b", label: "Banana" },
  ],
  "onUpdate:modelValue": (option) => console.log(option.value),
});
</script>

<template>
  <Select v-bind="picker" />
</template>

Layout

src/
  components/        — interactive components (32) plus the stateless data components: the table (table.vue and its table-* parts), the form (form.vue and form-field.vue), the browser (browser.vue and its browser-* parts), the editor (editor.vue), and the code editor (code.vue)
  composables/       — usePassthrough, useForward, useModel, useContext, useSelection, useSearch, useScroller, useDrag, useKeywords, usePaginate, useServiceRefs, useServiceEmits, useEditor, useCode
  definitions/       — define<Name>(definition | source) constructors
  types/             — common, events, passthrough, forward, slots, context, definition; per-component types in core/
  utils/             — passthrough merge, forward render, define runtime, date, keywords, paginate, table, browser, editor, code
  constants/         — constants by domain: date, filter, pagination, popper, scroll, selection, form, table, browser, editor, code
tests/               — vitest + happy-dom, mirroring src/ path-for-path

Items that can navigate carry an optional link (LinkTarget: to, target) and render through a link part: a reka Primitive whose recipe is a plain anchor. The part takes LinkProps (as, asChild, and the anchor attributes), so pt.link swaps the element for a component such as a router link, which receives the same href and target.

<Menu :groups="groups" :pt="{ link: () => ({ as: NuxtLink }) }" />

Data components

table.vue renders a headless @zoobzio/table service. The component owns no feature state: it reads the service, calls its methods, and relays its updated and filtered events as emits. The service's state container must be reactive for the table to update.

import { reactive } from "vue";
import { TableService, createTableState } from "@zoobzio/table";
import Table from "@zoobzio/vue/components/table.vue";

const config = { columns, rowKey: "id" };
const state = reactive(createTableState(config));
const service = new TableService("users", config, state, { fetch });
service.init();
<Table :service="service" @updated="onUpdated" />

form.vue renders a headless @zoobzio/form service the same way: one form-field.vue per config field, each resolved to the control its type names (input, textarea, select, multiSelect, checkbox, radio, datePicker, tagsInput), with the lifecycle events (initialized, submitted, rejected, restored, reset, failed) relayed as emits. Field passthrough is keyed by payload key under pt.fields; a field slot given to the form overrides that region in every field.

import { reactive } from "vue";
import { FormService, createFormState } from "@zoobzio/form";
import Form from "@zoobzio/vue/components/form.vue";

const config = { title, fields, schema, defaults };
const state = reactive(createFormState(config));
const service = new FormService("profile", config, state, { submit });
<Form
  :service="service"
  :pt="{ fields: { name: { input: { autofocus: true } } } }"
  @submitted="onSubmitted"
/>

browser.vue renders a headless @zoobzio/browser service: a breadcrumb over the service's trail, then one headerless table with the folder rows (browser-folders.vue) above the file rows (browser-files.vue), each a tbody of its own so the columns line up. Folder clicks open, crumb clicks navigate, row and folder menus dispatch by key through runAction and runFolderAction, and the bulk actions bar runs runBulkAction over the selection. The updated and navigated events relay as emits. Cell slots cascade as the table's do (cell:<key>, cell:<type>, cell); noFiles fills the empty file section, empty replaces the whole body once a folder settles with neither folders nor files.

import { reactive } from "vue";
import { BrowserService, createBrowserState } from "@zoobzio/browser";
import Browser from "@zoobzio/vue/components/browser.vue";

const config = {
  columns,
  fileKey: "id",
  actions: { download: { label: "Download" } },
};
const state = reactive(createBrowserState());
const service = new BrowserService("assets", config, state, {
  fetch,
  actions: { download: (file) => save(file) },
});
service.init();
<Browser
  :service="service"
  :pt="{ files: { actionsTrigger: { label: 'More' } } }"
  @navigated="onNavigated"
>
  <template #folderIcon><FolderGlyph /></template>
</Browser>

editor.vue renders a headless @zoobzio/editor service: a toolbar with one button per mark and node the service's schema holds (bold, italic, headings by level, lists, quote, code block, rule, then undo and redo), over a host div that shows the document as static HTML. The editor is off until the user interacts: a pointer or keyboard focus on the host, or a toolbar click, mounts the service's editor over the host in place, so the content is readable from the first paint (server-rendered and hydrated untouched) and turns editable with no visible change. The static copy follows load while the editor is off. Each button runs the tool's command and reports aria-pressed from the service's active. The loaded, updated, mounted, and unmounted events relay as emits; data-mounted on the root says the editor is on. pt.tool is built per tool; the tool and toolIcon slots receive their tool; toolbar replaces the whole bar, and its context carries activate for a custom bar to turn the editor on.

useEditor(service, host) is the component's view model on its own, for a consumer that renders the editor its own way: markup, the document as static HTML that follows load while the editor is off and freezes at mount; tools, the toolbar as data; and activate, which mounts the editor over the host in place. Bind markup to the host's innerHTML, activate to its pointerdown and focus, and give the host a tabindex. The editor unmounts with the scope.

import { reactive } from "vue";
import { EditorService, createEditorState, hast } from "@zoobzio/editor";
import Editor from "@zoobzio/vue/components/editor.vue";

const config = { headings: [1, 2, 3] };
const state = reactive(createEditorState());
const service = new EditorService("body", config, state, hast);
service.load(tree);
<Editor
  :service="service"
  :pt="{ tool: (tool) => ({ title: tool.label }) }"
  @updated="onUpdated"
/>

code.vue renders a headless @zoobzio/code service: a host div that shows the text static in a pre, and a status line that reports the cursor as a line and a column. The editor is off until the user interacts: a pointer or keyboard focus on the host mounts the service's CodeMirror view in the host, in place of the static copy, so the text is readable from the first paint (server-rendered and hydrated untouched) and turns editable with the same text under a different DOM. A pointer places the caret where it pressed; keyboard focus starts at the top. The static copy follows load while the editor is off. The component decides nothing about the look of the code: the language, the theme, the highlight style, and the gutters are the service's extensions, the consumer's own. The loaded, updated, mounted, and unmounted events relay as emits; data-mounted on the root says the editor is on. The status slot replaces the status line, and its context carries activate.

useCode(service, host) is the component's view model on its own, for a consumer that renders the editor its own way: markup, the text in a pre that follows load while the editor is off and freezes at mount; and activate, which mounts the view in the host. Bind markup to the host's innerHTML, activate to its pointerdown and focus, and give the host a tabindex. The editor unmounts with the scope.

import { reactive } from "vue";
import { CodeService, createCodeState, text } from "@zoobzio/code";
import { markdown } from "@codemirror/lang-markdown";
import Code from "@zoobzio/vue/components/code.vue";

const state = reactive(createCodeState());
const service = new CodeService("readme", {}, state, {
  ...text,
  extensions: [markdown(), myTheme],
});
service.load(source);
<Code
  :service="service"
  :pt="{ status: { hidden: true } }"
  @updated="onUpdated"
/>

Related Packages

@zoobzio/nuxt

Nuxt layer for the foundation design system: the Nuxt wiring over @zoobzio/vue a...

0 0

Version History

Version Released Vue License
0.0.6 ^3.5.0 MIT
0.0.5 ^3.5.0 MIT
0.0.4 ^3.5.0 MIT
0.0.3 ^3.5.0 MIT
0.0.2 ^3.5.0 MIT
Pre-releases (1)
Version Released License
0.0.0-stage