Lit

Lit helps you build web components using reactive properties and declarative templates. Start with a small public contract: what callers supply, what the element renders, and which events announce user actions. Keep internal state distinct from the values a host application owns.

A custom element is a browser integration boundary. Its name and HTML appearance do not establish accessibility, framework compatibility or a secure data flow. Test those behaviors in the actual host rather than assuming that one working component proves every integration.

TL;DR

Quick Example

Save this module as counter.js in a project with Lit installed and a bundler or import map resolving the lit package. Load it as a module and place <wiki-counter></wiki-counter> in the page. The plain JavaScript declaration avoids decorator-toolchain requirements.

The button starts at Count: 0; activating it increments the rendered count and emits count-changed with the new count. A host can listen with addEventListener on the element or an ancestor. The component and event were exercised in a local browser fixture. That check does not establish server rendering, every host framework or a complete accessibility audit.

Core Concepts

Reactive data schedules updates

Lit’s reactive properties trigger its update process. Public reactive properties normally correspond to observed attributes; internal reactive state does not. Attributes are strings, so conversion and reflection rules matter when exposing numbers, booleans or structured values. The example marks count as internal state. Reactive properties

Templates describe the intended DOM

The html template connects text and event bindings to component data. Lit’s event binding invokes the handler with component context. Keep rendering focused on the current state and place external effects in an appropriate lifecycle or event path. Events

Event propagation is part of the contract

bubbles lets an event travel through ancestors; composed permits it to cross shadow boundaries. Choose those settings according to the intended audience. The example dispatches on the host and also supports observation outside a containing shadow root. Do not expose sensitive internal data in a broadly propagating event. Shadow DOM events

Design a component a host can use

Write the contract before adding features. Name each public property, its type, default, accepted range, ownership and update behavior. Define each event’s payload and whether it describes an action, a state change or a request the host may reject.

For structured values, prefer an explicit JavaScript property contract rather than making callers guess a JSON attribute format. If the component is controlled by its host, define how it requests changes and waits for the authoritative value. Avoid an accidental loop where host reflection and component events repeatedly trigger each other.

Use native semantic controls when they match the interaction. The example’s native button provides a useful starting point for keyboard activation; it does not prove that a larger custom widget has correct labeling, focus order or assistive-technology behavior. Test those requirements independently.

Keep styles, lifecycle and trust explicit

Shadow-root styles help isolate component presentation. They also change how application styles reach internal elements. Define intended customization points instead of making host code depend on undocumented internal selectors or DOM structure.

Check the component in the actual themes and layouts it must support. Include text expansion, narrow containers, zoom and high-contrast requirements where applicable. A CSS rule that looks fine in an isolated demo can fail when the element is placed inside a dense form or an application with different typography.

External listeners, observers, timers and subscriptions need lifecycle ownership. Arrange connection and disconnection so removing and reattaching an element does not multiply callbacks. For asynchronous work, account for stale results and disconnected elements; a late response should not overwrite a newer request’s state.

Treat input as data and validate it at the appropriate trust boundary. A template helper is not a reason to insert untrusted raw HTML, construct executable content or expose credentials through attributes and events. Shadow DOM hides some implementation detail from ordinary styling and targeting; it is not an authorization or confidentiality mechanism.

Keep the example’s counter deliberately small. It has no network requests, global listeners or persistence, so it does not demonstrate cancellation, application state ownership or secret handling. Add those features only with an explicit contract and relevant failure cases.

Verify the host integration rather than its reputation

Check the actual framework and version where the element will be consumed. Confirm how the host sets properties, attaches custom-event listeners, renders initial markup and handles element registration. A framework wrapper may be useful when it provides a clearer contract, but validate its behavior rather than assuming it is required everywhere.

For a representative test, mount the component, observe its initial output, activate its control, assert the event payload and wait for the render to settle before inspecting the new text. Then unmount and remount it to catch lifecycle leaks. Test invalid inputs according to the declared policy.

Keep server rendering and hydration as separate acceptance work if they are required. Loading the same client module in a browser does not test server output, hydration mismatch or a host’s streaming behavior. Likewise, a passing local browser is not a compatibility inventory across all supported engines.

Choose Lit when its web-component model fits the required reuse and the team can maintain these contracts. Reconsider a component boundary if it forces excessive host synchronization, inaccessible custom controls or undocumented framework workarounds. The useful measure is a working integration that remains understandable to its maintainers.

Best Practices

Expose a small stable contract

Keep public inputs and events documented. Treat internal DOM and private state as implementation details that hosts should not depend on.

Observe completed updates

Wait for the component’s update boundary in tests before asserting rendered state. Verify the host-visible event separately from the final DOM.

Test the real interaction

Include keyboard, focus, removal/reconnection and the supported host framework. Add server behavior only when it has actually been exercised.

Common Mistakes

Bad: Expecting an attribute to carry an arbitrary JavaScript object.

Correct: Declare a property interface or an explicit conversion contract and test the real host assignment.

Bad: Assuming shadow DOM makes internal data secure.

Correct: Apply normal validation, authorization and confidentiality rules; encapsulation is not security isolation.

Bad: Testing only the rendered number.

Correct: Check action, event payload, propagation, update timing and the host’s response too.

FAQ

Do I need decorators?

No. Lit supports static property declarations, as the example shows. Choose syntax compatible with the project’s actual tooling.

Does count appear as an attribute here?

No. It is internal reactive state in this example. A public property/attribute contract would be a separate design choice.

Can I use the element from another framework?

Web components provide an integration boundary, but property, event, rendering and lifecycle behavior must be checked in that framework and version.

Related Topics

References