Open Web Components glassmorphism

Zero-dependency web component library. Drop in one script tag.

Theme
<script src="https://cdn.jsdelivr.net/gh/reneoun/Open-Web-Components@v1.5.0/OWC/dist/components.js"></script>
14Components
0Events fired
0Rows selected
0Btn clicks
0Searches

Keyboard shortcuts

Show this overlay?
Go to o-button1
Go to o-panel2
Go to o-toast3
Go to o-toggle4
Go to o-table5
Go to o-search6
Go to combined demo7
Fire random toastT
Close this overlayEsc

o-button

Glassmorphic button. Fires o-click instead of native click.

Click me Save Cancel
0 clicks
<o-button>Click me</o-button> // Event btn.addEventListener('o-click', () => console.log('clicked'))

o-panel

Glassmorphic content panel. Supports dragging, grid snapping, and resizing.

Draggable + snap 20px

Grab ⠿ to move. Snaps to 20px grid.

Resizable panel

Drag the right / bottom / corner handles.

Drag + snap 40px + resize

All features combined.

Static panel

No attributes.

<o-panel>Static content</o-panel> <o-panel move>Draggable</o-panel> <o-panel move snap="20">Snaps to 20px grid while dragging</o-panel> <o-panel resize>Resizable (right / bottom / corner handles)</o-panel> <o-panel move snap="20" resize>All combined</o-panel>

o-panel — attribute playground

Toggle attributes live and see the generated markup update in real time.

Playground panel

Toggle checkboxes above to change attributes.

<o-panel>Content</o-panel>

o-toast

Programmatic toast notifications. Call window.toast(message, type?, options?) after loading the CDN bundle.

Success Error Warning Info 8s toast
// type: 'success' | 'error' | 'warning' | 'info' (default: 'info') toast('File saved!', 'success') toast('Connection failed', 'error') toast('Long message', 'info', { duration: 8000 })

o-toggle

Segmented control toggle. Pass options as a comma-separated string or array. Fires o-change with { value, index }.

Selected: Day — change the toggle above to see this update live.
<o-toggle options="Day,Week,Month" value="day"></o-toggle> // Or set via JS toggle.options = [{ label: 'Day', value: 'day' }, { label: 'Week', value: 'week' }] toggle.value = 'week' // Event toggle.addEventListener('o-change', e => console.log(e.detail)) // { value, index, prev }

o-toggle — as a tab switcher

Use o-toggle to drive tab content — swap any panel based on the selected value.

Overview

Open Web Components is a glassmorphic web component library. Drop in one script tag and get beautiful, zero-dependency UI components that work in any framework.

o-table

Sortable, resizable, glassmorphic data table. Supports column persistence and optional row selection.

<o-table selectable></o-table> table.columns = [ { key: 'name', label: 'Name', width: 160, sortable: true }, { key: 'score', label: 'Score', width: 90, sortable: true, minWidth: 60 }, ] table.data = [{ name: 'Alice', score: 92 }, ...] // Events table.addEventListener('o-sort', e => console.log(e.detail)) // { col, dir } table.addEventListener('o-row-select', e => console.log(e.detail)) // { selected: row[] } // Attributes // storage="local|session" storage-key="my-key" — persists sort + column widths // selectable — adds checkbox column

o-table — dynamic data + undo/redo

Add, remove, or randomize rows at runtime. Every action is pushed to a history stack — undo and redo supported.

+ Add row - Remove last Randomize scores Reset
↩ Undo ↪ Redo 4 rows

o-button — simulated async loading

Disable and update text while awaiting an async operation — restore on completion.

Save to server Upload file Sync data
btn.addEventListener('o-click', async () => { btn.setAttribute('disabled', '') btn.textContent = 'Saving…' await doWork() btn.removeAttribute('disabled') btn.textContent = 'Save to server' toast('Saved!', 'success') })

o-toggle — color swatch picker

Drive any visual state from o-toggle — here a live color preview controlled by the selected option.

Hex: #3b82f6

