Drops
How much code it would take
with other frameworks?
Counter
<div :scope="{ count: 0 }">
<button :onclick="count++">
Clicked <span :text="count">0</span> times
</button>
</div>
Temperature converter
<div :scope="{
c: 0, f: 32,
setC(v) { c=v; f=c*9/5+32 },
setF(v) { f=v; c=(f-32)*5/9 }
}">
<input type="number" :value="c"
:change="setC" /> °C =
<input type="number" :value="f"
:change="setF" /> °F
</div>
°C =
°F
Flight booker
<div :scope="{
mode: 'one-way',
go: '2025-06-01',
back: '2025-06-15',
setMode(v) { mode = v },
setGo(v) { go = v },
setBack(v) { back = v },
get ok() {
return mode === 'one-way'
|| back >= go }
}">
<select :value="mode"
:change="setMode">
<option value="one-way">One-way</option>
<option value="return">Return</option>
</select>
<input type="date" :value="go"
:change="setGo" />
<input type="date" :value="back"
:change="setBack"
:disabled="mode==='one-way'" />
<button :disabled="!ok">Book</button>
</div>
Timer
<div :scope="{ max: 15, elapsed: 0,
setMax(v) { this.max = v } }"
:fx="() => {
let id = setInterval(
() => elapsed < max
&& elapsed++, 1000)
return () => clearInterval(id)
}">
<progress :value="elapsed" :max="max">
</progress>
<p><span :text="elapsed">0</span>
/ <span :text="max"></span>s</p>
<input type="range" :value="max"
:change="setMax" min="1" max="60" />
<button :onclick="elapsed=0">Reset</button>
</div>
0 / s
CRUD
<div :scope="{
items: ['Hans','Claus','Karl'],
sel: 0, name: '', filter: '',
setFilter(v) { filter = v },
pick(v) { sel=v; name=items[v] },
setName(v) { name = v },
match(n) { return n.toLowerCase()
.includes(filter.toLowerCase()) },
add() { items.push(name); name='' },
upd() { items[sel] = name },
del() { items.splice(sel,1);
sel=Math.min(sel,items.length-1) }
}">
<input :value="filter"
:change="setFilter" />
<select size="4" :value="sel"
:change="pick">
<option :each="n,i in items"
:if="match(n)" :value="i"
:text="n"></option>
</select>
<input :value="name" :change="setName"/>
<button :onclick="add()">Create</button>
<button :onclick="upd()">Update</button>
<button :onclick="del()">Delete</button>
</div>
Tabs
<div :scope="{ tab: 'one' }">
<button :class="{active: tab==='one'}"
:onclick="tab='one'">One</button>
<button :class="{active: tab==='two'}"
:onclick="tab='two'">Two</button>
<div :if="tab==='one'">First.</div>
<div :else>Second.</div>
</div>
First tab content.
Second tab content.
Third tab content.
Modal
<div :scope="{ open: false }">
<button :onclick="open=true">Open</button>
<dialog :if="open"
:onclick.self="open=false"
:onkeydown.window.escape="open=false">
<h2>Hello</h2>
<p>Content here.</p>
<button :onclick="open=false">Close</button>
</dialog>
</div>
Accordion
<div :each="item, i in items"
:scope="{ open: i===0 }">
<button :onclick="open=!open"
:text="item.title"></button>
<div :if="open" :text="item.body"></div>
</div>
Dropdown
<div :scope="{ open: false }">
<button :onclick="open=!open">Menu ▾</button>
<ul :if="open">
<li :onclick="open=false">Edit</li>
<li :onclick="open=false">Copy</li>
<li :onclick="open=false">Delete</li>
</ul>
</div>
Checkbox group
<div :scope="{
sel: ['Apple','Cherry'],
all: ['Apple','Banana','Cherry'],
has(f) { return sel.includes(f) },
flip(f,on) { on ? sel.push(f)
: sel.splice(sel.indexOf(f),1) }
}">
<label :each="f in all">
<input type="checkbox" :value="has(f)"
:onchange="e=>flip(f,e.target.checked)"/>
<span :text="f"></span>
</label>
<p>: <b :text="sel.join(', ')"></b></p>
</div>
Selected:
Responsive grid
<div :scope="{ cols: 3 }"
:resize="({width}) => cols =
Math.max(1,Math.floor(width/120))">
<div :style="{ display:'grid',
gridTemplateColumns:
'repeat('+cols+',1fr)' }">
<div :each="n in 6" :text="n"></div>
</div>
</div>
Dark mode
<button :scope="{ dark: false }"
:onclick="dark = !dark"
:fx="document.documentElement.style
.colorScheme = dark ? 'dark' : 'light'"
:text="dark ? '☀️ Light' : '🌙 Dark'">
</button>
Accent color
<div :scope="{ h: 262 }">
<button :onclick="h=262">Blue</button>
<button :onclick="h=180">Teal</button>
<button :onclick="h=0">Rose</button>
<button :onclick="h=80">Amber</button>
<div :style.document="{
'--accent-h': h }"></div>
</div>
Sortable table
<div :scope="{
rows: [
{name:'Alice', age:32},
{name:'Bob', age:25},
{name:'Carol', age:41}],
col: 'name', asc: true,
sort(c) {
if (col===c) asc=!asc
else { col=c; asc=true }
rows.sort((a,b) => asc
? (a[col]>b[col]?1:-1)
: (a[col]<b[col]?1:-1))
}
}">
<table>
<tr>
<th :onclick="sort('name')">Name</th>
<th :onclick="sort('age')">Age</th>
</tr>
<tr :each="r in rows">
<td :text="r.name"></td>
<td :text="r.age"></td>
</tr>
</table>
</div>
| Name | Age |
|---|---|
Color converter
<div :scope="{
hex: '#1548c1',
r:21, g:72, b:193,
setHex(v) {
hex = v;
let n = parseInt(v.slice(1), 16);
r=n>>16; g=n>>8&255; b=n&255
},
setRGB() {
hex = '#' + [r,g,b]
.map(v => v.toString(16)
.padStart(2,'0')).join('')
}
}">
<input :value="hex" :change="setHex" />
<div :style="{background: hex,
width:'3em', height:'3em'}"></div>
<label>R <input type="range"
:value="r" :change="v => {r=v; setRGB()}"
min="0" max="255" /></label>
<label>G <input type="range"
:value="g" :change="v => {g=v; setRGB()}"
min="0" max="255" /></label>
<label>B <input type="range"
:value="b" :change="v => {b=v; setRGB()}"
min="0" max="255" /></label>
</div>
rgb(, , )
Tip calculator
<div :scope="{
bill: 50, pct: 15, split: 2,
setPct(v) { pct = v },
setSplit(v) { split = v },
setBill(v) { bill = v },
get tip() { return bill*pct/100 },
get total() { return bill+tip },
get each() { return total / split }
}">
<input type="number" :value="bill"
:change="setBill" />
<input type="range" :value="pct"
:change="setPct" min="0" max="50" />
<span :text="pct + '%'"></span>
<input type="number" :value="split"
:change="setSplit" min="1" />
<b>Tip: $<span :text="tip.toFixed(2)"></span>
· Each: $<span :text="each.toFixed(2)">
</span></b>
</div>
Tip: $ · Each: $
Word counter
<div :scope="{ txt: '', set(v){txt=v} }">
<textarea :value="txt"
:change="set" rows="4"></textarea>
<p><span :text="txt.trim()
? txt.trim().split(/\s+/).length
: 0"></span> words ·
<span :text="txt.length"></span> chars</p>
</div>
words · chars · lines
Unit converter
<div :scope="{
val: 1,
from: 'km', to: 'mi',
units: {km:1, mi:1.60934,
m:0.001, ft:0.000305,
yd:0.000914},
setVal(v) { val = v },
setFrom(v) { from = v },
setTo(v) { to = v },
get result() {
return val*units[from]/units[to]
}
}">
<input type="number" :value="val"
:change="setVal" />
<select :value="from" :change="setFrom">
<option :each="u in Object.keys(units)"
:value="u" :text="u"></option>
</select> =
<b :text="result.toFixed(4)"></b>
<select :value="to" :change="setTo">
<option :each="u in Object.keys(units)"
:value="u" :text="u"></option>
</select>
</div>
=
Currency converter
<div :scope="{
amt: 100,
from: 'USD', to: 'EUR',
rates: {USD:1, EUR:0.92,
GBP:0.79, JPY:149.5,
CAD:1.36, INR:83.1},
setAmt(v) { amt = v },
setFrom(v) { from = v },
setTo(v) { to = v },
get result() {
return amt/rates[from]*rates[to]
}
}">
<input type="number" :value="amt"
:change="setAmt" />
<select :value="from" :change="setFrom">
<option :each="c in Object.keys(rates)"
:value="c" :text="c"></option>
</select> =
<b :text="result.toFixed(2)"></b>
<select :value="to" :change="setTo">
<option :each="c in Object.keys(rates)"
:value="c" :text="c"></option>
</select>
</div>
=
Stopwatch
<div :scope="{
ms: 0, on: false, id: 0,
start() {
if (on) { clearInterval(id);
on=false }
else { on=true;
id=setInterval(()=>ms+=10, 10) }
},
reset() { clearInterval(id);
on=false; ms=0 },
get t() {
let m = Math.floor(ms/60000),
s = Math.floor(ms%60000/1000),
c = Math.floor(ms%1000/10);
return [m,s,c].map(
v => String(v).padStart(2,'0'))
.join(':')
}
}">
<p :text="t"></p>
<button :onclick="start()"
:text="on ? 'Stop' : 'Start'"></button>
<button :onclick="reset()">Reset</button>
</div>
00:00:00
Pomodoro
<div :scope="{
left: 1500, on: false, id: 0,
work: 25, brk: 5, isWork: true,
start() {
if (on) { clearInterval(id);
on = false }
else { on = true;
id = setInterval(() => {
if (left-- <= 0) {
isWork = !isWork;
left = (isWork?work:brk)*60
}
}, 1000) }
},
reset() { clearInterval(id);
on=false; isWork=true;
left=work*60 },
get mm() {
return String(Math.floor(left/60))
.padStart(2,'0') },
get ss() {
return String(left%60)
.padStart(2,'0') }
}">
<p :text="mm + ':' + ss"></p>
<p :text="isWork ? 'Work' : 'Break'"></p>
<button :onclick="start()"
:text="on ? 'Pause' : 'Start'"></button>
<button :onclick="reset()">Reset</button>
</div>
25:00
Work
BMI calculator
<div :scope="{
h: 170, w: 70,
setH(v) { h = v },
setW(v) { w = v },
get bmi() {
return w / (h/100) ** 2 },
get cat() {
let b = bmi;
return b < 18.5 ? 'Underweight'
: b < 25 ? 'Normal'
: b < 30 ? 'Overweight'
: 'Obese'
}
}">
<label>Height
<input type="range" :value="h"
:change="setH"
min="100" max="220" />
<span :text="h + ' cm'"></span>
</label>
<label>Weight
<input type="range" :value="w"
:change="setW"
min="30" max="200" />
<span :text="w + ' kg'"></span>
</label>
<b :text="bmi.toFixed(1)+' — '+cat"></b>
</div>
—
Loan calculator
<div :scope="{
p: 200000, r: 5, y: 30,
setP(v) { p=v }, setR(v) { r=v },
setY(v) { y=v },
get monthly() {
let mr = r/100/12, n = y*12;
return mr
? p*mr*(1+mr)**n/((1+mr)**n-1)
: p/n
},
get total() { return monthly * y*12 }
}">
<label>Amount $<input type="number"
:value="p" :change="setP" /></label>
<label>Rate <input type="range"
:value="r" :change="setR"
min="0" max="15" step="0.1" />
<span :text="r + '%'"></span></label>
<label>Years <input type="range"
:value="y" :change="setY"
min="1" max="30" /></label>
<b>$<span :text="monthly.toFixed(2)">
</span>/mo · $<span
:text="total.toFixed(0)"></span> total
</b>
</div>
$/mo · $ total
JSON formatter
<div :scope="{
src: '{\"a\":1}', out: '',
err: '',
fmt(v) {
src = v;
try { out = JSON.stringify(
JSON.parse(v), null, 2);
err = '' }
catch(e) { err = e.message }
}
}">
<textarea :value="src"
:change="fmt" rows="3"></textarea>
<p :if="err" :text="err"></p>
<pre :if="!err" :text="out"></pre>
</div>
Phrase rotator
<b :scope="{
phrases: [
'Signal-Powered Reactive Attributes Engine',
'Structured Presentational Reactive Æsthetic',
'Simple PRogressive Ænhancement'
],
i: 0 }"
:fx="() => {
let id = setInterval(
() => i = (i+1) % phrases.length,
2000)
return () => clearInterval(id) }"
:text="phrases[i]">
</b>
Tooltip
<span :scope="{ show: false }"
:onmouseenter="show=true"
:onmouseleave="show=false"
style="position:relative">
Hover me
<span :if="show"
style="position:absolute;
bottom:100%; left:50%;
transform:translateX(-50%)">
Tooltip text
</span>
</span>
Hover me
Tooltip text
Copy to clipboard
<div :scope="{
copied: false,
copy() {
navigator.clipboard.writeText(
'npm i sprae')
copied = true
setTimeout(() => copied=false, 1500)
}
}">
<code>npm i sprae</code>
<button :onclick="copy()"
:text="copied ? '✓ Copied' : 'Copy'">
</button>
</div>
npm i sprae
Toast
<div :scope="{
msgs: [],
toast(text) {
let m = { text, id: Date.now() }
msgs.push(m)
setTimeout(
() => msgs.splice(
msgs.indexOf(m), 1), 2500)
}
}">
<button :onclick="toast('Saved!')">
Show toast</button>
<div :each="m in msgs"
:text="m.text"></div>
</div>
Star rating
<div :scope="{ rating: 0 }">
<span :each="n in 5"
:onclick="rating = n"
:text="n <= rating ? '★' : '☆'"
style="cursor:pointer;
font-size:1.5em">
</span>
<span :text="rating + '/5'"></span>
</div>
Tag input
<div :scope="{
tags: ['sprae','reactive'],
val: '',
setVal(v) { val = v },
add(e) {
if (e.key==='Enter' && val.trim()){
tags.push(val.trim())
val = '' }
},
rm(i) { tags.splice(i, 1) }
}">
<span :each="t, i in tags"
:onclick="rm(i)">
<span :text="t"></span> ×
</span>
<input :value="val" :change="setVal"
:onkeydown="add"
placeholder="Add tag..." />
</div>
×
Typewriter
<span :scope="{
text: 'Hello, sprae!',
i: 0, dir: 1 }"
:fx="() => {
let id = setInterval(() => {
i += dir
if (i >= text.length) dir = -1
if (i <= 0) dir = 1
}, 100)
return () => clearInterval(id) }"
:text="text.slice(0, i) + '▌'">
</span>
Password strength
<div :scope="{
pw: '', setPw(v) { pw = v },
get score() {
let s = 0
if (pw.length > 7) s++
if (/[A-Z]/.test(pw)) s++
if (/[0-9]/.test(pw)) s++
if (/[^A-Za-z0-9]/.test(pw)) s++
return s
},
get label() {
return ['Weak','Fair',
'Good','Strong'][score] || ''
}
}">
<input type="password" :value="pw"
:change="setPw" />
<progress :value="score" max="4">
</progress>
<span :text="label"></span>
</div>
Price toggle
<div :scope="{
yearly: false,
get price() {
return yearly ? '$96/yr' : '$12/mo'
},
get save() {
return yearly ? 'Save 33%' : ''
}
}">
<button :onclick="yearly=!yearly"
:text="yearly ? 'Yearly' : 'Monthly'">
</button>
<b :text="price"></b>
<small :text="save"></small>
</div>
Countdown
<div :scope="{
target: '2026-01-01',
now: Date.now(),
get diff() {
return Math.max(0,
new Date(target) - now) },
get d() {
return Math.floor(
diff / 86400000) },
get h() {
return Math.floor(
diff % 86400000 / 3600000) },
get m() {
return Math.floor(
diff % 3600000 / 60000) }
}" :fx="() => {
let id = setInterval(
() => now = Date.now(), 1000)
return () => clearInterval(id) }">
<b :text="d+'d '+h+'h '+m+'m'"></b>
</div>
d h m s
until 2027