cetera-vue-utils

Vue 3 components, composables and utilities
21,474
Install
npm install cetera-vue-utils
Latest Version:0.4.13
Vue:3
Last Updated:Oct 16, 2026
Links: npm
Maintainer: nicodim99

cetera-vue-utils

Components, composables, and utilities for Vue 3.

Playground →

Table of Contents

Components

Composables

Installation

npm install cetera-vue-utils

The package requires Vue as a peer dependency:

npm install vue

Nuxt

Add the module to nuxt.config.ts:

export default defineNuxtConfig({
    modules: ['cetera-vue-utils/nuxt'],
})

The module automatically:

  • registers all components globally
  • adds composables to auto-imports: useNotify, useFormElements, useErrors, useDataLoad

Since the components use Tailwind CSS classes, add a @source directive to your CSS entry point so Tailwind scans the library's dist files. Also import the library CSS (required for InputDate and InputTime datepicker styles):

/* app/assets/css/main.css */
@import "tailwindcss";
@import "cetera-vue-utils/style.css";
@source "../../../node_modules/cetera-vue-utils/dist/";

Note: cetera-vue-utils/style.css resolves to dist/style.css, which contains the compiled component CSS including the @vuepic/vue-datepicker base styles needed by InputDate and InputTime. The @source directive ensures your project's Tailwind generates the primary-* and other utility classes used by the components.

The Notifications component must be placed manually in the root layout:

<!-- app/app.vue -->
<script setup>
import { Notifications } from 'cetera-vue-utils'
</script>

<template>
    <Notifications />
    <NuxtLayout>
        <NuxtPage />
    </NuxtLayout>
</template>

Components

Notifications

Displays active notifications. Place once in the root layout.

Styled with Tailwind CSS — requires Tailwind to be configured in your project.

<script setup>
import { Notifications, useNotify } from 'cetera-vue-utils'

const notify = useNotify()
</script>

<template>
  <Notifications />
  <button @click="notify.success('Done!')">Save</button>
</template>

Notifications appear in the top-right corner with enter/leave animations.

Spinner

Loading overlay on top of content. Requires position: relative on the parent element.

<template>
  <div class="relative">
    <Spinner :isLoading="loading" size="lg" />
    <p>Content</p>
  </div>
</template>

<script setup>
import { Spinner } from 'cetera-vue-utils'
</script>
Prop Type Default Description
isLoading boolean true Show spinner
size 'xs' | 'sm' | 'base' | 'lg' | 'xl' 'xl' Icon size

InlineLoading

Inline loading indicator with optional text.

<template>
  <InlineLoading :isLoading="loading" loadingText="Saving..." />
</template>

<script setup>
import { InlineLoading } from 'cetera-vue-utils'
</script>
Prop Type Default Description
isLoading boolean true Show indicator
loadingText string 'Loading ...' Text next to icon

Button

Button with style variants, sizes, and loading state.

<template>
  <Button label="Save" @click="save" />
  <Button label="Loading" :loading="true" />
  <Button label="Secondary" severity="secondary" />
  <Button label="Delete" severity="danger" :icon="TrashIcon" />
  <Button label="Outlined" outlined />
  <Button label="Small" size="sm" />
  <Button label="Large" size="lg" />
</template>

<script setup>
import { Button } from 'cetera-vue-utils'
import { TrashIcon } from '@heroicons/vue/24/outline'
</script>
Prop Type Default Description
label string — Button text
severity 'primary' | 'secondary' | 'danger' 'primary' Color variant
outlined boolean false Outline style (transparent background)
size 'sm' | 'md' | 'lg' 'md' Button size
loading boolean false Spinner + disabled state
icon FunctionalComponent — Icon (hidden when loading)
Slot Description
default Button content (alternative to label prop)

All other attrs (type, disabled, etc.) are forwarded to <button>.

ButtonLink

Button styled as a link (<a>) with the same style variants.

<template>
  <ButtonLink href="/dashboard">Dashboard</ButtonLink>
  <ButtonLink href="https://example.com" target="_blank" severity="secondary">External link</ButtonLink>
  <ButtonLink href="/docs" outlined size="sm">Docs</ButtonLink>
</template>

<script setup>
import { ButtonLink } from 'cetera-vue-utils'
</script>
Prop Type Default Description
label string — Text (alternative to slot)
severity 'primary' | 'secondary' | 'danger' 'primary' Color variant
outlined boolean false Outline style
size 'sm' | 'md' | 'lg' 'md' Size
icon FunctionalComponent — Icon
Slot Description
default Link content (alternative to label prop)

All attrs (href, target, rel, etc.) are forwarded to <a>.

Dialog

Dialog window. Modal by default (based on @headlessui/vue, with backdrop and focus trap); with :modal="false" it becomes a floating window that leaves the page usable.

<template>
  <Button label="Open" @click="visible = true" />
  <Dialog v-model:visible="visible" header="Title">
    <p>Dialog content</p>
    <template #footer>
      <Button label="Cancel" severity="secondary" @click="visible = false" />
      <Button label="Save" @click="save" />
    </template>
  </Dialog>
