@lovince/ui-vue

中文文档
0.5.0发布于 2026/11/02MITTypeScriptES Module

Accessible headless Vue 3 components for Lovince UI — Disclosure, Checkbox, Tabs and Dialog, unstyled.

原始使用文档
文档目录11 节
  1. Components
  2. Primitives
  3. Button
  4. Icon
  5. Forms
  6. Shared conventions
  7. Lovince-specific behaviour
  8. Building blocks
  9. State attributes
  10. Known limitations
  11. License

@lovince/ui-vue

Accessible, unstyled Vue 3 components for Lovince UI. Every component is a state machine from @lovince/ui-headless; its browser effects run through @lovince/ui-dom. This package subscribes to the machine, feeds props in, and renders what connect() returns. It holds no behaviour of its own.

Status: 0.5.0, experimental. The API may change before 1.0.

bun add @lovince/ui-vue

Requires Vue 3.5+ (for SSR-stable useId). No CSS ships with the package. Style it with classes and the data-* state attributes, or use @lovince/themes for tokens.

Every component export carries the Lv prefix (LvButton, LvDialogRoot), so names never clash with your own components or native elements.

Components

GroupComponentParts
PrimitivesText, Heading, Label, Link, Code, Kbd, Box, AspectRatio, VisuallyHidden, Separator
IconLvIcon, from the separate @lovince/ui-vue/icon entry
DisclosureDisclosureLvDisclosureRoot, LvDisclosureTrigger, LvDisclosureContent
AccordionLvAccordionRoot, LvAccordionItem, LvAccordionHeader, LvAccordionTrigger, LvAccordionContent
TabsLvTabsRoot, LvTabsList, LvTabsTrigger, LvTabsContent
OverlaysDialogLvDialogRoot, LvDialogTrigger, LvDialogPortal, LvDialogOverlay, LvDialogContent, LvDialogTitle, LvDialogDescription, LvDialogClose
LightboxLvLightboxRoot, LvLightboxTrigger, LvLightboxPortal, LvLightboxOverlay, LvLightboxContent, LvLightboxTitle, LvLightboxItem, LvLightboxPrevious, LvLightboxNext, LvLightboxCounter, LvLightboxClose
PopoverLvPopoverRoot, LvPopoverTrigger, LvPopoverAnchor, LvPopoverPortal, LvPopoverContent, LvPopoverClose
TooltipLvTooltipRoot, LvTooltipTrigger, LvTooltipPortal, LvTooltipContent
Hover cardLvHoverCardRoot, LvHoverCardTrigger, LvHoverCardPortal, LvHoverCardContent
MenuLvDropdownMenuRoot, LvDropdownMenuTrigger, LvDropdownMenuContent, LvDropdownMenuItem, LvDropdownMenuCheckboxItem, LvDropdownMenuRadioGroup, LvDropdownMenuRadioItem, LvDropdownMenuItemIndicator, LvDropdownMenuLabel, LvDropdownMenuGroup, LvDropdownMenuSeparator, LvContextMenuTrigger
Navigation menuLvNavigationMenuRoot, LvNavigationMenuList, LvNavigationMenuItem, LvNavigationMenuTrigger, LvNavigationMenuContent, LvNavigationMenuLink
ToastLvToastViewport, LvToastRoot, LvToastTitle, LvToastDescription, LvToastAction, LvToastClose, plus toast()
ChoiceCheckbox, SwitchLvCheckboxRoot, LvCheckboxIndicator, LvSwitchRoot, LvSwitchThumb
Radio groupLvRadioGroupRoot, LvRadioGroupItem, LvRadioGroupIndicator
Toggle, toggle groupLvToggleRoot, LvToggleGroupRoot, LvToggleGroupItem
SelectLvSelectRoot, LvSelectTrigger, LvSelectValue, LvSelectContent, LvSelectItem, LvSelectItemText, LvSelectItemIndicator, LvSelectGroup, LvSelectLabel, LvSelectSeparator
ComboboxLvComboboxRoot, LvComboboxAnchor, LvComboboxInput, LvComboboxTrigger, LvComboboxClear, LvComboboxContent, LvComboboxItem, LvComboboxItemIndicator, LvComboboxEmpty
Command paletteLvCommandRoot, LvCommandInput, LvCommandList, LvCommandGroup, LvCommandItem, LvCommandEmpty, LvCommandSeparator
Emoji pickerLvEmojiPickerRoot, LvEmojiPickerSearch, LvEmojiPickerList, LvEmojiPickerGroup, LvEmojiPickerItem, LvEmojiPickerNav…
InputSliderLvSliderRoot, LvSliderTrack, LvSliderRange, LvSliderThumb
Angle SliderLvAngleSliderRoot, LvAngleSliderTrack, LvAngleSliderMarkers, LvAngleSliderIndicator, LvAngleSliderThumb, LvAngleSliderValue
Number fieldLvNumberFieldRoot, LvNumberFieldInput, LvNumberFieldIncrement, LvNumberFieldDecrement
PIN inputLvPinInputRoot, LvPinInputInput
CalendarLvCalendarRoot, LvCalendarHeading, LvCalendarPrev, LvCalendarNext, LvCalendarGrid, LvCalendarHeadCell, LvCalendarCell, LvCalendarDay
FormsField, Input, Textarea, Form, TagsInput, FileUpload, TimeField, PhoneInputLvFieldRoot, LvFieldLabel, LvTagsInputRoot, LvTagsInputInput, LvTagsInputItem, LvTagsInputRemove, LvFileUploadRoot, LvFileUploadInput, LvFileUploadItem, LvFileUploadRemove, LvTimeFieldRoot, LvTimeFieldInput, LvPhoneInputRoot, LvPhoneInputInput, LvPhoneInputCountrySelect
Clock PickerLvClockPickerRoot, LvClockPickerViewTrigger, LvClockPickerMeridiem, LvClockPickerDial, LvClockPickerHand
LayoutScrollAreaLvScrollAreaRoot, LvScrollAreaViewport, LvScrollAreaScrollbar, LvScrollAreaThumb
SplitterLvSplitterGroup, LvSplitterPanel, LvSplitterHandle
TreeLvTreeRoot, LvTreeItem, LvTreeGroup
CarouselLvCarouselRoot, LvCarouselViewport, LvCarouselSlide, LvCarouselPrevious, LvCarouselNext, LvCarouselIndicators, LvCarouselIndicator
SortableLvSortableRoot, LvSortableItem, LvSortableHandle
SectionLvSectionRoot, LvSectionHeader, LvSectionEyebrow, LvSectionTitle, LvSectionDescription, LvSectionContent
DisplayAvatar, ProgressLvAvatarRoot, LvAvatarImage, LvAvatarFallback, LvProgressRoot, LvProgressIndicator
Gauge, CountdownLvGauge, LvCountdownRoot, LvCountdownSegment
TableLvTableContainer, LvTableRoot, LvTableCaption, LvTableHeader, LvTableBody, LvTableFooter, LvTableRow, LvTableHead, LvTableCell, plus LvDataTable

