cetera-vue-utils
cetera-vue-utils
Components, composables, and utilities for Vue 3.
Table of Contents
Components
- Accordion
- Button
- ButtonLink
- Checkbox
- ConfirmAction
- DataTable
- Dialog
- Draggable
- InlineLoading
- Input
- InputDate
- InputMonth
- InputFile
- InputFileImage
- InputNumber
- InputPassword
- InputColor
- InputSearch
- InputTime
- Menu
- MultiSelect
- Notifications
- Pagination
- Popover
- Radiobutton
- Select
- Skeleton
- Slider
- Spinner
- Tabs
- Textarea
- Toggle
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.cssresolves todist/style.css, which contains the compiled component CSS including the@vuepic/vue-datepickerbase styles needed byInputDateandInputTime. The@sourcedirective ensures your project's Tailwind generates theprimary-*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:
2xxwith amessagefield — showsnotify.success400,5xx— showsnotify.error422— showsnotify.errorwith validation message401— callsonUnauthorized()403— showsnotify.error("Action not allowed!")419— showsnotify.errorabout 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
TypeScript utilities, browser worker messaging, and the Kibao Nuxt module for Op...
Version History
| Version | Released | License |
|---|---|---|
| 0.4.13 | ||
| 0.4.12 | ||
| 0.4.11 | ||
| 0.4.10 | ||
| 0.4.9 | ||
| 0.4.8 | ||
| 0.4.7 | ||
| 0.4.6 | ||
| 0.4.5 | ||
| 0.4.4 | ||
| 0.4.3 | ||
| 0.4.2 | ||
| 0.4.1 | ||
| 0.4.0 | ||
| 0.3.83 | ||
| 0.3.82 | ||
| 0.3.81 | ||
| 0.3.80 | ||
| 0.3.79 | ||
| 0.3.78 | ||
| 0.3.77 | ||
| 0.3.76 | ||
| 0.3.75 | ||
| 0.3.74 | ||
| 0.3.73 | ||
| 0.3.72 | ||
| 0.3.71 | ||
| 0.3.70 | ||
| 0.3.69 | ||
| 0.3.68 |
Showing the latest 30 of 133. See every release on npm