These are the everyday widgets a UI kit would ship - date pickers,
progress rings, trees, context menus, a docs sidebar - built from the web
platform itself. Each one is a working demo with its complete, copyable
source in a View source panel beneath it. Reach for these when you
are wondering how do I do X in JST
: for everything here the answer is
that you write the HTML and let the platform do the work.
The whole page is hermetic: every image is an inline SVG data URI and nothing is fetched over the network. Serve the repo root and open this file to try each demo.
A date, time and quantity form with no component. Native
input[type=date], input[type=time] and
input[type=number] give you a picker, keyboard entry and
locale-aware formatting in every browser, and the classless base already
styles them with the theme variables.
<jst-field><label>Date</label>
<input type="date" value="2026-07-10">
</jst-field>
<jst-field><label>Time</label>
<input type="time" value="09:30">
</jst-field>
<jst-field><label>Quantity</label>
<input type="number" min="0" max="99" value="3">
</jst-field>
<!-- No CSS. jst-layout.css styles native inputs by element. -->
A styled file picker with no component: the native
input[type=file] handles selection, and the classless base
skins its button through ::file-selector-button.
<jst-field>
<label>Attachment</label>
<input type="file">
</jst-field>
/* Already in jst-layout.css - shown for reference: */
::file-selector-button {
font: inherit;
padding: var(--jst-space-2xs) var(--jst-space-s);
border: var(--jst-border-width) solid var(--jst-border);
border-radius: var(--jst-radius);
background: var(--jst-surface);
color: var(--jst-fg);
cursor: pointer;
}
Native <video> gives you play controls,
fullscreen, picture-in-picture, a poster and <track>
captions with no library. (This demo has a poster but no media source, so
it shows the frame without playing.)
<video controls width="320" poster="poster.jpg">
<source src="clip.mp4" type="video/mp4">
<track kind="captions" default srclang="en" label="English"
src="captions.vtt">
Your browser cannot play this video.
</video>
<!-- controls, fullscreen, PiP and captions are all native. -->
A percentage indicator as a ring and a bar. The ring is a
conic-gradient() driven by one --value custom
property; the linear case is just <progress>. Drag the
slider to drive both.
<div class="ring-demo" style="--value: 68">
<div class="ring"><span class="ring-num">68%</span></div>
<input type="range" min="0" max="100" value="68"
oninput="const d = this.closest('.ring-demo');
d.style.setProperty('--value', this.value);
d.querySelector('.ring-num').textContent = this.value + '%'">
<progress value="0.68" max="1"></progress>
</div>
.ring {
inline-size: 5.5rem; aspect-ratio: 1; border-radius: 50%;
display: grid; place-items: center;
background: conic-gradient(var(--jst-accent)
calc(var(--value) * 1%), var(--jst-border) 0);
mask: radial-gradient(farthest-side, transparent 58%, #000 59%);
}
A multi-step progress header (checkout, onboarding) built from
an ordered list: CSS counters number the steps and
aria-current="step" marks the one you are on.
<ol class="steps">
<li>Cart</li>
<li aria-current="step">Shipping</li>
<li>Payment</li>
<li>Review</li>
</ol>
.steps { counter-reset: step; display: flex; list-style: none; }
.steps li { counter-increment: step; flex: 1; text-align: center; }
.steps li::before { content: counter(step); /* numbered bubble */ }
.steps li[aria-current="step"]::before {
background: var(--jst-accent); color: var(--jst-accent-fg);
}
A collapsible tree (file explorer, nav) from nested
<details>/<summary> with connector
lines. Expand, collapse and keyboard focus all come from the platform.
<div class="tree">
<details open>
<summary>src</summary>
<details>
<summary>components</summary>
<div class="leaf">button.js</div>
</details>
<div class="leaf">index.js</div>
</details>
</div>
.tree details { border-inline-start: 1px solid var(--jst-border);
padding-inline-start: var(--jst-space-s); }
A label that lifts out of the field on focus or input, in pure
CSS with :placeholder-shown and :focus-within - no
JavaScript at all.
<label class="float">
<input type="text" placeholder=" ">
<span>Email address</span>
</label>
.float { position: relative; }
.float span { position: absolute; inset-block-start: 0.85rem; transition: 0.15s; }
.float input:focus + span,
.float input:not(:placeholder-shown) + span {
transform: translateY(-0.75rem) scale(0.8);
}
A verification-code field. One input with
autocomplete="one-time-code" and
inputmode="numeric"; on a phone the platform offers the SMS
code for one-tap autofill.
<label>Verification code</label>
<input class="otp" type="text" inputmode="numeric"
autocomplete="one-time-code" maxlength="6"
pattern="[0-9]*" placeholder="000000">
.otp { font-family: var(--jst-font-mono); letter-spacing: 0.6em;
text-align: center; }
<!-- iOS/Android surface the SMS code above the keyboard. -->
Fade-and-lift a panel in on open and reverse it on close, in
CSS alone. @starting-style plus
transition-behavior: allow-discrete animate a popover both
ways, including the switch off display: none. No library.
Fades and lifts in on open, and reverses on close because display and overlay transition with allow-discrete.
<button command="toggle-popover" commandfor="pop">Toggle</button>
<div popover id="pop" class="anim-pop">Animated both ways.</div>
.anim-pop {
opacity: 0; transform: translateY(0.5rem) scale(0.97);
transition: opacity 0.25s, transform 0.25s,
overlay 0.25s allow-discrete,
display 0.25s allow-discrete;
}
.anim-pop:popover-open { opacity: 1; transform: none; }
@starting-style {
.anim-pop:popover-open { opacity: 0; transform: translateY(0.5rem); }
}
A drag-to-reveal before/after image wipe. An
input[type=range] writes a --pos custom property;
the top image is clipped with clip-path: inset(). The one-line
oninput is a native inline handler. Both images are inline
SVG.
<div class="compare" style="--pos: 50%">
<img class="under" src="after.jpg" alt="After">
<img class="over" src="before.jpg" alt="Before">
<div class="handle"></div>
<input type="range" min="0" max="100" value="50"
oninput="this.closest('.compare')
.style.setProperty('--pos', this.value + '%')">
</div>
.compare { position: relative; --pos: 50%; }
.compare img { position: absolute; inset: 0; object-fit: cover; }
.compare .over { clip-path: inset(0 calc(100% - var(--pos)) 0 0); }
.compare .handle { inset-inline-start: var(--pos); }
Two resizable panes side by side, using native CSS
resize on one pane inside a flex row. Drag the handle at the
pane's bottom-inline corner.
Limits: resize gives a corner grip only (no full-height
divider), one axis at a time, and clamps with
min/max-inline-size. When you need a draggable bar between
two panes, that is a real component - reach for a library or write one.
<div class="split">
<div class="pane pane-a">Left</div>
<div class="pane pane-b">Right</div>
</div>
.split { display: flex; gap: var(--jst-space-2xs); }
.split .pane-a {
resize: horizontal; overflow: auto;
inline-size: 45%; min-inline-size: 5rem; max-inline-size: 85%;
}
.split .pane-b { flex: 1; }
A table of contents that highlights the section you are
currently reading - the classic docs scrollspy
, and the one recipe
here that is a JST component. Its onreveal synthetic event
fires as each section scrolls into view; the handler records the active id
and JST re-highlights the list. For the same idea as a nested sidebar with
an active bar, see the docs sidebar
below.
<script type="jst" name="scroll-spy" attributes="sections active">
<nav class="spy-toc">
<ol>
$ (sections || []).forEach(s => {
<li><a href="#$(s.id)"
aria-current="$(active === s.id ? 'true' : 'false')">
$(s.label)</a></li>
$ })
</ol>
</nav>
$ (sections || []).forEach(s => {
<section id="$(s.id)" jst-key="$(s.id)"
onreveal="if (el.active !== this.id) el.active = this.id">
<h4>$(s.label)</h4><p>$(s.body)</p>
</section>
$ })
</script>
<scroll-spy sections='[...]' active="spy-intro"></scroll-spy>
Themeable, text-aligned icons from an inline SVG sprite
(<symbol> / <use>) sized by the
jst-icon primitive. Any icon set drops straight in: Lucide,
Heroicons, Font Awesome Free - copy their paths into symbols, no runtime
library.
<!-- Define the sprite once (paste any icon set's paths here) -->
<svg width="0" height="0" aria-hidden="true">
<symbol id="ic-home" viewBox="0 0 24 24" ...>
<path d="M3 10.5 12 3l9 7.5"/> ...
</symbol>
</svg>
<!-- Use it anywhere, sized by the jst-icon primitive. The viewBox on the
referencing svg matters: no viewBox means no aspect ratio, and Safari
collapses the auto width to zero. -->
<jst-icon><svg viewBox="0 0 24 24"><use href="#ic-home"/></svg> Home</jst-icon>
/* jst-icon (in jst-layout.css) aligns and sizes the svg to the text. */
Almost everything here is plain HTML and CSS with no runtime; a couple add a few lines of vanilla JavaScript, and exactly one - the scrollspy - is a small JST component that turns a platform observation into rendered state. That is the pattern worth taking away: reach for the platform first, keep the markup in your hands, and add JST only where it earns a place.