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>- With ES modules,
import 'vfunc/ui'(andimport 'vfunc/ui/data'for the grid and charts) afterimport vf from 'vfunc'adds the members tovf. - IE11 uses
vfunc-ui.legacy.min.js,vfunc-ui-data.legacy.min.jsandvfunc-ui.legacy.css.vfunc-all.legacy.min.jsholds the engine, the core and the data file in one file.
How to use them
vf.vs*(props)returns markup (SafeHtml). Put it invf.htmlorrenderas is; it is not escaped twice.vf.vf*(props)returns an instance. Attach it with.mount(el)or put it in a parent component'schilds. Callbacks receive{ sender, event, data }.- Every
vs*takesid,ref(data-ref),action(data-action),classNameanddescribedBy. Give a form controllabel,hintorerrorand it is wrapped in a linked field. - Every string prop is escaped. Pass markup with
vf.html. - The slots of
vs*(vsCardbodyand the like) take markup only. For a working component inside one, leave an empty element with anidand add{ targetId, component }to your component'schilds. The content of vfTabs, vfAccordion, vfCarousel, vfPopover, vfModal and vfDrawer takes an instance as is and keeps it alive across re-renders. - Delegate events on
data-action; thevf-*classes are for design only. - Built-in texts are message keys. Change them with props or
vf.i18n.add.
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).
| Name | Tier | What it is | Example |
|---|---|---|---|
| Input | |||
vsField | S | Label, 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}">` }) |
vsInput | S | A text-like <input>; label, hint and error make it a linked field. | vf.vsInput({ name: 'email', type: 'email', label: 'Email', error: errors.email }) |
vsTextarea | S | A <textarea> with the same field props as vsInput. | vf.vsTextarea({ name: 'memo', label: 'Memo', rows: 4 }) |
vsSelect | S | A native <select> from an options list, with groups, placeholder and multiple. | vf.vsSelect({ name: 'role', label: 'Role', options: ['admin', 'user'], value: 'user' }) |
vsCheckbox | S | A native checkbox with its label. | vf.vsCheckbox({ name: 'agree', label: 'I agree', checked: true }) |
vsRadioGroup | S | Radio buttons in a fieldset with a legend. | vf.vsRadioGroup({ name: 'plan', label: 'Plan', options: ['free', 'pro'], value: 'free' }) |
vsSwitch | S | An on/off switch: a native checkbox with role="switch". | vf.vsSwitch({ name: 'notify', label: 'Notifications', checked: true }) |
vsSlider | S | A native range slider. | vf.vsSlider({ name: 'volume', label: 'Volume', value: 30 }) |
vsProgress | S | A native progress bar; no value shows the indeterminate state. | vf.vsProgress({ label: 'Upload', value: 42, showValue: true }) |
vsNumberInputvfNumberInput | P | A 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) }) |
vsSearchInputvfSearchInput | P | A 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) }) |
vsPasswordInputvfPasswordInput | P | A password input with a show/hide button (aria-pressed). | vf.vfPasswordInput({ name: 'password', label: 'Password', autocomplete: 'new-password' }) |
vsChipsInputvfChipsInput | P | Chips (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) }) |
vsRatingvfRating | P | Stars 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) }) |
vsSelectButtonvfSelectButton | P | Options 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) }) |
vsMaskedInputvfMaskedInput | P | A 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) }) |
vsDatePickervfDatePicker | P | A 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) }) |
vsTimePickervfTimePicker | P | A native time input with 'HH:mm' values; vf* normalizes typed text (IE11). | vf.vfTimePicker({ name: 'at', label: 'Time', step: 900 }) |
vfDateRangePicker | F | Start 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 | |||
vsButton | S | A button with variant, size, loading state and data-action / data-ref hooks. | vf.vsButton({ label: 'Save', variant: 'primary', action: 'save' }) |
vsButtonGroup | S | Buttons side by side in a role="group". | vf.vsButtonGroup({ label: 'View', attached: true, buttons: [{ label: 'List', action: 'list' }, { label: 'Grid', action: 'grid' }] }) |
vsBadge | S | A small status label or count. | vf.vsBadge({ label: 'Active', variant: 'success', dot: true }) |
vsTag | S | A label, optionally with a remove button (data-action, data-value). | vf.vsTag({ label: 'urgent', value: 'urgent', removable: true, removeAction: 'remove-tag' }) |
vsAvatar | S | A user picture, or the initials of the name. | vf.vsAvatar({ name: 'Ada Lovelace', src: user.photo }) |
vsAlert | S | A message box: info, success, warning, danger; optionally dismissible. | vf.vsAlert({ variant: 'warning', title: 'Check', message: 'Unsaved changes', dismissible: true }) |
vsCard | S | A 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' }) }) |
vsDescriptions | S | Label/value pairs as a <dl>, in 1–4 columns. | vf.vsDescriptions({ columns: 2, items: [{ label: 'Name', value: user.name }, { label: 'Email', value: user.email }] }) |
vsStatCard | S | A 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' }) |
vsTimeline | S | Events in order, with times formatted by vf.fmt.date. | vf.vsTimeline({ items: [{ title: 'Order placed', time: order.createdAt, variant: 'success' }] }) |
vsEmptyState | S | What to show when a list or search has nothing. | vf.vsEmptyState({ description: 'Try another keyword', action: vf.vsButton({ label: 'Reset', action: 'reset' }) }) |
vsSkeleton | S | Loading placeholders (text lines, rectangle, circle), hidden from screen readers. | vf.vsSkeleton({ lines: 3 }) |
vsSpinner | S | A loading indicator with a hidden status text. | vf.vsSpinner({ size: 'sm' }) |
vsTooltip | S | A 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 }) }) |
vsListViewvfListView | P | A 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) }) |
vfCarousel | F | Slides 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 | |||
vsBreadcrumb | S | The 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 }] }) |
vsPaginationvfPagination | P | Page 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) }) |
vsTabsvfTabs | P | Tabs 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 }] }) |
vsAccordionvfAccordion | P | Sections 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 })) }) |
vsSteppervfStepper | P | The steps of a process: complete, current (aria-current="step"), upcoming. | vf.vfStepper({ steps: ['Cart', 'Shipping', 'Payment'], active: 1 }) |
vsSplitButtonvfSplitButton | P | A 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 | |||
vfModal | F | A 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(); |
vfDrawer | F | vfModal as a full-height panel at the end, start or bottom of the screen. | vf.vfDrawer({ title: 'Filters', side: 'end', content: filtersHtml }).open() |
vfConfirm | F | A 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(); |
vfToast | F | One 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' }); |
vfDropdown | F | A 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) }) |
vfPopover | F | A 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 | |||
vsTable | S | A 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 }) |
vsSparkline | S | A 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' }) |
vfGrid | F | A 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) }) |
vsChartvfChart | P | SVG 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] }] } }) |
Gallery
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.