vfunc.js

Components

Layer 2 (vfunc-ui) is a set of components without classes, built on the engine. Use them where they save work and keep plain HTML everywhere else. From 1.0.0 it ships in the vfunc package.

Loading

<link rel="stylesheet" href="css/vfunc.tokens.css">
<link rel="stylesheet" href="css/vfunc-ui.css">

<script src="dist/vfunc.min.js"></script>
<script src="dist/vfunc-ui.min.js"></script>
<!-- when you need the grid and charts -->
<script src="dist/vfunc-ui-data.min.js"></script>
<!-- built-in Korean texts -->
<script src="dist/vfunc-ui.locale.ko.js"></script>

How to use them

const save = vf.vfunc({
  render: () => vf.html`
    ${vf.vsInput({ name: 'email', type: 'email', label: 'Email', required: true })}
    ${vf.vsButton({ label: 'Save', variant: 'primary', action: 'save' })}`,
  delegates: [{ selector: '[data-action="save"]', eventType: 'click', onEvent: async () => {
    if (await confirmSave.open()) toast.show({ message: 'Saved', variant: 'success' });
  } }]
});
const confirmSave = vf.vfConfirm({ title: 'Save the changes?' });
const toast = vf.vfToast();
save.mount('#app');

List

Tier is S (vs* only), P (vs* + vf*; the vf* renders with the vs*) or F (vf* only).

