"After playing around with Replicant, I realized I could build Web Components without React — actually, even without Replicant.
I'm wondering if it's called 'Replicant' because of Blade Runner (I love Blade Runner).
Maybe I could name my own library something similar... Tyrell? No, that feels pretentious.
Anyway, I don't really want to type something long like
tyrell-button. It should be shorter — maybety-button.Yes! Let's call it ty."
Web components that work everywhere. React, Vue, HTMX, vanilla JS, ClojureScript — use what you like.
| Vanilla JS Guide | React Guide |
|---|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gersak/ty/dist/ty.css">
<script src="https://cdn.jsdelivr.net/npm/@gersak/ty/dist/ty.js"></script>
Then use components anywhere:
<ty-button flavor="primary">Click me</ty-button>
<ty-dropdown label="Country" placeholder="Select...">
<option value="us">United States</option>
<option value="de">Germany</option>
</ty-dropdown>
Add to deps.edn:
{:deps {dev.gersak/ty {:mvn/version "0.3.0"} ;; Router, i18n, layout
dev.gersak/ty-icons {:mvn/version "0.1.1"}}} ;; Tree-shakeable icons
(ns app.core
(:require [uix.core :refer [defui $]]
[ty.lucide :as lucide]))
;; Register only icons you use - Closure Compiler removes the rest
(defonce _ (js/window.tyIcons.register
#js {:check lucide/check
:calendar lucide/calendar
:globe lucide/globe}))
(defui app []
(let [[selected set-selected] (uix.core/use-state nil)]
($ :div.ty-canvas.min-h-screen.p-8
($ :div.ty-elevated.p-6.rounded-lg.max-w-md.space-y-4
($ :h1.ty-text++.text-2xl.font-bold "Book a Demo")
($ :ty-date-picker
{:label "Select Date"
:placeholder "Pick a date..."
:value selected
:on-change #(set-selected (.. % -detail -value))})
($ :ty-dropdown
{:label "Timezone"
:placeholder "Select timezone..."}
($ :option {:value "utc"} "UTC")
($ :option {:value "cet"} "Central European")
($ :option {:value "pst"} "Pacific Standard"))
($ :ty-button
{:flavor "primary"
:disabled (nil? selected)}
($ :ty-icon {:name "check" :slot "start"})
"Confirm Booking")))))
(ns app.core
(:require [replicant.dom :as d]
[ty.lucide :as lucide]))
(defonce _ (js/window.tyIcons.register
#js {:user lucide/user
:mail lucide/mail
:send lucide/send}))
(defn contact-form [state]
[:div.ty-canvas.min-h-screen.p-8
[:div.ty-elevated.p-6.rounded-lg.max-w-md.space-y-4
[:h1.ty-text++.text-2xl.font-bold "Contact Us"]
[:ty-input
{:label "Name"
:placeholder "Your name"
:value (:name @state)
:on {:input #(swap! state assoc :name (.. % -target -value))}}
[:ty-icon {:name "user" :slot "start"}]]
[:ty-input
{:label "Email"
:type "email"
:placeholder "you@example.com"
:value (:email @state)
:on {:input #(swap! state assoc :email (.. % -target -value))}}
[:ty-icon {:name "mail" :slot "start"}]]
[:ty-textarea
{:label "Message"
:placeholder "How can we help?"
:rows 4
:value (:message @state)
:on {:input #(swap! state assoc :message (.. % -target -value))}}]
[:ty-button
{:flavor "primary"
:on {:click #(js/alert "Sent!")}}
[:ty-icon {:name "send" :slot "start"}]
"Send Message"]]])
(defonce state (atom {:name "" :email "" :message ""}))
(d/render (js/document.getElementById "app")
(contact-form state))
Component-based routing with segments and authorization:
(ns app.routes
(:require [ty.router :as router]))
;; Initialize router with base path
(router/init! "") ;; or "my-app" for /my-app/... URLs
;; Define routes by linking to parent
(router/link ::router/root
[{:id :app/home
:segment "home"
:landing 100} ;; Landing priority (highest wins)
{:id :app/users
:segment "users"}
{:id :app/admin
:segment "admin"
:roles #{:admin}}]) ;; Authorization
;; Nested routes
(router/link :app/users
[{:id :app/user-detail
:segment "detail"}]) ;; /users/detail
;; Navigate
(router/navigate! :app/home)
(router/navigate! :app/user-detail {:tab "profile"}) ;; with query params
;; Check if route is active
(router/rendered? :app/users) ;; true if on /users or /users/detail
(router/rendered? :app/users true) ;; true only if exactly on /users
;; Query params
(router/query-params) ;; => {:tab "profile"}
(router/set-query! {:page 2})
Protocol-based formatting with Intl API:
(ns app.i18n
(:require [ty.i18n :as i18n]
[ty.i18n.number :as num]
[ty.i18n.time :as time]))
;; Current locale (auto-detected from browser)
i18n/*locale* ;; => :en_US
;; Number formatting
(num/format-number 1234567.89) ;; "1,234,567.89"
(num/format-currency 99.99 "EUR") ;; "€99.99"
(num/format-percent 0.156) ;; "16%"
(num/format-compact 1500000) ;; "1.5M"
;; Date formatting
(time/format-date (js/Date.)) ;; "2/19/2026"
(time/format-date-full (js/Date.)) ;; "Wednesday, February 19, 2026"
(time/format-relative -3 "day") ;; "3 days ago"
;; With explicit locale
(num/format-currency 1234.50 "EUR" :de_DE) ;; "1.234,50 €"
(time/format-date-full (js/Date.) :hr) ;; "srijeda, 19. veljače 2026."
;; Protocol-based translation (i18n/t)
;; Numbers are extended to support direct translation
(i18n/t 1234.56) ;; "1,234.56" (current locale)
(i18n/t 1234.56 "EUR") ;; "€1,234.56" (as currency)
(i18n/t 1234.56 :de_DE) ;; "1.234,56" (German locale)
(i18n/t 1234.56 :de_DE {:style "currency" :currency "EUR"}) ;; "1.234,56 €"
Use ty.shim to turn any ClojureScript render function into a Web Component:
(ns app.components
(:require [replicant.dom :as d]
[ty.shim :as shim]))
(defn greeting [name]
[:div.ty-elevated.p-4.rounded-lg
[:h2.ty-text+ "Hello, " name "!"]
[:ty-button {:flavor "primary"} "Wave"]])
(defn render! [^js el]
(d/render (shim/ensure-shadow el)
(greeting (or (shim/attr el "name") "World"))))
(shim/define! "my-greeting"
{:observed [:name]
:connected render!
:attr (fn [el _] (render! el))})
<my-greeting name="Clojure"></my-greeting>
Component Building Guide → | Code Splitting →
| Component | Description |
|---|---|
ty-button | Semantic buttons with flavors, sizes, and icon slots |
ty-input | Text input with labels, validation, numeric formatting, debounce |
ty-textarea | Multi-line text with auto-resize and character count |
ty-checkbox | Styled checkbox with indeterminate state |
ty-dropdown | Searchable select with keyboard nav and mobile modal |
ty-multiselect | Multi-select with tags and search |
ty-calendar | Full calendar with date selection and form integration |
ty-date-picker | Calendar dropdown for date input |
ty-tabs / ty-tab | Carousel tabs with smooth animations |
ty-wizard / ty-step | Step-by-step wizard with progress tracking |
ty-modal | Native dialog with backdrop and focus management |
ty-popup | Anchored popover with smart positioning |
ty-tooltip | Hover tooltips with placement options |
ty-icon | SVG icons from Lucide, Heroicons, Material, FontAwesome |
ty-tag | Removable tags for selections |
ty-copy | Click-to-copy with visual feedback |
ty-scroll-container | Scrollable area with fade indicators |
See all components in action →
Semantic CSS classes that flip correctly for dark mode:
<!-- Surfaces -->
<div class="ty-canvas">...</div> <!-- App background -->
<div class="ty-content">...</div> <!-- Main content -->
<div class="ty-elevated">...</div> <!-- Cards, panels -->
<div class="ty-floating">...</div> <!-- Modals, dropdowns -->
<!-- Text emphasis -->
<h1 class="ty-text++">Maximum</h1> <!-- Strongest -->
<h2 class="ty-text+">High</h2>
<p class="ty-text">Normal</p>
<span class="ty-text-">Muted</span>
<small class="ty-text--">Faint</small> <!-- Weakest -->
<!-- Semantic colors -->
<span class="ty-text-primary">Primary</span>
<span class="ty-text-success">Success</span>
<span class="ty-text-danger">Danger</span>
<div class="ty-bg-warning- p-2">Warning background</div>
MIT License
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |