@nativework/core

Native-feeling Vue 3 components for the web: gesture-driven sheets, drawers, reorderable lists, pull-to-refresh, and a Zod-validated form kit.
135
Install
npm install @nativework/core
Latest Version:0.1.0
Vue:3
License:MIT
Last Updated:Sep 13, 2026
Links: npm
Maintainer: matpulis

@nativework/core

npm bundle size types license

@nativework/core

A Vue 3 component framework for building native-like mobile experiences with web technology: gesture-driven sheets and drawers, reorderable lists, pull-to-refresh, a Zod-validated form kit, date and time pickers, and dialogs.

It is a plain Vue library with no native dependencies, which makes it a natural fit for Capacitor, Ionic or an installed PWA. It works just as well in a browser tab.

📖 Documentation · 🎮 Playground

  • Gestures, not approximations. Drag, long-press, fling and rubber-band behaviour built on pointer and touch events, with haptics where the platform allows.
  • Webview-ready. Safe areas, the hardware back button, keyboard insets and iOS's missing vibrate API are all accounted for.
  • Three config objects drive the type scale, every form control and every dialog. Override them globally or per subtree, reactively.
  • Validation is built in. Any control takes a Zod schema, standalone or inside a form.
  • Typed all the way through. ESM and CJS, tree-shakeable, and no preflight forced on your app.
Form controls with validation Date picker in a bottom sheet Destructive confirm dialog
Forms: Zod-validated controls Pickers: calendar and wheel Dialogs: pre-built and custom
Drag to reorder a list Grouped PIN input Type scale
Reorder: drag, auto-scroll, keyboard PinInput: grouped codes Typography: one type scale
pnpm add @nativework/core      # or npm install / yarn add

Requirements

Package Version Needed for
vue ^3.5 required
zod ^4 only if you validate with schemas
vue-router ^4 only for RouterTransition and useNavigation

lucide-vue-next comes as a dependency; the components use it for their icons.

Getting started

// main.ts
import '@nativework/core/style.css';
<script setup lang="ts">
import { ref } from 'vue';
import { ActionSheet, Button, TextField } from '@nativework/core';

const open = ref(false);
const email = ref('');
</script>

<template>
  <Button @click="open = true">Open sheet</Button>

  <ActionSheet v-model="open" mode="content">
    <div class="p-4">
      <TextField v-model="email" label="Email" type="email" clearable />
    </div>
  </ActionSheet>
</template>

Styles

@nativework/core/style.css ships the utilities the components use plus their scoped styles. It deliberately excludes Tailwind's preflight, so it will not reset your app.

Already on Tailwind v4 and would rather generate the utilities yourself? Skip the stylesheet and point Tailwind at the package instead:

@import "tailwindcss";
@source "../node_modules/@nativework/core/dist";

Components

Overlays: Dialog, DialogConfirm, DialogPrompt, DialogHost, DialogActions, ActionSheet, Drawer, PickerSheet

Gestures & motion: Reorder, PullToRefresh, RouterTransition

Typography: Text, Heading

Forms: Form, TextField, TextArea, ToggleSwitch, RadioGroup, Radio, Slider, PinInput, FieldShell

Date & time: DatePicker, TimePicker, DateTimePicker, Calendar, TimeWheel

Providers: TypographyProvider, FormsProvider, DialogProvider

Other: Button

Forms

Every control works three ways: bound to a form by name, standalone with v-model and its own schema, or fully controlled.

<script setup lang="ts">
import { z } from 'zod';
import { Button, Form, TextField, ToggleSwitch } from '@nativework/core';

const schema = z.object({
  email: z.email('Enter a valid email'),
  // false is a legitimate value, so "required" cannot express must-accept
  terms: z.literal(true, { message: 'Accept the terms' }),
});
</script>

<template>
  <Form :schema="schema" @submit="save">
    <template #default="{ isValid, isSubmitting }">
      <TextField name="email" label="Email" required />
      <ToggleSwitch name="terms" label="Accept the terms" required />
      <Button type="submit" :disabled="!isValid" :loading="isSubmitting">Save</Button>
    </template>
  </Form>
</template>

Own the controller when you need the state outside the template:

import { useForm } from '@nativework/core';

const form = useForm({ schema, initialValues, onSubmit });
// form.values / errors / touched / isValid / isDirty / isSubmitting / submitCount
// form.setValue() setError() validate() validateField() reset() handleSubmit()

form.setError('code', 'That code is not right') puts a server-side failure back on a field. Schema-level rules land on the right control too: a .refine() with path: ['party'] shows its message on the party field.

No form at all:

<TextField v-model="handle" :schema="z.string().min(3)" validate-on="input" />

Validation timing resolves field prop → form → config, defaulting to validate on blur and then re-check on every keystroke once a field has spoken up.

Dialogs

Three levels, depending on how much you want handed to you.

<!-- Pre-built -->
<DialogConfirm
  v-model="open"
  tone="destructive"        // default | destructive | warning | success
  actions="row"             // or "stacked"
  title="Delete note?"
  message="This cannot be undone."
  :on-confirm="deleteNote"  // promise: the button spins; a rejection keeps it open
/>

<!-- Your own layout; the shell keeps backdrop, focus trap, escape and scroll lock -->
<Dialog v-model="open" size="lg">
  <MyContent />
  <template #footer="{ close }"><Button @click="close">Done</Button></template>
</Dialog>

Awaited, with a single <DialogHost /> mounted near the app root:

const { alert, confirm, prompt } = useDialog();

if (await confirm({ title: 'Delete note?', tone: 'destructive' })) await remove();
const name = await prompt({ label: 'List name', required: true }); // string | null

Configuration

Nothing is hardcoded per component. Sizes, radii, colors, validation timing, haptics, the type scale and the calendar/wheel metrics all come from three config objects.

import { provideTypography, provideFormConfig, provideDialogConfig } from '@nativework/core';

// App.vue setup, reactive, applies everywhere
provideTypography(() => ({ scale: uiScale.value, defaults: { family: 'rounded' } }));
provideFormConfig(() => ({ size: 'lg', variant: 'outlined', validateOn: 'input' }));
provideDialogConfig(() => ({ backdropBlur: 0, labels: { confirm: 'Yes' } }));
<!-- Or scope it; nested providers merge onto their parent -->
<TypographyProvider :scale="1.2">...</TypographyProvider>
<FormsProvider :config="{ datetime: { use24Hour: true, weekStart: 0 } }">...</FormsProvider>

Import the defaults (typography, forms, dialog) to read or extend them.

scale is the Dynamic Type hook: one number resizes every piece of text. selectable defaults to false, because native body text doesn't select, opt in per element.

Overlays inside a container

Sheets, drawers and dialogs teleport to <body> by default. To keep them inside a container - a device frame, a modal, an embedded preview, point the portal at it and give that element a transform, so position: fixed resolves against it:

providePortalTarget(() => screenEl.value); // or per component: <Dialog teleport-to="#frame">

Composables

useForm, useField, useFormConfig, useControlBox, useTypography, useDialog, useDialogConfig, usePortalTarget, useNavigation, useNativeTransition, plus local-time date helpers: toISODate, toISOTime, toISODateTime, parseISODate, parseISODateTime, buildMonth, weekdayLabels, monthLabel, formatDate, localeUses24Hour, isSameDay, startOfDay, addMonths, clampDate.

The date helpers format by hand in local time, toISOString() shifts the date across midnight for anyone off UTC, which is the classic date-picker off-by-one.

TypeScript

Types ship with the package. They re-export from .vue modules, which resolves out of the box with vue-tsc / the Vue language server, the normal Vue setup. A plain tsc build needs "allowArbitraryExtensions": true.

Development

This package lives in a workspace alongside a playground app:

pnpm install
pnpm dev          # playground, resolving this package from source
pnpm build:lib    # build the package into packages/core/dist

License

MIT © Matthew Pulis

Related Packages

vue3-hand-mobile

Vue mobile gesture action event.

5,464
vue-native-router

Router for mobile apps

853 37
@norbiros/nuxt-auto-form

Auto-generate Nuxt UI forms from Zod 4 schemas

9,137 58
@iankibetsh/sh-tailwind

Vue 3 + Tailwind CSS v4 component library for Laravel backends, built on @iankib...

3,215 0
buildkit-primevue

FormKit components and helpers built on PrimeVue v4 with Tailwind CSS

6,986 0

Version History

Version Released Vue License
0.1.0 ^3.5.0 MIT