Glassmorphic search bar with built-in filtering and optional dropdown. Add no-dropdown to use it as a pure filter input.

<o-search placeholder="Search..." no-dropdown></o-search> search.data = [{ name: 'Apple' }, { name: 'Banana' }] search.searchKeys = ['name'] search.valueKey = 'name' // fills input on select search.renderItem = item => `<b>${item.name}</b>` // custom dropdown row // Custom filter (overrides searchKeys) search.filterFn = (query, item) => item.name.startsWith(query) // Events search.addEventListener('o-input', e => console.log(e.detail)) // { query } search.addEventListener('o-results', e => console.log(e.detail)) // { query, results } search.addEventListener('o-select', e => console.log(e.detail)) // { item, query }

o-search — rich dropdown with custom renderItem

Pass a renderItem function to build any dropdown layout — emoji, icons, multi-line, badges.

search.renderItem = item => ` <span style="font-size:18px">${item.icon}</span> <span><b>${item.name}</b><br> <small style="opacity:0.6">${item.desc}</small></span>`

o-table — editable cells

editable="always" renders inline inputs. editable="click" opens a form row below the original with label:input pairs. Press ✏️ to edit, ✓ to confirm, ✗ to cancel.

<o-table editable></o-table> table.columns = [ { key: 'name', label: 'Name', width: 160, editable: 'always' }, { key: 'role', label: 'Role', width: 140, editable: 'click' }, { key: 'dept', label: 'Dept', width: 140, editable: 'click' }, ] // Click ✏️ → form row appears below with editable fields table.addEventListener('o-cell-change', e => console.log(e.detail)) table.addEventListener('o-row-change', e => console.log(e.detail))

o-note

Glass-styled note component. variant="textarea" (default) auto-resizes with optional floating label and char counter. variant="card" adds a title and tag chips.

Textarea variant

Card variant

<!-- textarea --> <o-note label="Note" placeholder="..." max-length="200"></o-note> <!-- card --> <o-note variant="card"></o-note> note.addEventListener('o-change', e => console.log(e.detail))

o-dialog

Glassmorphism form dialog with blurred backdrop, slot-based content, and programmatic open/close. Fires o-submit with named input values and o-cancel on backdrop click or Escape.

Open dialog
Add team member
Add member Cancel
<o-dialog id="d"> <span slot="title">Add Item</span> <label>Name</label> <input name="name" type="text"> <div slot="actions"> <o-button type="submit">Save</o-button> </div> </o-dialog> document.getElementById('d').open() d.addEventListener('o-submit', e => console.log(e.detail)) // { name } d.addEventListener('o-cancel', () => console.log('cancelled'))

o-tooltip

Glassmorphic tooltip that wraps any element. Appears on hover or focus. Supports four positions via the position attribute.

Hover me (top) Bottom Left Right Plain span
<o-tooltip text="Hello!" position="top"> <o-button>Hover me</o-button> </o-tooltip> <!-- position: top (default) | bottom | left | right -->

o-dropdown

Glassmorphic dropdown menu. Set options via the options property. Fires o-select with { value, label }. Keyboard-navigable with arrow keys and Escape.

