Signature Library · Indian
Balance Scale
A kadai thulam: load weights until the beam sits level.
States
Default
Hover
Active
Focus
Code
balance-scale.html
<div class="sig-balance-scale" data-goods="750">
<span class="sig-balance-scale__post" aria-hidden="true"></span>
<span class="sig-balance-scale__beam">
<span class="sig-balance-scale__hang" data-side="left">
<span class="sig-balance-scale__cord" aria-hidden="true"></span>
<span class="sig-balance-scale__pan">750 g</span>
</span>
<span class="sig-balance-scale__hang" data-side="right">
<span class="sig-balance-scale__cord" aria-hidden="true"></span>
<span class="sig-balance-scale__pan">
<span class="sig-balance-scale__read" data-read aria-live="polite">-</span>
</span>
</span>
</span>
<div class="sig-balance-scale__weights" role="group" aria-label="Weights">
<button class="sig-balance-scale__weight" type="button" aria-pressed="false" data-weight="500">500 g</button>
<button class="sig-balance-scale__weight" type="button" aria-pressed="false" data-weight="250">250 g</button>
<button class="sig-balance-scale__weight" type="button" aria-pressed="false" data-weight="100">100 g</button>
<button class="sig-balance-scale__weight" type="button" aria-pressed="false" data-weight="50">50 g</button>
</div>
</div>
balance-scale.css
.sig-balance-scale {
--sig-brass: #c9a13b;
--sig-text: #e9e4d3;
--sig-w: 280px;
--sig-speed: 420ms;
}
.sig-balance-scale {
position: relative;
box-sizing: border-box;
display: grid;
justify-items: center;
width: var(--sig-w);
max-width: 100%;
height: 196px;
font-family: ui-monospace, Consolas, monospace;
font-size: 11px;
color: var(--sig-text);
}
.sig-balance-scale__post {
position: absolute;
top: 6px;
bottom: 36px;
left: calc(50% - 3px);
width: 6px;
background: var(--sig-brass);
border-radius: 3px 3px 0 0;
}
.sig-balance-scale__beam {
position: absolute;
top: 10px;
left: 14%;
right: 14%;
height: 5px;
background: var(--sig-brass);
border-radius: 3px;
transform: rotate(var(--tilt, 0deg));
transition: transform var(--sig-speed) cubic-bezier(.3,1.6,.5,1);
}
.sig-balance-scale__hang {
position: absolute;
top: 3px;
width: 70px;
text-align: center;
transform: rotate(calc(var(--tilt, 0deg) * -1));
transform-origin: 50% 0;
transition: transform var(--sig-speed) cubic-bezier(.3,1.6,.5,1);
}
.sig-balance-scale__hang[data-side=left] {
left: -35px;
}
.sig-balance-scale__hang[data-side=right] {
right: -35px;
}
.sig-balance-scale__cord {
display: block;
height: 58px;
background: linear-gradient(64deg, transparent 49.4%, var(--sig-brass) 49.4% 50.6%, transparent 50.6%), linear-gradient(-64deg, transparent 49.4%, var(--sig-brass) 49.4% 50.6%, transparent 50.6%);
}
.sig-balance-scale__pan {
display: grid;
place-items: center;
height: 32px;
padding: 0 2px;
color: #201a10;
font-weight: 700;
background: linear-gradient(180deg, color-mix(in srgb, var(--sig-brass) 70%, #fff), var(--sig-brass));
border-radius: 6px 6px 34px 34px;
}
.sig-balance-scale__read {
font-size: 10px;
white-space: nowrap;
}
.sig-balance-scale__weights {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
gap: 5px;
padding-top: 6px;
border-top: 4px solid var(--sig-brass);
}
.sig-balance-scale__weight {
padding: 4px 7px;
color: inherit;
font: inherit;
background: none;
border: 1px solid var(--sig-brass);
border-radius: 4px;
cursor: pointer;
}
.sig-balance-scale__weight:hover {
background: color-mix(in srgb, var(--sig-brass) 22%, transparent);
}
.sig-balance-scale__weight:focus-visible {
outline: 2px solid var(--sig-brass);
outline-offset: 2px;
}
.sig-balance-scale__weight[aria-pressed=true] {
color: #201a10;
background: var(--sig-brass);
}
@media (prefers-reduced-motion: reduce) {
.sig-balance-scale__beam,
.sig-balance-scale__hang {
animation: none !important;
transition: none !important;
}
}
balance-scale.js
document.querySelectorAll(".sig-balance-scale").forEach((el) => {
const goods = Number(el.dataset.goods);
const weights = Array.from(el.querySelectorAll("[data-weight]"));
const read = el.querySelector("[data-read]");
const show = () => {
const sum = weights.filter((w) => w.getAttribute("aria-pressed") === "true").reduce((n, w) => n + Number(w.dataset.weight), 0);
const gap = sum - goods;
el.style.setProperty("--tilt", Math.max(-12, Math.min(12, gap / 40)) + "deg");
read.textContent = gap === 0 ? "Balanced" : gap < 0 ? -gap + " g short" : gap + " g over";
};
const pick = (e) => {
const hit = (e.target).closest("[data-weight]");
if (!hit) return;
hit.setAttribute("aria-pressed", String(hit.getAttribute("aria-pressed") !== "true"));
show();
};
show();
el.addEventListener("click", pick);
return () => el.removeEventListener("click", pick);
});
balance-scale.tsx
import { useEffect, useRef } from "react";
import type React from "react";
import "./balance-scale.css"; // the CSS tab
function attach(el: HTMLElement) {
const goods = Number(el.dataset.goods);
const weights = Array.from(el.querySelectorAll("[data-weight]")) as HTMLElement[];
const read = el.querySelector("[data-read]") as HTMLElement;
const show = () => {
const sum = weights.filter((w) => w.getAttribute("aria-pressed") === "true").reduce((n, w) => n + Number(w.dataset.weight), 0);
const gap = sum - goods;
el.style.setProperty("--tilt", Math.max(-12, Math.min(12, gap / 40)) + "deg");
read.textContent = gap === 0 ? "Balanced" : gap < 0 ? -gap + " g short" : gap + " g over";
};
const pick = (e: MouseEvent) => {
const hit = (e.target as HTMLElement).closest("[data-weight]");
if (!hit) return;
hit.setAttribute("aria-pressed", String(hit.getAttribute("aria-pressed") !== "true"));
show();
};
show();
el.addEventListener("click", pick);
return () => el.removeEventListener("click", pick);
}
export function SigBalanceScale(props: React.ComponentProps<"div">) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => attach(ref.current!), []);
return (
<div className="sig-balance-scale" data-goods="750" ref={ref} {...props}>
<span className="sig-balance-scale__post" aria-hidden="true" />
<span className="sig-balance-scale__beam">
<span className="sig-balance-scale__hang" data-side="left">
<span className="sig-balance-scale__cord" aria-hidden="true" />
<span className="sig-balance-scale__pan">{"750 g"}</span>
</span>
<span className="sig-balance-scale__hang" data-side="right">
<span className="sig-balance-scale__cord" aria-hidden="true" />
<span className="sig-balance-scale__pan">
<span className="sig-balance-scale__read" data-read aria-live="polite">{"-"}</span>
</span>
</span>
</span>
<div className="sig-balance-scale__weights" role="group" aria-label="Weights">
<button className="sig-balance-scale__weight" type="button" aria-pressed="false" data-weight="500">{"500 g"}</button>
<button className="sig-balance-scale__weight" type="button" aria-pressed="false" data-weight="250">{"250 g"}</button>
<button className="sig-balance-scale__weight" type="button" aria-pressed="false" data-weight="100">{"100 g"}</button>
<button className="sig-balance-scale__weight" type="button" aria-pressed="false" data-weight="50">{"50 g"}</button>
</div>
</div>
);
}
balance-scale.html
<div class="sig-balance-scale relative [box-sizing:border-box] grid [justify-items:center] w-[280px] [max-width:100%] h-[196px] [font-family:ui-monospace,_Consolas,_monospace] text-[11px] text-[#e9e4d3]" data-goods="750">
<span class="absolute [top:6px] [bottom:36px] [left:calc(50%_-_3px)] w-[6px] bg-[#c9a13b] rounded-[3px_3px_0_0]" aria-hidden="true"></span>
<span class="absolute [top:10px] [left:14%] [right:14%] h-[5px] bg-[#c9a13b] rounded-[3px] [transform:rotate(var(--tilt,_0deg))] [transition:transform_420ms_cubic-bezier(.3,1.6,.5,1)] motion-reduce:[transition:none!important]">
<span class="absolute [top:3px] w-[70px] [text-align:center] [transform:rotate(calc(var(--tilt,_0deg)_*_-1))] [transform-origin:50%_0] [transition:transform_420ms_cubic-bezier(.3,1.6,.5,1)] [&[data-side=left]]:[left:-35px] [&[data-side=right]]:[right:-35px] motion-reduce:[transition:none!important]" data-side="left">
<span class="[display:block] h-[58px] [background:linear-gradient(64deg,_transparent_49.4%,_#c9a13b_49.4%_50.6%,_transparent_50.6%),_linear-gradient(-64deg,_transparent_49.4%,_#c9a13b_49.4%_50.6%,_transparent_50.6%)]" aria-hidden="true"></span>
<span class="grid [place-items:center] h-[32px] [padding:0_2px] text-[#201a10] font-[700] [background:linear-gradient(180deg,_color-mix(in_srgb,_#c9a13b_70%,_#fff),_#c9a13b)] rounded-[6px_6px_34px_34px]">750 g</span>
</span>
<span class="absolute [top:3px] w-[70px] [text-align:center] [transform:rotate(calc(var(--tilt,_0deg)_*_-1))] [transform-origin:50%_0] [transition:transform_420ms_cubic-bezier(.3,1.6,.5,1)] [&[data-side=left]]:[left:-35px] [&[data-side=right]]:[right:-35px] motion-reduce:[transition:none!important]" data-side="right">
<span class="[display:block] h-[58px] [background:linear-gradient(64deg,_transparent_49.4%,_#c9a13b_49.4%_50.6%,_transparent_50.6%),_linear-gradient(-64deg,_transparent_49.4%,_#c9a13b_49.4%_50.6%,_transparent_50.6%)]" aria-hidden="true"></span>
<span class="grid [place-items:center] h-[32px] [padding:0_2px] text-[#201a10] font-[700] [background:linear-gradient(180deg,_color-mix(in_srgb,_#c9a13b_70%,_#fff),_#c9a13b)] rounded-[6px_6px_34px_34px]">
<span class="text-[10px] whitespace-nowrap" data-read aria-live="polite">-</span>
</span>
</span>
</span>
<div class="absolute [bottom:0] [left:0] [right:0] [display:flex] justify-center gap-[5px] [padding-top:6px] [border-top:4px_solid_#c9a13b]" role="group" aria-label="Weights">
<button class="[padding:4px_7px] [color:inherit] [font:inherit] [background:none] [border:1px_solid_#c9a13b] rounded-[4px] cursor-pointer hover:[background:color-mix(in_srgb,_#c9a13b_22%,_transparent)] focus-visible:[outline:2px_solid_#c9a13b] focus-visible:[outline-offset:2px] [&[aria-pressed=true]]:text-[#201a10] [&[aria-pressed=true]]:bg-[#c9a13b]" type="button" aria-pressed="false" data-weight="500">500 g</button>
<button class="[padding:4px_7px] [color:inherit] [font:inherit] [background:none] [border:1px_solid_#c9a13b] rounded-[4px] cursor-pointer hover:[background:color-mix(in_srgb,_#c9a13b_22%,_transparent)] focus-visible:[outline:2px_solid_#c9a13b] focus-visible:[outline-offset:2px] [&[aria-pressed=true]]:text-[#201a10] [&[aria-pressed=true]]:bg-[#c9a13b]" type="button" aria-pressed="false" data-weight="250">250 g</button>
<button class="[padding:4px_7px] [color:inherit] [font:inherit] [background:none] [border:1px_solid_#c9a13b] rounded-[4px] cursor-pointer hover:[background:color-mix(in_srgb,_#c9a13b_22%,_transparent)] focus-visible:[outline:2px_solid_#c9a13b] focus-visible:[outline-offset:2px] [&[aria-pressed=true]]:text-[#201a10] [&[aria-pressed=true]]:bg-[#c9a13b]" type="button" aria-pressed="false" data-weight="100">100 g</button>
<button class="[padding:4px_7px] [color:inherit] [font:inherit] [background:none] [border:1px_solid_#c9a13b] rounded-[4px] cursor-pointer hover:[background:color-mix(in_srgb,_#c9a13b_22%,_transparent)] focus-visible:[outline:2px_solid_#c9a13b] focus-visible:[outline-offset:2px] [&[aria-pressed=true]]:text-[#201a10] [&[aria-pressed=true]]:bg-[#c9a13b]" type="button" aria-pressed="false" data-weight="50">50 g</button>
</div>
</div>
<!-- Needs the script from the JS tab. -->