Liking cljdoc? Tell your friends :D

"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 — maybe ty-button.

Yes! Let's call it ty."

ty

jsDelivr NPM Version Clojars Project

Web components that work everywhere. React, Vue, HTMX, vanilla JS, ClojureScript — use what you like.

Live Demo & Docs →

Vanilla JS GuideReact Guide

Load from CDN

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

ClojureScript

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

UIx

(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")))))

Replicant

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

Router

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

i18n

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

Build Your Own Components

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 →


Components

ComponentDescription
ty-buttonSemantic buttons with flavors, sizes, and icon slots
ty-inputText input with labels, validation, numeric formatting, debounce
ty-textareaMulti-line text with auto-resize and character count
ty-checkboxStyled checkbox with indeterminate state
ty-dropdownSearchable select with keyboard nav and mobile modal
ty-multiselectMulti-select with tags and search
ty-calendarFull calendar with date selection and form integration
ty-date-pickerCalendar dropdown for date input
ty-tabs / ty-tabCarousel tabs with smooth animations
ty-wizard / ty-stepStep-by-step wizard with progress tracking
ty-modalNative dialog with backdrop and focus management
ty-popupAnchored popover with smart positioning
ty-tooltipHover tooltips with placement options
ty-iconSVG icons from Lucide, Heroicons, Material, FontAwesome
ty-tagRemovable tags for selections
ty-copyClick-to-copy with visual feedback
ty-scroll-containerScrollable area with fade indicators

See all components in action →


Design System

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>

Links


MIT License

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close