NameTierWhat it isExample
Input
vsFieldSLabel, hint and error around a control you build yourself; gives it the id and aria-describedby.vf.vsField({ label: 'Color', control: (a) => vf.html`<input type="color" id="${a.id}" aria-describedby="${a.describedBy}">` })
vsInputSA text-like <input>; label, hint and error make it a linked field.vf.vsInput({ name: 'email', type: 'email', label: 'Email', error: errors.email })
vsTextareaSA <textarea> with the same field props as vsInput.vf.vsTextarea({ name: 'memo', label: 'Memo', rows: 4 })
vsSelectSA native <select> from an options list, with groups, placeholder and multiple.vf.vsSelect({ name: 'role', label: 'Role', options: ['admin', 'user'], value: 'user' })
vsCheckboxSA native checkbox with its label.vf.vsCheckbox({ name: 'agree', label: 'I agree', checked: true })
vsRadioGroupSRadio buttons in a fieldset with a legend.vf.vsRadioGroup({ name: 'plan', label: 'Plan', options: ['free', 'pro'], value: 'free' })
vsSwitchSAn on/off switch: a native checkbox with role="switch".vf.vsSwitch({ name: 'notify', label: 'Notifications', checked: true })
vsSliderSA native range slider.vf.vsSlider({ name: 'volume', label: 'Volume', value: 30 })
vsProgressSA native progress bar; no value shows the indeterminate state.vf.vsProgress({ label: 'Upload', value: 42, showValue: true })
vsNumberInput
vfNumberInput
PA number input with decrease/increase buttons; vf* steps, clamps to min/max and rounds to the step.vf.vfNumberInput({ name: 'qty', label: 'Quantity', value: 1, min: 1, max: 99, onChange: (e) => setQty(e.data.value) })
vsSearchInput
vfSearchInput
PA search box with a clear button; vf* debounces onSearch (IME-aware), searches at once on Enter.vf.vfSearchInput({ debounce: 300, onSearch: (e) => load(e.data.value) })
vsPasswordInput
vfPasswordInput
PA password input with a show/hide button (aria-pressed).vf.vfPasswordInput({ name: 'password', label: 'Password', autocomplete: 'new-password' })
vsChipsInput
vfChipsInput
PChips (vsTag) and a text input; Enter or comma adds, Backspace removes; hidden inputs for forms.vf.vfChipsInput({ name: 'tags', label: 'Tags', value: ['ui'], max: 5, onChange: (e) => save(e.data.value) })
vsRating
vfRating
PStars on native radios (keyboard works); readonly shows an image with a text alternative.vf.vfRating({ name: 'score', value: 3, onChange: (e) => rate(e.data.value) })
vsSelectButton
vfSelectButton
POptions as aria-pressed toggle buttons; one choice or several with multiple.vf.vfSelectButton({ ariaLabel: 'Period', options: ['day', 'week', 'month'], value: 'week', onChange: (e) => setPeriod(e.data.value) })
vsMaskedInput
vfMaskedInput
PA text input formatted by a mask (0 digit, a letter, * either); vf* keeps the caret and gives the raw value.vf.vfMaskedInput({ name: 'phone', label: 'Phone', mask: '000-0000-0000', onChange: (e) => save(e.data.raw) })
vsDatePicker
vfDatePicker
PA native date input with 'YYYY-MM-DD' values; vf* normalizes typed text (IE11) and gives a Date.vf.vfDatePicker({ name: 'due', label: 'Due date', min: new Date(), onChange: (e) => setDue(e.data.date) })
vsTimePicker
vfTimePicker
PA native time input with 'HH:mm' values; vf* normalizes typed text (IE11).vf.vfTimePicker({ name: 'at', label: 'Time', step: 900 })
vfDateRangePickerFStart and end dates on two native inputs; the end never goes before the start.vf.vfDateRangePicker({ label: 'Period', names: ['from', 'to'], onChange: (e) => search(e.data.start, e.data.end) })
Display
vsButtonSA button with variant, size, loading state and data-action / data-ref hooks.vf.vsButton({ label: 'Save', variant: 'primary', action: 'save' })
vsButtonGroupSButtons side by side in a role="group".vf.vsButtonGroup({ label: 'View', attached: true, buttons: [{ label: 'List', action: 'list' }, { label: 'Grid', action: 'grid' }] })
vsBadgeSA small status label or count.vf.vsBadge({ label: 'Active', variant: 'success', dot: true })
vsTagSA label, optionally with a remove button (data-action, data-value).vf.vsTag({ label: 'urgent', value: 'urgent', removable: true, removeAction: 'remove-tag' })
vsAvatarSA user picture, or the initials of the name.vf.vsAvatar({ name: 'Ada Lovelace', src: user.photo })
vsAlertSA message box: info, success, warning, danger; optionally dismissible.vf.vsAlert({ variant: 'warning', title: 'Check', message: 'Unsaved changes', dismissible: true })
vsCardSA surface with header (title, subtitle, actions), body and footer.vf.vsCard({ title: 'Sales', body: vf.html`<p>${text}</p>`, footer: vf.vsButton({ label: 'More', action: 'more' }) })
vsDescriptionsSLabel/value pairs as a <dl>, in 1–4 columns.vf.vsDescriptions({ columns: 2, items: [{ label: 'Name', value: user.name }, { label: 'Email', value: user.email }] })
vsStatCardSA key figure formatted with vf.fmt, with an optional change (+12.5%).vf.vsStatCard({ label: 'Revenue', value: 1250000, format: { style: 'currency', currency: 'KRW' }, delta: 0.125, deltaLabel: 'vs last month' })
vsTimelineSEvents in order, with times formatted by vf.fmt.date.vf.vsTimeline({ items: [{ title: 'Order placed', time: order.createdAt, variant: 'success' }] })
vsEmptyStateSWhat to show when a list or search has nothing.vf.vsEmptyState({ description: 'Try another keyword', action: vf.vsButton({ label: 'Reset', action: 'reset' }) })
vsSkeletonSLoading placeholders (text lines, rectangle, circle), hidden from screen readers.vf.vsSkeleton({ lines: 3 })
vsSpinnerSA loading indicator with a hidden status text.vf.vsSpinner({ size: 'sm' })
vsTooltipSA short text on hover and focus, linked with aria-describedby; CSS only.vf.vsTooltip({ text: 'Copy link', trigger: (a) => vf.vsButton({ label: 'Copy', action: 'copy', describedBy: a.describedBy }) })
vsListView
vfListView
PA list of items; with selectable, a listbox with roving focus (arrows, Space, Enter).vf.vfListView({ label: 'Users', items: users, selectable: 'single', onSelect: (e) => open(e.data.value) })
vfCarouselFSlides one at a time with previous/next, dots and optional autoplay that pauses on hover, focus and reduced motion.vf.vfCarousel({ label: 'News', items: [{ src: '/a.jpg', alt: 'Launch' }, { content: vf.html`<p>${text}</p>` }], autoplay: 5000 })
Navigation
vsBreadcrumbSThe path to the current page; the last item has aria-current="page".vf.vsBreadcrumb({ items: [{ label: 'Home', href: '#/' }, { label: 'Users', href: '#/users' }, { label: user.name }] })
vsPagination
vfPagination
PPage buttons with previous/next and gaps; vf* changes the page and calls onChange.vf.vfPagination({ total: 230, pageSize: 20, onChange: (e) => load(e.data.page) })
vsTabs
vfTabs
PTabs and panels (WAI-ARIA): roving tabindex; vf* adds arrow keys (RTL aware), Home and End.vf.vfTabs({ label: 'Settings', tabs: [{ id: 'profile', label: 'Profile', content: profileHtml }, { id: 'security', label: 'Security', content: securityHtml }] })
vsAccordion
vfAccordion
PSections with heading buttons (aria-expanded) and region panels; one or multiple open.vf.vfAccordion({ items: faq.map((q) => ({ id: q.id, title: q.question, content: q.answer })) })
vsStepper
vfStepper
PThe steps of a process: complete, current (aria-current="step"), upcoming.vf.vfStepper({ steps: ['Cart', 'Shipping', 'Payment'], active: 1 })
vsSplitButton
vfSplitButton
PA main action with a menu of related actions (the vfDropdown menu).vf.vfSplitButton({ label: 'Save', variant: 'primary', items: [{ label: 'Save as draft', action: 'draft' }], onClick: save, onSelect: (e) => run(e.data.action) })
Overlay
vfModalFA modal dialog: focus kept inside and returned, Escape for the top layer, page scroll locked; footer actions through onAction.const dialog = vf.vfModal({ title: 'Edit user', content: form, footer: vf.vsButton({ label: 'Save', action: 'save', variant: 'primary' }), onAction: (e) => save() }); dialog.open();
vfDrawerFvfModal as a full-height panel at the end, start or bottom of the screen.vf.vfDrawer({ title: 'Filters', side: 'end', content: filtersHtml }).open()
vfConfirmFA yes/no alertdialog; open() returns a Promise of the answer; danger starts on cancel.const c = vf.vfConfirm({ title: 'Delete 3 items?', variant: 'danger', confirmLabel: 'Delete' }); const ok = await c.open(); c.destroy(); if (ok) remove();
vfToastFOne notification region (live region); show() adds toasts that go after a while and stay while hovered or focused.const toast = vf.vfToast(); toast.show({ message: 'Saved', variant: 'success' });
vfDropdownFA button that opens a menu of actions (WAI-ARIA menu button: arrows, Home, End, letters, Escape).vf.vfDropdown({ trigger: { label: 'Actions' }, items: [{ label: 'Rename', action: 'rename' }, { separator: true }, { label: 'Delete', action: 'delete', danger: true }], onSelect: (e) => run(e.data.action) })
vfPopoverFA non-modal dialog next to a button; Escape, an outside press or leaving it closes.vf.vfPopover({ trigger: { label: 'Help' }, title: 'Shortcuts', content: vf.html`<p>Press / to search.</p>` })
Data
vsTableSA table with sortable header buttons (aria-sort, data-action="sort"), row keys, empty and loading states; with vfPagination and app state it makes list screens.vf.vsTable({ caption: 'Users', columns: [{ key: 'name', label: 'Name', sortable: true }, { key: 'age', label: 'Age', align: 'end' }], data: rows, sort: state.sort })
vsSparklineSA small static SVG trend (line or bars) with a text alternative; for tables and stat cards.vf.vsSparkline({ data: [3, 5, 4, 8, 7], type: 'line' })
vfGridFA data grid (data file): sorting, paging, filter, row selection, server mode, fixed header; the G-1 grid contract. (vfunc-ui-data.js)vf.vfGrid({ columns, data: users, pageSize: 20, selectable: 'multiple', onSelect: (e) => setSelected(e.data.keys) })
vsChart
vfChart
PSVG charts (data file): bar, line, area, pie, donut, sparkline; colors from --vf-chart-* tokens. vfChart adds tooltips, legend toggles and redraws at its width. (vfunc-ui-data.js)vf.vfChart({ type: 'line', label: 'Monthly sales', data: { labels: ['Jan', 'Feb', 'Mar'], series: [{ name: '2026', data: [12, 19, 15] }] } })

Try every component on one page: the component gallery. You can switch between English and Korean; the dark theme follows your OS.

List for AI tools

The full list with props and methods is ai/en/components.md. The build generates it from the catalog and the type declarations, so it always matches the code. Give it to your AI tool together with llms.txt.

Other libraries

AG Grid, Tabulator, Chart.js and ECharts have official adapters. For a library that is not on the list, follow Third-party integration.