Getting Started with Duct UI
Duct UI is an opinionated framework that brings clarity to web development through explicit separation of concerns. Unlike React’s all-in-one approach, Duct clearly separates templates from logic, making your code more maintainable and easier to understand.
Why Duct?
Duct was born from the increasing complexity in debugging complex web applications where render logic, state management, and side effects are all intertwined. We believe that templates should be templates and logic should be logic.
With the rise of AI driven development and websites being interfaces to LLMs in more than
one way, the way we build and use websites are also changing. So we felt it is time for a
fresh look at how we build interactive websites. More on this later.
Your First Component
Let’s create a simple counter component to understand Duct’s approach:
import { createBlueprint, type BaseProps } from "@duct-ui/core/blueprint"
// Define your props
interface CounterProps {
initialCount?: number
step?: number
}
// Pure template - just structure and presentation
function render(props: BaseProps<CounterProps>) {
const { initialCount = 0, step = 1 } = props
return (
<div class="counter">
<button data-decrement class="btn">-{step}</button>
<span data-count class="mx-4">{initialCount}</span>
<button data-increment class="btn">+{step}</button>
</div>
)
}
// Logic separated from rendering
function bind(el: HTMLElement, eventEmitter, props: CounterProps) {
const countEl = el.querySelector('[data-count]')!
const incrementBtn = el.querySelector('[data-increment]')!
const decrementBtn = el.querySelector('[data-decrement]')!
let count = props.initialCount || 0
const step = props.step || 1
function updateDisplay() {
countEl.textContent = count.toString()
}
function handleIncrement() {
count += step
updateDisplay()
}
function handleDecrement() {
count -= step
updateDisplay()
}
// Add event listeners
incrementBtn.addEventListener('click', handleIncrement)
decrementBtn.addEventListener('click', handleDecrement)
return {
release: () => {
// Proper cleanup - remove event listeners
incrementBtn.removeEventListener('click', handleIncrement)
decrementBtn.removeEventListener('click', handleDecrement)
}
}
}
// Create the component
const Counter = createBlueprint(
{ id: "my-app/counter" },
render,
{ bind }
)
export default Counter
Key Concepts
1. Render Function
The render function is pure - it takes props and returns JSX. No hooks, no state, no side effects. Just a template.
2. Bind Function
The bind function is where all your component logic lives. It receives the rendered DOM element and sets up all interactivity.
3. Lifecycle
Duct components have a clear, predictable lifecycle:
- Render: Generate HTML from props
- Load (optional): Fetch async data
- Bind: Attach event listeners and logic
- Release: Cleanup when component unmounts
4. No Magic
Everything in Duct is explicit. No hidden re-renders, no mysterious hook dependencies, no virtual DOM diffing. You’re in control.
Next Steps
Now that you understand the basics:
- Explore the component lifecycle in detail
- Learn about async data loading
- Discover how to compose components
- See how Duct compares to other frameworks
Welcome to a clearer way of building web applications. Welcome to Duct UI!