Actions menu
Actions ▾
With icons
File ▾
Status
Set status ▾
const dd = document.querySelector('o-dropdown') dd.options = [ { label: 'Edit', value: 'edit', icon: '✏️' }, { label: 'Duplicate', value: 'duplicate', icon: '📋' }, { label: 'Delete', value: 'delete', icon: '🗑️' }, ] dd.addEventListener('o-select', e => { console.log(e.detail) // { value, label } }) // Programmatic control dd.open() dd.close() dd.toggle()

o-tabs

Glassmorphic tab bar. Add slot="tab" children with data-value for the tab headers, and data-tab on panel elements for content. Fires o-change with { value, prev }. Arrow-key navigable.

Overview Installation Events Theming
Open Web Components A glassmorphic web component library — zero dependencies, one script tag. All components are built with native Custom Elements and Shadow DOM, making them compatible with any framework or plain HTML.
Via CDN
<script src="https://cdn.jsdelivr.net/gh/reneoun/Open-Web-Components@v1.5.0/OWC/dist/components.js"></script>
Via npm
npm install @owc/components
o-button → o-click
o-toggle → o-change { value, index, prev }
o-search → o-input, o-results, o-select
o-table → o-sort, o-row-select, o-cell-change
o-dialog → o-submit, o-cancel
o-note → o-change { value } / { title, body, tags }
o-dropdown → o-select { value, label }
o-tabs → o-change { value, prev }
CSS custom properties All components inherit from GlassElement and use CSS variables: --glass-bg, --glass-border, --glass-blur, --glass-shadow, --glass-text, --glass-hover, --accent-warm. Override on :root or any ancestor to apply a custom theme.
<o-tabs> <span slot="tab" data-value="one">Tab 1</span> <span slot="tab" data-value="two">Tab 2</span> <div data-tab="one">Content for tab one</div> <div data-tab="two">Content for tab two</div> </o-tabs> tabs.addEventListener('o-change', e => console.log(e.detail)) // { value, prev } tabs.value = 'two' // programmatic switch

o-input

Glassmorphic text input with optional static label and validation states. Fires o-input on every keystroke and o-change on blur.

Default
Error state
Success state
Disabled
<o-input label="Name" placeholder="Alice"></o-input> <o-input label="Email" error="Invalid email"></o-input> <o-input label="User" success></o-input> <o-input label="Key" disabled></o-input> input.addEventListener('o-input', e => console.log(e.detail)) // { value } input.addEventListener('o-change', e => console.log(e.detail)) // { value }

o-skeleton

Pulsing glassmorphic placeholder for loading states. Three variants: block (default), table (matches o-table), and panel (matches o-panel). Swap in wherever content is loading.

Panel variant
Table variant
Simulate load (2s) Reset
Block variant (custom size)
<!-- block (default) --> <o-skeleton width="200px" height="40px" radius="10px"></o-skeleton> <!-- while o-table loads --> <o-skeleton variant="table" rows="5"></o-skeleton> <!-- while o-panel loads --> <o-skeleton variant="panel"></o-skeleton>

o-progress

Fixed top-of-page loading bar. Use the static API or set the value attribute (0–100). OProgress.done() or value="100" animates to full then fades out automatically.

Start Set 50% Set 75% Done
Value attribute (drag to set)
0%
<o-progress></o-progress> // Static API (works without a JS reference) OProgress.start() // auto-increments slowly OProgress.set(60) // jump to 60% OProgress.done() // shoot to 100% then fade out // Attribute API document.querySelector('o-progress').setAttribute('value', '75')

asyncPlus

asyncPlus(...promises) wraps multiple promises and automatically drives the progress bar — starts on the first call, increments as each settles, completes when all are done. Fires a progress-complete event on document.

Simulate parallel requests
3 requests 5 requests 8 requests (1 fails)
// Auto-drives OProgress for any set of promises const results = await asyncPlus( fetch('/api/users'), fetch('/api/posts'), fetch('/api/comments') ) // results: PromiseSettledResult[] — never rejects // Fires when all settle document.addEventListener('progress-complete', e => { console.log(e.detail.results) })

Search + Table + Pagination

Live example combining o-search, o-table (selectable), o-toggle for pages + page size, and o-button for prev/next.

Columns:
Export CSV
// Wire search to filter table data search.addEventListener('o-input', e => { const q = e.detail.query.toLowerCase() table.data = allData.filter(r => Object.values(r).some(v => String(v).toLowerCase().includes(q)) ) }) // Paginate tglPageSize.addEventListener('o-change', e => { pageSize = e.detail.value; render() }) btnPrev.addEventListener('o-click', () => { if (page > 0) { page--; render() } }) btnNext.addEventListener('o-click', () => { if ((page + 1) * pageSize < data.length) { page++; render() } }) // Export selected rows as CSV btnExport.addEventListener('o-click', () => { const rows = table.selected const csv = [cols.map(c => c.label), ...rows.map(r => cols.map(c => r[c.key]))].map(r => r.join(',')).join('\n') const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(new Blob([csv])), download: 'export.csv' }) a.click() })

Custom toast builder

Compose a custom notification — type your message, pick a type, set a duration, and fire. Built entirely from OWC components.

Type
Duration
Fire toast
<o-input id="msg" label="Message" placeholder="Your message..."></o-input> <o-toggle id="type" options="Success,Error,Warning,Info" value="info"></o-toggle> <o-toggle id="dur" options="2s,4s,8s,∞" value="4s"></o-toggle> <o-button>Fire toast</o-button> const durMap = { '2s': 2000, '4s': 4000, '8s': 8000, '∞': 99999 } btn.addEventListener('o-click', () => toast(msg.value || 'Hello!', type.value.toLowerCase(), { duration: durMap[dur.value] }) )

o-panel — kanban board

Three o-panel columns as a kanban board. Move items between columns using the arrow buttons.

To do (3)
In progress (2)
Done (1)
<!-- Three o-panel columns as a kanban board --> <div style="display:flex;gap:12px"> <o-panel id="todo">...</o-panel> <o-panel id="doing">...</o-panel> <o-panel id="done">...</o-panel> </div> // Move item between columns function move(item, fromCol, toCol) { fromCol.items = fromCol.items.filter(i => i !== item) toCol.items = [...toCol.items, item] renderColumn(fromCol) renderColumn(toCol) }

o-panel — as a live widget

An o-panel used as a real-time widget host — draggable clock showing that panels can hold any live content.

00:00:00
Notification queue
+Success +Error +Info Fire queue
Queue: 0 toasts
// Clock: update every second setInterval(() => { clock.textContent = new Date().toLocaleTimeString() }, 1000) // Notification queue: fire with staggered delays queue.forEach((item, i) => { setTimeout(() => toast(item.msg, item.type), i * 800) })

Auto-complete with side effects

Select a country in the dropdown — the currency, flag, and continent fields auto-populate. One o-search driving multiple outputs.

Country
Flag
Currency
Continent
Dialing code
// One o-search drives multiple output fields search.items = countries.map(c => ({ label: c.name, value: c.name })) search.addEventListener('o-select', e => { const c = countries.find(x => x.name === e.detail.value) flag.textContent = c.flag currency.textContent = c.currency continent.textContent = c.continent dial.textContent = c.dial })

Form validation

A submit button that stays disabled until all required fields have content — built with o-search as text inputs and o-button as submit.

Submit Fill all fields to enable
<o-input id="fname" label="First name *" placeholder="Alice"></o-input> <o-input id="email" label="Email *" type="email" placeholder="name@example.com"></o-input> <o-button id="submit" disabled>Submit</o-button> const values = {} inputs.forEach(el => el.addEventListener('o-input', e => { values[el.id] = e.detail.value submit.toggleAttribute('disabled', !Object.values(values).every(Boolean)) }) ) // Show validation state on blur emailInput.addEventListener('o-change', e => { const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.detail.value) emailInput.setAttribute('error', valid ? '' : 'Enter a valid email') })

Multi-search (AND filter)

Two o-search bars filtering the same dataset simultaneously — results must match both queries.

Clear
// Two o-search bars — results must satisfy both filters simultaneously s1.addEventListener('o-input', e => { q1 = e.detail.query; render() }) s2.addEventListener('o-input', e => { q2 = e.detail.query; render() }) function render() { const a = q1.toLowerCase(), b = q2.toLowerCase() table.data = allData.filter(r => (!a || r.name.toLowerCase().includes(a) || r.dept.toLowerCase().includes(a)) && (!b || r.company.toLowerCase().includes(b)) ) }

Kitchen sink

All components inside a single draggable + resizable o-panel — a practical multi-component composition.

Component playground
Search
Mode
Mode: Read
Actions
Save Publish Discard

Live event log

All component events from this page, logged in real time.

Events (0) Clear
No events yet — interact with the components above.