Primitives

The primitives provide semantic HTML and stable styling hooks without imposing CSS. LvText and LvHeading expose a data-text-role matching the typography roles in @lovince/tokens; heading level controls semantics independently from visual size.

<script setup lang="ts">
import { LvAspectRatio, LvCode, LvHeading, LvKbd, LvLabel, LvLink, LvSeparator, LvText, LvVisuallyHidden } from '@lovince/ui-vue'
</script>

<template>
  <LvHeading :level="1" size="display-md">Workspace</LvHeading>
  <LvText variant="body-md">Manage projects and members.</LvText>
  <LvLabel for="query">Search</LvLabel>
  <LvLink href="/guide">Read the guide</LvLink>
  <LvCode>bun test</LvCode>
  <LvKbd>Ctrl+K</LvKbd>
  <LvAspectRatio :ratio="16 / 9">
    <img src="/product.webp" alt="Product detail" />
  </LvAspectRatio>
  <LvVisuallyHidden>Search projects</LvVisuallyHidden>
  <LvSeparator :decorative="false" />
</template>

LvBox, LvAspectRatio, LvText, LvHeading, LvLabel, LvLink, LvCode, LvKbd, and LvVisuallyHidden support as and asChild. LvAspectRatio defaults to a 1:1 frame; set ratio to a positive finite number such as 16 / 9 to size its content region. LvLink defaults to an anchor and leaves navigation attributes such as href, target, and rel under the caller's control. LvCode and LvKbd default to their corresponding semantic elements. When LvHeading renders a non-heading element, it keeps its semantic level with role="heading" and aria-level. LvVisuallyHidden applies the minimal inline clipping styles needed to remain available to assistive technology. A non-decorative LvSeparator exposes separator semantics and its orientation; decorative separators are hidden from the accessibility tree.

