Framework-free HTML5 Web Components (tc-*) with their own from-scratch toolcase styling — no Bootstrap dependency, but a Bootstrap-compatible class and 12-column grid API. Drop them into plain HTML, React, Vue, Svelte, Angular, or any other stack — no framework required.
📖 Live demos: toolcase.kalevski.dev/web-components
npm install @toolcase/web-components@toolcase/base ^3.x
import { register } from '@toolcase/web-components'
import '@toolcase/web-components/style.css'
register() // registers every tc-* element on window.customElementsAll tc-* component classes extend HTMLElement, which does not exist in Node.js. A top-level import '@toolcase/web-components' in server-rendered code will throw ReferenceError: HTMLElement is not defined at module evaluation time, before any idempotency guard can run.
Node.js environments (Next.js SSR, RSC, prerender, Vitest with environment: 'node') automatically resolve the node export condition to a no-op stub that exports a safe register() — so require/import from server code will not throw.
Client-side registration must use a dynamic import inside useEffect or another client-only boundary, never a static top-level import in a component file that is also rendered on the server:
// Next.js app directory (client component) — safe pattern
'use client'
import { useEffect } from 'react'
useEffect(() => {
void import('@toolcase/web-components').then(m => m.register())
}, [])The stylesheet import is also unsafe at the top level in RSC — put it in a client boundary or in _app.tsx / layout.tsx alongside the dynamic import.
<tc-button variant="primary">Save</tc-button>
<tc-alert variant="success" dismissible>Saved successfully.</tc-alert>
<tc-modal title="Confirm" id="confirm-modal">Are you sure?</tc-modal>tc-input, tc-textarea, tc-select, tc-switch, tc-radio-group, and tc-checkbox-group are form-associated custom elements — they participate in <form> submission, reset, and validation via the ElementInternals API.
<form id="demo">
<tc-input name="username" required></tc-input>
<tc-select name="role">
<tc-option value="admin">Admin</tc-option>
<tc-option value="user">User</tc-option>
</tc-select>
<button type="submit">Submit</button>
</form>
<script>
const form = document.getElementById('demo')
form.addEventListener('submit', e => {
e.preventDefault()
const data = new FormData(form)
console.log(data.get('username'), data.get('role'))
})
// form.reset() clears all tc-* controls back to their initial values
// form.checkValidity() / form.reportValidity() honour tc-input[required] etc.
</script>- The
nameattribute on the outertc-*element is whatFormDatauses. The inner native control intentionally carries nonameto avoid double-submission. form.reset()restores each control to the value it had when first connected to the DOM (the HTML attribute value, or empty).- Validity is mirrored from the inner control:
tc-input[required]makesform.checkValidity()returnfalseuntil a value is entered. tc-checkbox-groupwith multiple selections uses aFormDataobject internally, sonew FormData(form).getAll('fieldname')returns the array of checked values.tc-radio-groupinner radio buttons use an internal name for native grouping; the user-facingnameattribute ontc-radio-groupis forwarded to the form entry viaElementInternals.- Browser support:
ElementInternals/formAssociatedrequires Chrome 77+, Firefox 98+, Safari 16.4+. In older browsers these controls degrade gracefully — they render correctly but their values are not included inFormDataand form reset/validation do not apply to them.