</template>

<script setup>
import { ref } from 'vue'
import { Dialog, Button } from 'cetera-vue-utils'

const visible = ref(false)
</script>

Custom header via slot:

<Dialog v-model:visible="visible">
  <template #header>
    <h2 class="text-xl font-bold text-red-600">Warning!</h2>
  </template>
  Content
</Dialog>
Prop Type Description
visible (v-model) boolean Dialog visibility
header string Title (alternative to #header slot)
contentClass string Extra class for the content wrapper <div>
headerClass string Extra class for the header. Non-modal windows default to bg-gray-50 and have a 1px border-gray-300 frame
modal boolean Modal dialog (default true). false — window without backdrop and focus trap, page stays interactive
draggable boolean Drag the window by its header (default false)
Slot Description
default Dialog content
header Custom header (overrides header prop)
footer Action buttons (right-aligned)

Attrs (e.g. class) are forwarded to DialogPanel — use to override width:

<Dialog v-model:visible="visible" header="Wide dialog" class="max-w-3xl">...</Dialog>

Non-modal draggable window:

<Dialog v-model:visible="visible" :modal="false" draggable header="Change history" class="w-[80vw]">
  ...
</Dialog>

Non-modal windows are teleported to <body>, stack in the z-40…z-49 range (dropdowns, notifications and modal dialogs at z-50 stay above them) and are raised to the front on click.

A non-modal window hides together with the part of the UI it belongs to and comes back unchanged: inside a TabPanel it follows the active tab, inside <KeepAlive> it follows the active page.

Escape closes the topmost open non-modal window — the page does not have to be clicked first. The closed window marks the event as handled (preventDefault), so a page-level Escape handler can skip it with event.defaultPrevented.

A draggable window can be moved past the edges of the viewport, but its header always stays on screen — fully in height and at least 80px in width — so the window can be dragged back.

ConfirmAction

Confirmation dialog. Controlled via ref and the confirm(id) method.

<template>
  <Button label="Delete" severity="danger" @click="confirmRef?.confirm(item.id)" />
  <ConfirmAction ref="confirmRef" danger @confirm="onDelete">
    Are you sure you want to delete this record?
  </ConfirmAction>
</template>

<script setup>
import { ref } from 'vue'
import { ConfirmAction, Button } from 'cetera-vue-utils'

const confirmRef = ref()
const onDelete = (id) => {
  // delete record with id
}
</script>
Prop Type Description
danger boolean Renders the confirm button in red
Event Payload Description
confirm id: any Fired after user confirms
Method Description
confirm(id) Opens the dialog and stores id to pass to the event
Slot Description
default Confirmation message body

Input

Wrapper around an input field with label, validation errors, and helper text support.

<template>
  <Input label="Email" v-model="email" :invalidMessage="errors.get('email')" />

  <!-- With a custom component -->
  <Input label="Date" :component="InputDate" v-model="date" />

  <!-- With helper text -->
  <Input label="Password" v-model="password" helperText="Minimum 8 characters" />
</template>

<script setup>
import { Input, InputDate } from 'cetera-vue-utils'
</script>
Prop Type Default Description
label string — Field label
labelPosition 'top' | 'left' 'top' Label position: above the field or inline to the left
component Component InputText Input component to render
invalidMessage string — Validation error text
helperText string — Helper text below the field
id string auto uuid ID for label/input association
size 'sm' | 'small' | 'md' | 'lg' — Controls padding/text size (sm: px-2 py-1 text-sm, md/default: px-3 py-2, lg: px-4 py-3 text-lg)
Slot Props Description
default { invalid: boolean } Replaces the input element entirely
invalid-message — Custom validation error content
helper-text — Custom helper text content

Checkbox

Checkbox with label. Supports indeterminate state.

<template>
  <Checkbox v-model="checked" label="I agree to the terms" />
  <Checkbox v-model="value" label="Option" :value="'option1'" name="group" />
  <Checkbox v-model="checked" label="Partial" :indeterminate="true" />
</template>

<script setup>
import { Checkbox } from 'cetera-vue-utils'
</script>
Prop Type Description
label string Text next to the checkbox
labelPosition 'right' | 'top' | 'left' 'right'
id string ID for label/input association (auto-generated if omitted)
value boolean | string | number Value when used in a group
name string Group name
disabled boolean Disable the control
indeterminate boolean Intermediate state
Event Payload Description
change value: boolean | string | number | undefined Emitted after each toggle

Radiobutton

Radio button with label.

<template>
  <Radiobutton v-model="selected" label="Option 1" value="a" name="group" />
  <Radiobutton v-model="selected" label="Option 2" value="b" name="group" />
</template>

<script setup>
import { Radiobutton } from 'cetera-vue-utils'
</script>
Prop Type Description
label string Text next to the button
labelPosition 'right' | 'top' | 'left' 'right'
value string | number This button's value
name string Group name
disabled boolean Disable the control

Toggle

Toggle switch with label.

<template>
  <Toggle v-model="enabled" label="Enable notifications" />
  <Toggle v-model="enabled" label="Active" labelPosition="right" />
</template>

<script setup>
import { Toggle } from 'cetera-vue-utils'
</script>
Prop Type Default Description
label string — Label text
labelPosition 'top' | 'right' 'top' Label position
disabled boolean — Disable the control

Select

Dropdown select backed by @headlessui/vue Listbox.

<template>
  <Select v-model="selected" label="Status" :options="statuses" />
</template>

<script setup>
import { Select } from 'cetera-vue-utils'

const statuses = [
  { name: 'Active', value: 'active' },
  { name: 'Archived', value: 'archived' },
]
</script>
Prop Type Default Description
options any[] — List of options
optionLabel string 'name' Field used for display
optionValue string 'value' Field used for value
optionGroupChildren string — Field containing child options — enables nested/grouped tree
optionGroupCollapsible boolean — Let groups be collapsed/expanded by clicking the group header (groups are expanded while filtering)
expanded (v-model:expanded) any[] — Keys of expanded groups: when set, only the listed groups are expanded. Group key is its optionValue field, or optionLabel if there is none. Requires optionGroupCollapsible
collapsed (v-model:collapsed) any[] — Keys of collapsed groups: when set, only the listed groups are collapsed. Ignored if expanded is set
placeholder string — Placeholder text
showClear boolean false Show clear button
filter boolean — Show search input inside the dropdown
filterFields string[] — Option fields to search across (defaults to optionLabel)
disabled boolean false Disable the control
size 'sm' | 'small' | 'md' | 'lg' — Input size
Event Payload Description
filter query: string Emitted on every keystroke in the filter input
Slot Props Description
option { option: any } Custom option rendering

Combobox

Free-text input with optional dropdown suggestions, backed by @headlessui/vue Combobox. The user can either pick a value from the list or type any arbitrary text.

<template>
  <Combobox v-model="value" label="City" :options="cities" placeholder="Type or select" />
</template>

<script setup>
import { Combobox } from 'cetera-vue-utils'

const cities = [
  { name: 'Moscow', value: 'msk' },
  { name: 'Saint Petersburg', value: 'spb' },
]
</script>
Prop Type Default Description
options any[] [] Suggestion list
optionLabel string 'name' Field used for display
optionValue string 'value' Field used as value when option is selected from list
mask string — Input mask (maska syntax, e.g. ##:## for time)
placeholder string '' Placeholder text
showClear boolean false Show clear button
disabled boolean false Disable the control
size 'sm' | 'small' | 'md' | 'lg' — Input size

modelValue behaviour: when the user selects from the dropdown, modelValue is set to option[optionValue]; when the user types freely and closes the dropdown without selecting, modelValue is set to the typed string (or null if empty).

Event Payload Description
filter query: string Emitted on every keystroke — use for server-side filtering
Slot Props Description
option { option: any } Custom option rendering

MultiSelect

Multiple selection backed by @headlessui/vue Listbox with multiple.

<template>
  <MultiSelect v-model="selected" label="Tags" :options="tags" selectedFirst />
</template>

<script setup>
import { MultiSelect } from 'cetera-vue-utils'
</script>
Prop Type Default Description
options any[] — List of options
optionLabel string 'name' Field used for display
optionValue string 'value' Field used for value
optionDisabled string — Option field that locks the option: it can be neither selected nor deselected; clear, Select all / Deselect all and group toggles leave it as is
optionGroupChildren string — Field containing child options — enables nested/grouped tree
optionGroupSelectable boolean — Make group headers clickable to select/deselect all children
optionGroupCollapsible boolean — Let groups be collapsed/expanded by the chevron in the group header (groups are expanded while filtering)
expanded (v-model:expanded) any[] — Keys of expanded groups: when set, only the listed groups are expanded. Group key is its optionValue field, or optionLabel if there is none. Requires optionGroupCollapsible
collapsed (v-model:collapsed) any[] — Keys of collapsed groups: when set, only the listed groups are collapsed. Ignored if expanded is set
placeholder string — Placeholder text
showClear boolean false Show clear button
selectAll boolean — Show Select all / Deselect all checkbox in the dropdown header
filter boolean — Show search input inside the dropdown
filterFields string[] — Option fields to search across (defaults to optionLabel)
selectedFirst boolean — Show selected options at the top of the list
selectedLabel (count: number) => string — Custom summary label when items are selected
disabled boolean — Disable the control
size 'sm' | 'small' | 'md' | 'lg' — Input size
panelMinWidth number — Dropdown width follows the field width but is never narrower than this value (px)
Event Payload Description
filter query: string Emitted on every keystroke in the filter input
Slot Props Description
option { option: any } Custom option rendering

Menu

Dropdown menu — a Popover wrapper with a vertically stacked list of MenuItem buttons. Each MenuItem closes the menu on click. Place <Menu> inside the trigger element (e.g. <Button>) for target=true (default).

<template>
  <Button label="Actions" outlined>
    <Menu placement="bottom-start">
      <MenuItem @click="edit">Edit</MenuItem>
      <MenuItem @click="remove">Delete</MenuItem>
    </Menu>
  </Button>
</template>

<script setup>
import { Button, Menu, MenuItem } from 'cetera-vue-utils'
</script>
Menu

Accepts the same positioning props as Popover.

Prop Type Default Description
target boolean | string | HTMLElement true Trigger element. true = parent element, false = programmatic only, string = CSS selector, HTMLElement = ref
placement 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'bottom' | 'top' 'bottom-start' Panel placement
contextMenu boolean false Open on right-click at cursor position instead of click
hover boolean false Open on mouseenter, close on mouseleave (with 80 ms grace period to move into the panel)
autoClose boolean true Close on any click inside the menu — useful when items don't have individual click handlers
Slot Description
default MenuItem components
Method Description
open(anchor?) Programmatically open. anchor can be a MouseEvent or HTMLElement
MenuItem

Renders a full-width <button>. Automatically closes the parent menu on click via inject.

Prop Type Default Description
disabled boolean — Native disabled — dims the item and disables interaction

Forwards all other attributes via v-bind="$attrs" (e.g. class, event handlers).

Slider

Range slider using a native <input type="range">.

<template>
  <Slider v-model="value" label="Volume" :min="0" :max="100" />
</template>

<script setup>
import { Slider } from 'cetera-vue-utils'
</script>

All Input props and native range attrs are forwarded.

Textarea

Multi-line text input with optional auto-resize.

<template>
  <Textarea v-model="text" label="Comment" rows="4" autoResize />
</template>

<script setup>
import { Textarea } from 'cetera-vue-utils'
</script>

All Input props are forwarded. Pass autoResize to grow the textarea with content.

InputDate

Date picker. Stores value as yyyy-MM-dd string.

<template>
  <InputDate v-model="date" label="Date of birth" min-date="2000-01-01" :max-date="today" />
</template>

<script setup>
import { InputDate } from 'cetera-vue-utils'

const date = ref<string | null>(null) // '2024-01-15'
const today = new Date()
</script>
Prop Type Default Description
modelValue string | null — Date as yyyy-MM-dd
minDate string | Date undefined Earliest selectable date
maxDate string | Date undefined Latest selectable date
size InputSize undefined Input size
showClear boolean false Show clear button
disabled boolean false Disables the picker

Requires date-fns (included as a dependency).

InputMonth

Month picker. Stores value as YYYY-MM string (e.g. "2025-06").

<template>
  <InputMonth v-model="month" label="Billing month" min-date="2020-01" :max-date="today" />
</template>

<script setup>
import { InputMonth } from 'cetera-vue-utils'

const month = ref<string | null>(null) // '2025-06'
const today = new Date()
</script>
Prop Type Default Description
modelValue string | null — Month as YYYY-MM
minDate string | Date undefined Earliest selectable month
maxDate string | Date undefined Latest selectable month
size InputSize undefined Field size
showClear boolean false Show clear button
disabled boolean false Disables the picker

InputTime

Time picker. Stores value as HH:mm string.

<template>
  <InputTime v-model="time" label="Start time" />
</template>

<script setup>
import { InputTime } from 'cetera-vue-utils'

const time = ref<string | null>(null) // '14:30'
</script>
Prop Type Default Description
modelValue string | null — Time as HH:mm
showClear boolean false Show clear button
disabled boolean false Disable the picker

InputFile

File upload with drag-and-drop support. Stores selected files as File[].

<template>
  <InputFile v-model="files" name="docs" label="Documents" multiple upload-max-filesize="10 MB" />
</template>

<script setup>
import { InputFile } from 'cetera-vue-utils'

const files = ref([])
</script>
Prop Type Default Description
modelValue File[] [] Selected files
name string — Input name attribute
label string undefined Field label
multiple boolean false Allow multiple files
disabled boolean false Disable input
uploadMaxFilesize string undefined Max size hint shown below the dropzone

InputFileImage

Image upload with drag-and-drop, preview, and delete button. Accepts .jpg, .jpeg, .png, .webp.

<template>
  <InputFileImage v-model="imageUrl" v-model:file="imageFile" name="avatar" label="Avatar" />
</template>

<script setup>
import { InputFileImage } from 'cetera-vue-utils'

const imageUrl = ref(null)  // base64 data URL of the selected image
const imageFile = ref(null) // raw File object
</script>
Prop Type Default Description
modelValue string | null undefined Base64 data URL of the image
file (v-model) File | null undefined Raw File object
name string 'files' Input name attribute
label string undefined Field label
disabled boolean false Disable input
invalidMessage string undefined Validation error message
uploadMaxFilesize string undefined Max size hint shown below the dropzone
Slot Description
invalid-message Custom validation error content

InputNumber

Numeric input using a native <input type="number">.

<template>
  <InputNumber v-model="amount" label="Amount" />
</template>

<script setup>
import { InputNumber } from 'cetera-vue-utils'
</script>

All Input props are forwarded.

InputPassword

Password field with show/hide toggle.

<template>
  <InputPassword v-model="password" label="Пароль" placeholder="Введите пароль" />
</template>

<script setup>
import { ref } from 'vue'
import { InputPassword } from 'cetera-vue-utils'

const password = ref('')
</script>

All Input props (label, invalidMessage, helperText, etc.) are supported.

InputColor

Color field: a swatch that opens the browser's color picker plus an editable #rrggbb code. The model receives the color in lowercase #rrggbb only once the typed code is complete; an incomplete code reverts on blur.

<template>
  <InputColor v-model="color" label="Цвет" />
</template>

<script setup>
import { ref } from 'vue'
import { InputColor } from 'cetera-vue-utils'

const color = ref('#3b82f6')
</script>

All Input props (label, invalidMessage, helperText, etc.) are supported.

InputSearch

Search field with icon, clear button, and debounce. Requires @heroicons/vue and @vueuse/core.

<template>
  <InputSearch v-model="query" :debounce="500" :loading="isSearching" placeholder="Search..." />
</template>

<script setup>
import { InputSearch } from 'cetera-vue-utils'
</script>
Prop Type Default Description
modelValue string '' Search value
debounce number 800 Delay before emit (ms)
loading boolean false Shows spinner instead of clear icon
size 'sm' | 'small' | 'md' | 'lg' — Controls padding/text size

InputText

Base text input. Used by Input and the other input components.

<template>
  <InputText v-model="name" />

  <!-- Debounced model updates -->
  <InputText v-model="name" :debounce="500" />
</template>

<script setup>
import { InputText } from 'cetera-vue-utils'
</script>
Prop Type Default Description
modelValue string | number | null — Input value
fluid boolean false Makes the input full width
invalid boolean false Applies error styling
size 'sm' | 'small' | 'md' | 'lg' — Controls padding/text size
debounce number — Delay before emitting update:modelValue (ms). Omit/0 to emit immediately
showClear boolean false Shows a clear (✕) button when the field has a value
clearValue string | number | null '' Value emitted when the clear button is clicked

Tabs

Tab component based on provide/inject. Tab styling is fully controlled via Tailwind.

<template>
  <Tabs v-model:value="activeTab">
    <TabList class="border-b border-gray-200 gap-0 mb-4">
      <Tab
        v-for="tab in tabs"
        :key="tab.value"
        :value="tab.value"
        :class="[
          'px-4 py-2 text-sm font-medium border-b-2 -mb-px transition',
          activeTab === tab.value
            ? 'border-primary-500 text-primary-600'
            : 'border-transparent text-gray-500 hover:text-gray-700',
        ]"
      >
        {{ tab.label }}
      </Tab>
    </TabList>

    <TabPanel value="one"><p>First tab</p></TabPanel>
    <TabPanel value="two"><p>Second tab</p></TabPanel>
  </Tabs>
</template>

<script setup>
import { ref } from 'vue'
import { Tabs, TabList, Tab, TabPanel } from 'cetera-vue-utils'

const activeTab = ref('one')
</script>
Component Prop Description
Tabs value (v-model) Active tab value
Tabs lazy Mount TabPanel content only when first activated
Tabs keepHeight Inactive panels use visibility:hidden instead of display:none. Use with <TabPanels> wrapper — it automatically becomes a CSS grid that overlaps all panels, so the container height equals the tallest panel.
Tab value Unique tab identifier
Tab as HTML tag or component (default: button)
TabPanel value Shown when it matches Tabs.value
TabPanels — Optional wrapper for keepHeight mode; auto-applies CSS grid to overlay all panels

TabList is a role="tablist" wrapper that accepts any classes. Tab sets aria-selected automatically. The class attribute on Tab is forwarded to the overflow MenuItem, so layout classes like flex items-center gap-2 apply in both contexts.

TabList props:

Prop Type Default Description
activeVisible boolean false When true, selecting a hidden tab from the overflow menu moves it to the first visible position; also highlights the "More" button border when the active tab is hidden
orderable boolean false Enables drag & drop reordering of tabs (see below). Turns off activeVisible repositioning, which would break drag positions
placement 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'bottom' | 'top' 'bottom-end' Placement of the overflow dropdown menu

TabList events:

Event Payload Description
reorder (string | number)[] New order of tab values after a drag. Fired only with orderable

TabList slots:

Slot Description
default Tab items (<Tab> components)
#more Overflow button label. Scoped: { count: number }. Default content: More N ...

Reordering. With orderable tabs can be dragged inside the tab strip, and hidden tabs can be dragged out of the "More" menu onto the strip. TabList does not own the tabs — they are rendered by the caller — so it puts the DOM back the way it was and emits reorder with the new order of values; the caller reorders its own list:

<TabList orderable @reorder="reorder">
  <Tab v-for="tab in tabs" :key="tab.value" :value="tab.value">{{ tab.label }}</Tab>
</TabList>

<script setup>
const reorder = (values) =>
  tabs.value.sort((a, b) => values.indexOf(a.value) - values.indexOf(b.value))
</script>

Accordion

Collapsible panels. Supports single and multiple open modes.

<template>
  <Accordion :multiple="true">
    <AccordionPanel value="one">
      <AccordionHeader class="px-4 py-3 text-sm font-medium">
        Header
      </AccordionHeader>
      <AccordionContent class="px-4 py-3 text-sm text-gray-600">
        Panel content
      </AccordionContent>
    </AccordionPanel>
    <AccordionPanel value="two">
      <AccordionHeader class="px-4 py-3 text-sm font-medium">
        Another header
      </AccordionHeader>
      <AccordionContent class="px-4 py-3 text-sm text-gray-600">
        Another content
      </AccordionContent>
    </AccordionPanel>
  </Accordion>
</template>

<script setup>
import { Accordion, AccordionPanel, AccordionHeader, AccordionContent } from 'cetera-vue-utils'
</script>
Prop Component Type Default Description
multiple Accordion boolean false Allow multiple panels open at once
value Accordion (string | number)[] — Array of open panel values (v-model)
value AccordionPanel string | number — Unique panel identifier

AccordionHeader renders a button with an animated chevron. AccordionContent opens with a smooth height animation.

Skeleton

Placeholder for loading content with a pulse animation.

<template>
  <!-- Text lines -->
  <Skeleton height="1rem" width="100%" />
  <Skeleton height="1rem" width="75%" />

  <!-- Avatar + text -->
  <div class="flex items-center gap-3">
    <Skeleton shape="circle" width="3rem" height="3rem" />
    <div class="flex-1 space-y-2">
      <Skeleton height="0.875rem" width="40%" />
      <Skeleton height="0.75rem" width="60%" />
    </div>
  </div>
</template>

<script setup>
import { Skeleton } from 'cetera-vue-utils'
</script>
Prop Type Default Description
width string '100%' Block width
height string '1rem' Block height
shape 'rectangle' | 'circle' 'rectangle' Shape: rectangle or circle

Draggable

Sortable list component based on SortableJS. Supports drag between lists, multi-drag, and cloning.

<template>
  <!-- Sortable list -->
  <Draggable v-model="items" item-key="id" tag="ul" ghost-class="opacity-40">
    <template #item="{ element }">
      <li class="cursor-grab">{{ element.label }}</li>
    </template>
  </Draggable>

  <!-- Drag between two lists -->
  <Draggable v-model="listA" item-key="id" group="shared">
    <template #item="{ element }"><div>{{ element.label }}</div></template>
  </Draggable>
  <Draggable v-model="listB" item-key="id" group="shared">
    <template #item="{ element }"><div>{{ element.label }}</div></template>
  </Draggable>
</template>

<script setup>
import { ref } from 'vue'
import { Draggable } from 'cetera-vue-utils'

const items = ref([
  { id: 1, label: 'Item A' },
  { id: 2, label: 'Item B' },
])
</script>
Prop Type Default Description
modelValue any[] — List (use with v-model)
list any[] — List (mutated in place; mutually exclusive with v-model)
itemKey string | Function — Field or function to produce a unique key per item
tag string 'div' Root element tag
group string | object — SortableJS group — enables drag between lists
clone Function — Custom clone function for pull: 'clone'
move Function — Callback to validate a move; return false to cancel
multiDrag boolean false Enable multi-item drag (requires SortableJS MultiDrag plugin)

All other props are forwarded as SortableJS options (e.g. ghost-class, chosen-class, animation, handle, filter, sort, disabled).

@change emits { added }, { removed }, or { moved } after each reorder.

DataTable

Data table that reads column definitions from Column child components.

<template>
  <DataTable :value="users" scrollable scroll-height="400px">
    <Column field="name" header="Name" />
    <Column field="email" header="Email" />
    <Column header="Actions">
      <template #body="{ data }">
        <Button label="Edit" size="sm" @click="edit(data)" />
      </template>
    </Column>
  </DataTable>
</template>

<script setup>
import { DataTable, Column, Button } from 'cetera-vue-utils'
</script>
Prop Type Default Description
value any[] — Row data
scrollable boolean false Enable scrollable body
scrollHeight string — Fixed height for scrollable body (e.g. '400px')
stickyHeader boolean false Stick header on scroll
stickyTop string '0px' Top offset for sticky header (e.g. '64px' when a fixed navbar is present)
loading boolean false Show skeleton rows
zebra boolean false Alternate row background
borders boolean false Show column borders
rowClass string | ((row, index) => string) — Extra class(es) for each row
headerClass string — Extra class for header cells
dataKey string — Field that uniquely identifies a row; used as the row key and as the key in expandedRows (row index when omitted)
v-model:expandedRows Record<string | number, boolean> {} Expanded rows keyed by dataKey value. Optional — without v-model the table keeps the state itself
rowExpandable (row) => boolean — Return false to forbid expanding a row (its expander button is hidden)
Event Payload Description
row-click (row: object, index: number) Emitted when a row is clicked
row-expand (row: object, index: number) Emitted when a row is expanded
row-collapse (row: object, index: number) Emitted when a row is collapsed

Column props:

Prop Type Description
field string Data field name
header string Column header text
class string Extra class for both header and body cells
headerClass string Extra class for the header cell only
bodyClass string Extra class for body cells only
expander boolean Render a chevron button that expands/collapses the row

Column slots:

Slot Description
#body="{ data, index, expanded, toggle }" Custom cell renderer — receives the row object, its index, whether the row is expanded and a function that toggles it
#filter Filter control rendered in the filter row. If any column defines #filter, a filter row is shown below the header row.

DataTable slots:

Slot Description
header Content above the column headers (spans full width)
empty Content shown when value is empty. Default: «Нет данных»
#expansion="{ data, index }" Content of the expanded row, rendered under the row across all columns

Expanding rows — add a Column with expander (or call toggle from any #body slot) and an #expansion slot:

<DataTable v-model:expanded-rows="expandedRows" :value="users" data-key="id">
  <Column expander style="width: 3rem" />
  <Column field="name" header="Name" />
  <template #expansion="{ data }">
    <pre>{{ data.details }}</pre>
  </template>
</DataTable>

Pagination

Page navigation with rows-per-page selector. Optionally syncs current page with a URL query param via the History API.

<template>
  <!-- v-model style (preferred) -->
  <Pagination :total-records="243" v-model:limit="limit" v-model:offset="offset" />

  <!-- legacy @refresh style (still supported) -->
  <Pagination :total-records="243" :limit="limit" :offset="offset" @refresh="onRefresh" />
</template>

<script setup>
import { ref } from 'vue'
import { Pagination } from 'cetera-vue-utils'

const limit = ref(20)
const offset = ref(0)

const onRefresh = ({ limit: l, offset: o }) => {
  limit.value = l
  offset.value = o
}
</script>
Prop Type Default Description
totalRecords number 0 Total number of records
limit number — Records per page
offset number — Current offset
limitOptions number[] [20, 50, 100] Rows-per-page options
queryParam string — When set, syncs current page with this query param (queryParam="page" → ?page=2). Restores on mount, supports browser back/forward.

v-model:limit and v-model:offset update automatically on page/rows-per-page change. The @refresh event emits { limit, offset } on each change and remains supported for backward compatibility.

Popover

Floating panel that attaches to an external trigger element. No wrapper slot — place the component next to or inside the target element.

<!-- target=true (default): attaches to the parent element -->
<template>
  <div>
    <Button label="Open" outlined />
    <Popover>
      <div class="p-3">Panel content</div>
    </Popover>
  </div>
</template>

<script setup>
import { Popover, Button } from 'cetera-vue-utils'
</script>
Prop Type Default Description
target true | false | string | HTMLElement true Trigger element. true = parent element, false = no binding (programmatic only), string = CSS selector ('#id', '.class'), HTMLElement = direct ref
placement 'auto' | 'bottom-start' | 'bottom-end' | 'bottom' | 'top-start' | 'top-end' | 'top' | 'bottom-end-out' | 'top-end-out' 'auto' Panel placement (ignored when contextMenu). auto picks the side with most available space. *-out variants start the panel at the target's right edge and extend rightward
contextMenu boolean false Open on right-click at cursor position instead of click
hover boolean false Open on mouseenter, close on mouseleave (with 80 ms grace period to move into the panel)
Slot Description
default Panel content. Exposes { close } to close the panel programmatically
Method Description
open(anchor?) Programmatically open the panel. anchor can be a MouseEvent (positions at cursor) or HTMLElement (positions below element)

Closes on Escape or click outside.

target by selector:

<Button id="my-btn" label="Open" outlined />
<Popover target="#my-btn">
  <div class="p-3">Content</div>
</Popover>

target by ref:

<button ref="btn">Open</button>
<Popover :target="btn">
  <div class="p-3">Content</div>
</Popover>

target=false — programmatic only:

<Button label="Open" @click="(e) => popover.open(e)" />
<Popover ref="popover" :target="false">
  <div class="p-3">Content</div>
</Popover>

contextmenu — panel opens at cursor:

<div class="p-8">
  Right-click here
  <Popover contextMenu>
    <Menu>
      <MenuItem @click="edit">Edit</MenuItem>
      <MenuItem @click="remove">Delete</MenuItem>
    </Menu>
  </Popover>
</div>

Composables

useNotify

Global notification system with auto-dismiss.

import { useNotify } from 'cetera-vue-utils'

const notify = useNotify()

notify.success('Saved!')
notify.error('Something went wrong')
notify.warn('Please check the input')
notify.info('Update available')

// Custom duration (ms)
notify.add('info', 'Custom message', 5000)
Method Type Duration
success(message) success 3000ms
info(message) info 3000ms
warn(message) warn 4000ms
error(message) error 5000ms
add(type, message, life) any custom

The notifications ref is shared across all useNotify() calls — readable from anywhere in the app.

useHttpClient

HTTP client based on axios with automatic error notifications and request cancellation support.

import { useHttpClient } from 'cetera-vue-utils'

const http = useHttpClient({
    baseURL: 'https://api.example.com',
    onUnauthorized: () => router.push('/login'),
})

// GET
const { data } = await http.get<User[]>('/users')

// POST
await http.post('/users', { data: { name: 'John' } })

// With params
await http.get('/users', { params: { page: 1 } })

// Cancel previous request on repeat call
await http.get('/search', { params: { q: 'text' }, cancelPrevious: true })

// Suppress all notifications for this request
await http.get('/status', { silent: true })

// Download file
await http.download('/reports/export.xlsx')
Method Description
get<T>(url, config?) GET request
post<T>(url, config?) POST request
patch<T>(url, config?) PATCH request
put<T>(url, config?) PUT request
destroy<T>(url, config?) DELETE request
download(url, config?) Download file

Automatic behavior:

  • 2xx with a message field — shows notify.success
  • 400, 5xx — shows notify.error
  • 422 — shows notify.error with validation message
  • 401 — calls onUnauthorized()
  • 403 — shows notify.error("Action not allowed!")
  • 419 — shows notify.error about expired session

useErrors

Validation error management for forms.

import { useErrors } from 'cetera-vue-utils'

const errors = useErrors()

// Record errors from server response
errors.record(response.errors)

// Check if a field has an error
errors.has('email')       // true / false

// Get error text for a field
errors.get('email')       // "Email is required"

// Check if any errors exist
errors.any()              // true / false

// Set a single error
errors.set({ path: 'email', value: 'Invalid format' })

// Clear one field or all fields
errors.clear('email')
errors.clear()

useDataLoad

Data loading via useHttpClient with managed loading and data state.

import { useDataLoad, useHttpClient } from 'cetera-vue-utils'

const http = useHttpClient({ baseURL: '/api' })

// Simple case
const { data, loading, load } = useDataLoad<User[]>(() => http.get('/users'))

// With params
const { data, meta, loading, load } = useDataLoad<User[], { page: number }>(
    (params) => http.get('/users', { params })
)

// Load
await load({ page: 1 })

// Load only if data is not yet fetched (cache)
await load(true)
await load({ page: 1 }, true)

// Share state between multiple loaders
const data = ref<User[]>()
const loading = ref(false)
const { load: loadUsers } = useDataLoad(() => http.get('/users'), { data, loading })
const { load: loadMore } = useDataLoad((p) => http.get('/users', { params: p }), { data, loading })

The returned meta contains total — useful for pagination.

useForm

Reactive form state with optional two-way URL query string sync.

import { useForm } from 'cetera-vue-utils'

// Basic usage
const form = useForm({
    defaults: { search: '', page: 1, active: true },
    onChange: () => load(),
})

// Sync form state to/from URL query params
const form = useForm({
    defaults: { search: '', page: 1 },
    syncQuery: true,
    onChange: () => load(),
})

// Remember values across visits (sessionStorage), keyed by route path
const form = useForm({
    defaults: { search: '', page: 1 },
    remember: true,
    onChange: () => load(),
})
Option Type Default Description
defaults T — Initial form values
onChange () => void — Called on any change (deep watch)
syncQuery boolean false Sync form state to/from URL query string
remember boolean | string false Persist form state to sessionStorage and restore it next time useForm is used. true derives the storage key from the current route path; pass a string to set the key explicitly (useful when a route hosts more than one remembered form)

When syncQuery is enabled, form values are read from the URL on mount and written back on every change. Supports nested objects (filter[status]), dates, numbers, booleans, and arrays. Arrays are serialized as comma-separated values (e.g. ids=1,2,3) and deserialized back into arrays by checking whether the default value is an array. This works for both top-level array fields and array fields inside nested objects.

When remember is enabled, form values are read from sessionStorage on mount and written back on every change (JSON-serialized, so nested objects, dates, numbers, booleans and arrays round-trip without extra parsing rules). If both remember and syncQuery are enabled, URL query values take precedence over remembered ones.

Returns a Ref<T> — use as form.value.fieldName or v-model="form.value.fieldName".

useFormElements

Helper composable for form components. Used internally by Input, but available standalone.

import { useFormElements } from 'cetera-vue-utils'

const { isInvalid, isHelper, uuid } = useFormElements(props)

TypeScript

All exports are fully typed:

import type {
  Notification,
  HttpResponse,
  UseHttpOptions,
  ErrorsData,
  InputProps,
  SpinnerSize,
} from 'cetera-vue-utils'

License

MIT

Related Packages

@tb-dev/vue

Vue utilities

12,617
@bernankez/utils

Cole's utils preset

1,146 0
@chiballc/utils

TypeScript utilities, browser worker messaging, and the Kibao Nuxt module for Op...

10,723 0
@gits-id/utils

GITS Utilities

4,086

Version History

Version Released License
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9