Button

LvButton is the one part that names a design decision: variant (solid, subtle, outline, ghost, danger) and size (sm, md, lg, icon) become data-variant and data-size, and your stylesheet decides what each of them looks like. What the component owns is the markup a hand-written button gets wrong.

<script setup lang="ts">
import { LvButton } from '@lovince/ui-vue'
</script>

<template>
  <LvButton @click="save">Save</LvButton>
  <LvButton type="submit" variant="solid" size="lg" :loading="saving">Save</LvButton>
  <LvButton as="a" href="/settings" variant="ghost" size="sm">Settings</LvButton>
  <LvButton :disabled="!valid" variant="outline">Publish</LvButton>
</template>
  • type defaults to button, so a button inside a form never submits by accident; ask for submit or reset explicitly.
  • disabled sets the native attribute when the element is a real button, and aria-disabled when it is not, since an anchor cannot be disabled.
  • loading sets aria-busy and keeps the control in the tab order — a request started from the keyboard stays visible to the keyboard — while swallowing clicks so the handler cannot fire twice.
  • Both states also appear as data-disabled and data-loading, and neither runs the onClick it was given. With asChild the element is yours, click handling included; the state attributes still arrive.

Icon

LvIcon draws a glyph from @lovince/icons on the registry's shared 24-unit canvas. It is its own entry because the icon registry is an optional peer: importing @lovince/ui-vue loads no icon data, and an app that brings its own icons needs nothing extra.

<script setup lang="ts">
import { LvIcon } from '@lovince/ui-vue/icon'
</script>

<template>
  <LvIcon name="arrowRight" />
  <LvIcon name="success" :size="16" :stroke-width="1.5" label="Completed" />
</template>

An icon is decorative — aria-hidden="true" — unless it is given a label, which makes it role="img" with that accessible name. absoluteStrokeWidth keeps the rendered stroke thickness constant as size grows; fill takes 0 to 1 for a partial wash; rotate and flip turn about the canvas centre. Size and stroke fall back to the registry's own defaults, and an unknown name renders an empty box rather than breaking layout.

Forms

LvFieldRoot provides a stable input id and wires the label, description, and error into LvInput or LvTextarea. Description and error copy can be passed as props or named slots. Set invalid to expose an error slot, or pass error to mark the field invalid automatically. required and disabled are propagated to the field control; each control can override them.

<script setup lang="ts">
import { LvFieldLabel, LvFieldRoot, LvForm, LvInput, LvTextarea } from '@lovince/ui-vue'
</script>

<template>
  <LvForm @submit.prevent="save">
    <LvFieldRoot required description="Use your work email.">
      <LvFieldLabel>Email</LvFieldLabel>
      <LvInput type="email" name="email" autocomplete="email" />
    </LvFieldRoot>

    <LvFieldRoot :invalid="Boolean(message)" :error="message">
      <LvFieldLabel>Project summary</LvFieldLabel>
      <LvTextarea name="summary" />
    </LvFieldRoot>

    <button type="submit">Save</button>
  </LvForm>
