Skip to content

Latest commit

 

History

History

README.md

@toolcase/web-components

GitHub npm version

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

Install

npm install @toolcase/web-components

Peer dependencies

  • @toolcase/base ^3.x

Setup

import { register } from '@toolcase/web-components'
import '@toolcase/web-components/style.css'

register() // registers every tc-* element on window.customElements

SSR / Next.js / server-side rendering

All 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.

Usage

<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>

Form controls

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>

Behaviour notes

  • The name attribute on the outer tc-* element is what FormData uses. The inner native control intentionally carries no name to 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] makes form.checkValidity() return false until a value is entered.
  • tc-checkbox-group with multiple selections uses a FormData object internally, so new FormData(form).getAll('fieldname') returns the array of checked values.
  • tc-radio-group inner radio buttons use an internal name for native grouping; the user-facing name attribute on tc-radio-group is forwarded to the form entry via ElementInternals.
  • Browser support: ElementInternals / formAssociated requires Chrome 77+, Firefox 98+, Safari 16.4+. In older browsers these controls degrade gracefully — they render correctly but their values are not included in FormData and form reset/validation do not apply to them.

License

MIT