@amazon/vinyl-tsx
v1.1.1
Published
TSX declarative markup for creating UI
Readme
@amazon/vinyl-tsx
A lightweight, reactive JSX runtime for creating dynamic DOM elements with
TypeScript support. Provides a declarative way to build user interfaces with
reactive data binding via @amazon/vinyl-observable.
Features
- Reactive properties — bind observable values to DOM properties that update automatically.
- Hook extensions — enhanced DOM manipulation through custom hooks (
style,visible,classList). - Lifecycle management — automatic cleanup and connection tracking for DOM elements.
- TypeScript support — full type safety with utility types for property extraction.
- Zero dependencies — lightweight runtime with no external framework dependencies.
- Performance optimized — efficient updates with passive event handlers and selective rendering.
Install
npm install @amazon/vinyl-tsx @amazon/vinyl-util @amazon/vinyl-observableBasic Usage
Setting up JSX
Configure your TypeScript compiler to use the vinyl-tsx JSX factory:
// tsconfig.json
{
"compilerOptions": {
"jsx": "preserve",
"jsxFactory": "jsx",
"jsxFragmentFactory": "Fragment"
}
}Creating Elements
import { jsx } from '@amazon/vinyl-tsx'
// Basic element creation
const button = <button>Click me</button>
// With properties
const input = <input type="text" placeholder="Enter text" disabled={false} />
// With children
const container = (
<div className="container">
<h1>Title</h1>
<p>Content goes here</p>
</div>
)Reactive Properties
Use @amazon/vinyl-observable to create reactive properties that automatically
update the DOM:
import { data } from '@amazon/vinyl-observable'
import { jsx } from '@amazon/vinyl-tsx'
const count = data(0)
const isVisible = data(true)
const counter = (
<div>
<span>Count: {count}</span>
<button onclick={() => count.value++}>Increment</button>
<button visible={isVisible}>Toggle me</button>
</div>
)
count.value = 5 // DOM updates to show "Count: 5"
isVisible.value = false // Button becomes hiddenHook Extensions
Style Hook
Apply CSS styles with support for both static and observable values:
import { data } from '@amazon/vinyl-observable'
const color = data('red')
const fontSize = data('16px')
const styledDiv = (
<div
style={{
color, // Observable value
fontSize, // Observable value
backgroundColor: 'blue', // Static value
padding: '10px', // Static value
}}
>
Styled content
</div>
)
color.value = 'green' // Text color changes to greenVisibility Hook
Control element visibility with the visible property:
const isVisible = data(true)
const conditionalElement = (
<div visible={isVisible}>This element can be hidden/shown</div>
)
isVisible.value = false // Sets display: none
isVisible.value = true // Removes display propertyClassList Hook
Manage CSS classes dynamically with an array of static strings and/or observable values:
const theme = data<string | null>('dark')
const element = <div classList={['base-class', theme]}>Content</div>
// Element has classes: base-class dark
theme.value = 'light' // Updates to: base-class light
theme.value = null // Updates to: base-classConnection Hook
Execute code when elements are connected to or disconnected from the DOM:
const element = (
<div
onConnect={(el) => {
console.log('Element connected:', el)
return () => {
console.log('Element disconnected:', el)
}
}}
>
Content
</div>
)Event Handlers
Touch and scroll events use passive listeners automatically:
const element = (
<div
ontouchstart={(e) => console.log('Touch start')}
ontouchmove={(e) => console.log('Touch move')}
onwheel={(e) => console.log('Wheel')}
onmousedown={(e) => console.log('Mouse down')}
>
Interactive content
</div>
)Fragments
Use fragments to group elements with a wrapper div using display: contents:
import { Fragment } from '@amazon/vinyl-tsx'
const list = (
<Fragment>
<li>Item 1</li>
<li>Item 2</li>
</Fragment>
)
const list2 = (
<>
<li>Item 1</li>
<li>Item 2</li>
</>
)Custom Components
interface ButtonProps {
text: string
onClick: () => void
variant?: 'primary' | 'secondary'
}
function Button({ text, onClick, variant = 'primary' }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} onclick={onClick}>
{text}
</button>
)
}
const myButton = <Button text="Click me" onClick={() => alert('Clicked!')} />Conditional Rendering
Unlike React, vinyl-tsx does not re-render — JSX expressions are evaluated once
at creation time. Use the visible hook to reactively show or hide content:
const showDetails = data(false)
const userCard = (
<div>
<h3>John</h3>
<div visible={showDetails}>
<p>Email: [email protected]</p>
</div>
<button onclick={() => (showDetails.value = !showDetails.value)}>
Toggle Details
</button>
</div>
)Best Practices
Initialize the connected observer once in your application to enable lifecycle management:
import { initializeConnectedObserver } from '@amazon/vinyl-tsx' const cleanup = initializeConnectedObserver()Use observables for dynamic content — prefer observable values for properties that change over time.
Leverage hook extensions —
style,visible, andclassListcover most common DOM manipulations.Type safety — let TypeScript check your props and children.
Memory management — observable subscriptions are cleaned up automatically when elements are disconnected.
License
Apache-2.0