</template>

LvInput and LvTextarea support modelValue / defaultValue and emit update:modelValue. LvTextarea uses content-based field sizing where the browser supports it and retains a native rows fallback. LvForm is a native form wrapper: browser constraint validation and submit events remain in control. Set submitting to expose aria-busy and data-submitting, or noValidate when validation is handled elsewhere.

TagsInput and FileUpload expose their current values and rejected entries through the root's default slot. Their pure rules and rejection reasons live in @lovince/ui-headless.

<script setup lang="ts">
import {
  LvFileUploadInput,
  LvFileUploadItem,
  LvFileUploadRemove,
  LvFileUploadRoot,
  LvTagsInputInput,
  LvTagsInputItem,
  LvTagsInputRemove,
  LvTagsInputRoot,
} from '@lovince/ui-vue'
</script>

<LvTagsInputRoot v-model="emails" :max="5" :validate="isEmail" name="emails">
  <LvTagsInputItem v-for="(email, index) in emails" :key="email" :index="index">
    {{ email }} <LvTagsInputRemove />
  </LvTagsInputItem>
  <LvTagsInputInput label="Add email address" />
</LvTagsInputRoot>

<LvFileUploadRoot v-model="files" accept="image/*,.pdf" :max-files="4" :max-size="5000000">
  <LvFileUploadInput />
  <LvFileUploadItem v-for="(file, index) in files" :key="file.name" :index="index">
    <template #default="{ file, size }">
      {{ file.name }} ({{ size }}) <LvFileUploadRemove />
    </template>
  </LvFileUploadItem>
</LvFileUploadRoot>

Tags are created with Enter or a configured separator; pasted separator-delimited text is split, and duplicate, invalid, and over-limit values emit rejected. File selection and dropping share the same type, per-file size, count, and total size checks. LvFileUploadRoot emits accepted File[] values and rejected files with their reason; LvFileUploadItem also exposes kind, extension, and localized size to its slot.

TimeField accepts human-entered times and stores normalized HH:mm values. It validates optional min and max bounds on blur or Enter, and exposes invalid when parsing or range validation fails. PhoneInput formats a national number on blur, recognizes supported international prefixes, and emits E.164 values. It includes a country selector and accepts an optional custom country list.

<LvTimeFieldRoot v-model="appointment" min="09:00" max="17:00" required name="appointment">
  <LvTimeFieldInput label="Appointment time" />
</LvTimeFieldRoot>

<LvPhoneInputRoot v-model="phone" country="KE" name="phone">
  <LvPhoneInputCountrySelect />
  <LvPhoneInputInput label="Mobile number" />
</LvPhoneInputRoot>

ScrollArea keeps scrolling native while exposing custom, keyboard-operable scrollbar parts. Size the root from its container, then render the viewport and the scrollbar/ thumb parts inside it. The root slot reports current overflow metrics; style the track and thumb with their data-scroll-area-* attributes. The viewport keeps its native scroll behavior and uses scrollbar-width: none where that property is supported.

<LvScrollAreaRoot class="results">
  <LvScrollAreaViewport>
    <div v-for="result in results" :key="result.id">{{ result.title }}</div>
  </LvScrollAreaViewport>
  <LvScrollAreaScrollbar>
    <LvScrollAreaThumb />
  </LvScrollAreaScrollbar>
</LvScrollAreaRoot>

Composables: useDataTable, useVirtualList, useInView, useScrollEdges, useActiveSection, useClipboard, plus the building blocks below.

The package is sideEffects: false. An app importing only LvDialogRoot ships no menu, select or table code, and a test enforces this.

