컴포넌트
layer 2(vfunc-ui)는 엔진 위에 만든 클래스 없는 컴포넌트입니다. 수고를 덜어 주는 곳에만 쓰고, 나머지는 HTML 그대로 두면 됩니다. 1.0.0부터 vfunc 패키지에 함께 들어 있습니다.
불러오기
<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>
<!-- 그리드·차트가 필요할 때 -->
<script src="dist/vfunc-ui-data.min.js"></script>
<!-- 한국어 기본 문구 -->
<script src="dist/vfunc-ui.locale.ko.js"></script>- ES 모듈은
import vf from 'vfunc'다음에import 'vfunc/ui'(그리드·차트는import 'vfunc/ui/data')를 쓰면vf에 멤버가 붙습니다. - IE11은
vfunc-ui.legacy.min.js·vfunc-ui-data.legacy.min.js·vfunc-ui.legacy.css를 씁니다. 엔진·코어·데이터를 한 파일로 묶은vfunc-all.legacy.min.js도 있습니다.
쓰는 법
vf.vs*(props)는 마크업(SafeHtml)을 돌려줍니다.vf.html이나render에 그대로 넣으면 되고 두 번 이스케이프되지 않습니다.vf.vf*(props)는 인스턴스를 돌려줍니다..mount(el)로 붙이거나 부모 컴포넌트의childs에 넣습니다. 콜백은{ sender, event, data }를 받습니다.- 모든
vs*는id,ref(data-ref),action(data-action),className,describedBy를 받습니다. 폼 컨트롤에label·hint·error를 주면 연결된 필드로 감쌉니다. - 문자열 props는 모두 이스케이프합니다. 마크업은
vf.html로 넘깁니다. vs*의 슬롯(vsCardbody등)은 마크업만 받습니다. 그 안에 동작하는 컴포넌트가 필요하면id를 가진 빈 요소를 두고 내 컴포넌트의childs에{ targetId, component }로 넣습니다. vfTabs·vfAccordion·vfCarousel·vfPopover·vfModal·vfDrawer의 content는 인스턴스를 그대로 받아 다시 그려도 살려 둡니다.- 이벤트는
data-action에 위임하고,vf-*클래스는 디자인에만 씁니다. - 기본 문구는 메시지 키입니다. props나
vf.i18n.add로 바꿉니다.
const save = vf.vfunc({
render: () => vf.html`
${vf.vsInput({ name: 'email', type: 'email', label: '이메일', required: true })}
${vf.vsButton({ label: '저장', variant: 'primary', action: 'save' })}`,
delegates: [{ selector: '[data-action="save"]', eventType: 'click', onEvent: async () => {
if (await confirmSave.open()) toast.show({ message: '저장했습니다', variant: 'success' });
} }]
});
const confirmSave = vf.vfConfirm({ title: '저장할까요?' });
const toast = vf.vfToast();
save.mount('#app');목록
Tier는 S(vs*만), P(vs* + vf*, vf*가 vs*로 그림), F(vf*만)입니다.
| 이름 | Tier | 설명 | 예시 |
|---|---|---|---|
| 입력 | |||
vsField | S | 직접 만든 컨트롤을 라벨·힌트·오류로 감싸고 id와 aria-describedby를 넘겨 줍니다. | vf.vsField({ label: 'Color', control: (a) => vf.html`<input type="color" id="${a.id}" aria-describedby="${a.describedBy}">` }) |
vsInput | S | 텍스트형 <input>. label·hint·error를 주면 연결된 필드가 됩니다. | vf.vsInput({ name: 'email', type: 'email', label: 'Email', error: errors.email }) |
vsTextarea | S | vsInput과 같은 필드 props를 받는 <textarea>. | vf.vsTextarea({ name: 'memo', label: 'Memo', rows: 4 }) |
vsSelect | S | 옵션 목록으로 만드는 네이티브 <select>. 그룹, placeholder, multiple 지원. | vf.vsSelect({ name: 'role', label: 'Role', options: ['admin', 'user'], value: 'user' }) |
vsCheckbox | S | 라벨이 붙은 네이티브 체크박스. | vf.vsCheckbox({ name: 'agree', label: 'I agree', checked: true }) |
vsRadioGroup | S | fieldset과 legend로 묶은 라디오 버튼. | vf.vsRadioGroup({ name: 'plan', label: 'Plan', options: ['free', 'pro'], value: 'free' }) |
vsSwitch | S | 켜기/끄기 스위치. role="switch"인 네이티브 체크박스. | vf.vsSwitch({ name: 'notify', label: 'Notifications', checked: true }) |
vsSlider | S | 네이티브 범위 슬라이더. | vf.vsSlider({ name: 'volume', label: 'Volume', value: 30 }) |
vsProgress | S | 네이티브 진행 막대. value가 없으면 진행 중 표시. | vf.vsProgress({ label: 'Upload', value: 42, showValue: true }) |
vsNumberInputvfNumberInput | P | 감소/증가 버튼이 있는 숫자 입력. vf*는 증감, min/max 보정, step 자릿수 반올림. | vf.vfNumberInput({ name: 'qty', label: 'Quantity', value: 1, min: 1, max: 99, onChange: (e) => setQty(e.data.value) }) |
vsSearchInputvfSearchInput | P | 지우기 버튼이 있는 검색 상자. vf*는 onSearch 디바운스(IME 조합 중 제외), Enter로 즉시 검색. | vf.vfSearchInput({ debounce: 300, onSearch: (e) => load(e.data.value) }) |
vsPasswordInputvfPasswordInput | P | 보기/숨기기 버튼(aria-pressed)이 있는 비밀번호 입력. | vf.vfPasswordInput({ name: 'password', label: 'Password', autocomplete: 'new-password' }) |
vsChipsInputvfChipsInput | P | 칩(vsTag)과 텍스트 입력. Enter·쉼표로 추가, Backspace로 삭제, 폼 전송용 hidden input. | vf.vfChipsInput({ name: 'tags', label: 'Tags', value: ['ui'], max: 5, onChange: (e) => save(e.data.value) }) |
vsRatingvfRating | P | 네이티브 라디오 기반 별점(키보드 동작). readonly는 대체 텍스트가 있는 이미지. | vf.vfRating({ name: 'score', value: 3, onChange: (e) => rate(e.data.value) }) |
vsSelectButtonvfSelectButton | P | aria-pressed 토글 버튼으로 고르는 옵션. 하나 또는 multiple로 여럿. | vf.vfSelectButton({ ariaLabel: 'Period', options: ['day', 'week', 'month'], value: 'week', onChange: (e) => setPeriod(e.data.value) }) |
vsMaskedInputvfMaskedInput | P | 마스크로 서식하는 텍스트 입력(0 숫자, a 문자, * 둘 다). vf*는 커서를 유지하고 원래 값도 줍니다. | vf.vfMaskedInput({ name: 'phone', label: 'Phone', mask: '000-0000-0000', onChange: (e) => save(e.data.raw) }) |
vsDatePickervfDatePicker | P | 'YYYY-MM-DD' 값의 네이티브 날짜 입력. vf*는 입력한 글을 정규화(IE11)하고 Date를 줍니다. | vf.vfDatePicker({ name: 'due', label: 'Due date', min: new Date(), onChange: (e) => setDue(e.data.date) }) |
vsTimePickervfTimePicker | P | 'HH:mm' 값의 네이티브 시간 입력. vf*는 입력한 글을 정규화(IE11). | vf.vfTimePicker({ name: 'at', label: 'Time', step: 900 }) |
vfDateRangePicker | F | 두 네이티브 입력의 시작일·종료일. 종료일이 시작일보다 앞서지 않습니다. | vf.vfDateRangePicker({ label: 'Period', names: ['from', 'to'], onChange: (e) => search(e.data.start, e.data.end) }) |
| 표시 | |||
vsButton | S | 변형·크기·로딩 상태와 data-action / data-ref 훅을 가진 버튼. | vf.vsButton({ label: 'Save', variant: 'primary', action: 'save' }) |
vsButtonGroup | S | role="group"로 묶은 나란한 버튼. | vf.vsButtonGroup({ label: 'View', attached: true, buttons: [{ label: 'List', action: 'list' }, { label: 'Grid', action: 'grid' }] }) |
vsBadge | S | 작은 상태 표시나 개수. | vf.vsBadge({ label: 'Active', variant: 'success', dot: true }) |
vsTag | S | 라벨. 삭제 버튼(data-action, data-value)을 붙일 수 있습니다. | vf.vsTag({ label: 'urgent', value: 'urgent', removable: true, removeAction: 'remove-tag' }) |
vsAvatar | S | 사용자 사진, 또는 이름의 머리글자. | vf.vsAvatar({ name: 'Ada Lovelace', src: user.photo }) |
vsAlert | S | 정보·성공·경고·위험 메시지 상자. 닫기 버튼 선택. | vf.vsAlert({ variant: 'warning', title: 'Check', message: 'Unsaved changes', dismissible: true }) |
vsCard | S | 머리(제목·부제·동작)·본문·바닥을 가진 카드. | vf.vsCard({ title: 'Sales', body: vf.html`<p>${text}</p>`, footer: vf.vsButton({ label: 'More', action: 'more' }) }) |
vsDescriptions | S | 라벨/값 쌍을 1~4열의 <dl>로. | vf.vsDescriptions({ columns: 2, items: [{ label: 'Name', value: user.name }, { label: 'Email', value: user.email }] }) |
vsStatCard | S | vf.fmt로 서식한 핵심 수치와 증감(+12.5%). | vf.vsStatCard({ label: 'Revenue', value: 1250000, format: { style: 'currency', currency: 'KRW' }, delta: 0.125, deltaLabel: 'vs last month' }) |
vsTimeline | S | 시간 순서의 이벤트. 시간은 vf.fmt.date로 서식. | vf.vsTimeline({ items: [{ title: 'Order placed', time: order.createdAt, variant: 'success' }] }) |
vsEmptyState | S | 목록이나 검색 결과가 없을 때의 화면. | vf.vsEmptyState({ description: 'Try another keyword', action: vf.vsButton({ label: 'Reset', action: 'reset' }) }) |
vsSkeleton | S | 로딩 자리 표시(텍스트 줄·사각형·원). 스크린리더에서는 숨김. | vf.vsSkeleton({ lines: 3 }) |
vsSpinner | S | 숨은 상태 문구가 있는 로딩 표시. | vf.vsSpinner({ size: 'sm' }) |
vsTooltip | S | hover·focus 때 보이는 짧은 글. aria-describedby로 연결, CSS만 사용. | vf.vsTooltip({ text: 'Copy link', trigger: (a) => vf.vsButton({ label: 'Copy', action: 'copy', describedBy: a.describedBy }) }) |
vsListViewvfListView | P | 항목 목록. selectable이면 로빙 포커스 listbox(화살표, Space, Enter). | vf.vfListView({ label: 'Users', items: users, selectable: 'single', onSelect: (e) => open(e.data.value) }) |
vfCarousel | F | 이전/다음·점 버튼과 선택적 자동 넘김(hover·focus·reduced-motion에서 멈춤)의 슬라이드. | vf.vfCarousel({ label: 'News', items: [{ src: '/a.jpg', alt: 'Launch' }, { content: vf.html`<p>${text}</p>` }], autoplay: 5000 }) |
| 내비게이션 | |||
vsBreadcrumb | S | 현재 페이지까지의 경로. 마지막 항목에 aria-current="page". | vf.vsBreadcrumb({ items: [{ label: 'Home', href: '#/' }, { label: 'Users', href: '#/users' }, { label: user.name }] }) |
vsPaginationvfPagination | P | 이전/다음과 생략 표시가 있는 페이지 버튼. vf*는 페이지를 바꾸고 onChange 호출. | vf.vfPagination({ total: 230, pageSize: 20, onChange: (e) => load(e.data.page) }) |
vsTabsvfTabs | P | 탭과 패널(WAI-ARIA). 로빙 tabindex, vf*는 화살표(RTL 반영)·Home·End. | vf.vfTabs({ label: 'Settings', tabs: [{ id: 'profile', label: 'Profile', content: profileHtml }, { id: 'security', label: 'Security', content: securityHtml }] }) |
vsAccordionvfAccordion | P | 제목 버튼(aria-expanded)과 region 패널의 접이식 섹션. 하나 또는 여럿 열기. | vf.vfAccordion({ items: faq.map((q) => ({ id: q.id, title: q.question, content: q.answer })) }) |
vsSteppervfStepper | P | 진행 단계: 완료, 현재(aria-current="step"), 예정. | vf.vfStepper({ steps: ['Cart', 'Shipping', 'Payment'], active: 1 }) |
vsSplitButtonvfSplitButton | P | 주 동작과 관련 동작 메뉴(vfDropdown 메뉴 공유). | vf.vfSplitButton({ label: 'Save', variant: 'primary', items: [{ label: 'Save as draft', action: 'draft' }], onClick: save, onSelect: (e) => run(e.data.action) }) |
| 오버레이 | |||
vfModal | F | 모달 대화상자. 포커스를 안에 가두고 되돌리며, Esc는 가장 위 레이어만, 배경 스크롤 잠금. footer 동작은 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. | vf.vfDrawer({ title: 'Filters', side: 'end', content: filtersHtml }).open() |
vfConfirm | F | 예/아니요 alertdialog. open()이 답의 Promise를 돌려주고, danger면 취소 버튼에서 시작. | const c = vf.vfConfirm({ title: 'Delete 3 items?', variant: 'danger', confirmLabel: 'Delete' }); const ok = await c.open(); c.destroy(); if (ok) remove(); |
vfToast | F | 알림 영역 하나(라이브 영역). show()로 추가하고 일정 시간 뒤 사라지며, hover·focus 중에는 유지. | const toast = vf.vfToast(); toast.show({ message: 'Saved', variant: 'success' }); |
vfDropdown | F | 동작 메뉴를 여는 버튼(WAI-ARIA 메뉴 버튼: 화살표, Home, End, 글자, Esc). | 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 | 버튼 옆의 비모달 대화상자. Esc, 바깥 클릭, 포커스 이탈로 닫힘. | vf.vfPopover({ trigger: { label: 'Help' }, title: 'Shortcuts', content: vf.html`<p>Press / to search.</p>` }) |
| 데이터 | |||
vsTable | S | 정렬 헤더 버튼(aria-sort, data-action="sort"), 행 키, 빈·로딩 상태를 가진 표. vfPagination과 앱 state로 목록 화면을 만듭니다. | vf.vsTable({ caption: 'Users', columns: [{ key: 'name', label: 'Name', sortable: true }, { key: 'age', label: 'Age', align: 'end' }], data: rows, sort: state.sort }) |
vsSparkline | S | 대체 텍스트가 있는 작은 정적 SVG 추세(선·막대). 표와 통계 카드용. | vf.vsSparkline({ data: [3, 5, 4, 8, 7], type: 'line' }) |
vfGrid | F | 데이터 그리드(데이터 파일): 정렬, 페이징, 필터, 행 선택, 서버 모드, 고정 헤더. G-1 그리드 계약. (vfunc-ui-data.js) | vf.vfGrid({ columns, data: users, pageSize: 20, selectable: 'multiple', onSelect: (e) => setSelected(e.data.keys) }) |
vsChartvfChart | P | SVG 차트(데이터 파일): 막대·선·영역·원·도넛·스파크라인, 색은 --vf-chart-* 토큰. vfChart는 툴팁, 범례 토글, 폭 추적. (vfunc-ui-data.js) | vf.vfChart({ type: 'line', label: 'Monthly sales', data: { labels: ['Jan', 'Feb', 'Mar'], series: [{ name: '2026', data: [12, 19, 15] }] } }) |
갤러리
모든 컴포넌트를 한 페이지에서 조작해 볼 수 있습니다: 컴포넌트 갤러리. 한국어·영어를 바꿔 볼 수 있고, 다크 테마는 OS 설정을 따릅니다.
AI용 목록
props와 메서드까지 담은 전체 목록은 ai/ko/components.md입니다. catalog와 타입 선언에서 빌드가 생성하므로 항상 코드와 같습니다. AI 도구에 이 파일과 llms.txt를 함께 주세요.
다른 라이브러리
AG Grid·Tabulator·Chart.js·ECharts는 공식 어댑터가 있습니다. 목록에 없는 라이브러리는 서드파티 통합의 절차대로 가져옵니다.