Shared conventions

  • as and asChild. Every part renders a sensible element and takes as to change it. With asChild, the part renders your single child instead, merging its attributes and handlers in.
  • Controlled or not. Leave v-model off and the component owns its state. Bind it and your value is the source of truth. Every accepted change is emitted, and setting a model to undefined clears it.
  • Your ids are respected. Give any part an id and the ARIA references elsewhere follow it.
  • Exit animations play. Closing content stays mounted, with data-state="closed", until its CSS animation ends. With no animation, or reduced motion, it unmounts at once.
  • Hydration-safe portals. Overlays teleport only after mount, so server and client renders always agree.
  • Cancellable dismissal. Overlays emit escapeKeyDown and pointerDownOutside (Dialog also focusOutside) before closing. Call event.preventDefault() to keep them open.

Lovince-specific behaviour

Two behaviours differ from the Doitrix components these were ported from:

  • Disclosure content and tab panels stay in the DOM with hidden while closed, so find-in-page and aria-controls keep working.
  • Tabs select the first enabled tab when no value is given, including during server rendering.

Modal dialogs also make the rest of the page inert, not just focus-trapped.

Building blocks

Every component in this package is built the same way:

const machine = useMachine(dialogMachine, initialContext, {
  controlled: { open: () => props.open },          // a v-model, synced in
  onChange: (next, previous) => emit(…),           // accepted changes, never echoes of props
  beforeSend: dismiss.filter,                      // e.g. cancellable dismissal
})
const api = computed(() => connectDialog(machine.snapshot.value, machine.send))
// h(Primitive, normalizeProps(api.value.contentProps), slots)
  • useMachine starts effects on mount (none on the server), stops them on unmount, keeps a controlled value the source of truth, and provides the query that connect reads the page through. Components never touch the DOM themselves.
  • normalizeProps turns the engine's neutral props (onKeyDown) into Vue's (onKeydown). A React adapter would ship its own, against the same machines.
  • useListEnv reads a list's items from the DOM, in visual order, for machines that navigate lists.
  • Primitive, Slot, Portal, usePresence, useIdWatch handle rendering concerns: as/asChild, hydration-safe portals, exit animations, consumer ids.

State attributes

AttributeValues
data-stateopen / closed, checked / unchecked / indeterminate, active / inactive
data-disabled, data-readonlypresent when set
data-orientationhorizontal / vertical
data-side, data-alignwhere floating content was placed after collision handling

Floating content carries data-anchor-hidden while its anchor is scrolled out of view, and exposes --lovince-floating-available-height, --lovince-floating-available-width and --lovince-anchor-width. Disclosure content exposes --lovince-collapsible-height and -width for height animations.

Known limitations

  • Positioning is the engine's own (see @lovince/ui-headless): it respects scrolling containers and transformed ancestors, and hides content whose anchor has scrolled away (data-anchor-hidden). Arrows are not built in.
  • See @lovince/ui-dom for inert and scroll-lock browser caveats.

License

MIT

版本与发布记录

@lovince/ui-vue 共发布 11 个版本,首次发布于 2026/10/16;过去 12 个月发布了 11 个版本。

版本发布日期距今
0.5.0latest2026/11/021 天前
0.4.02026/11/021 天前
0.3.02026/11/011 天前
0.2.02026/11/012 天前
0.1.12026/10/1618 天前
0.1.02026/10/1618 天前

关于 @lovince/ui-vue 的常见问题

@lovince/ui-vue 是什么?

可访问的无头Vue 3组件为Lovince UI -披露,复选框,选项卡和对话框,无样式。

如何安装 @lovince/ui-vue?

在项目目录运行 npm install @lovince/ui-vue,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:vue(^3.5.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@lovince/ui-vue 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@lovince/ui-vue 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@lovince/ui-vue 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@lovince/ui-vue 可以商用吗?

@lovince/ui-vue 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@lovince/ui-vue 还在维护吗?

最新版本 0.5.0 发布于 2026/11/02(1 天前);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/10/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。