Codync
A free, open-source 1:1 alternative to Grok Bot and Muse. Message Claude Code, Codex, Cursor and 40+ coding agents as persistent bots from your iPhone, Mac or Linux desktop. Group chats, threads, appr
Composable SVG gauge primitives for React — build speedometers, dials, meters and clocks from arcs, ticks, zones and needles. Distributed as a shadcn registry, so the source lands in your project and
Developer toolsTypeScript · JavaScriptGitHub @thordurskOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Composable SVG gauges for React, distributed as a shadcn registry.
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗Composable SVG gauge primitives for React, distributed as a shadcn registry. Design a gauge in the studio, copy the code, and own the source in your project. No runtime dependencies beyond React and the shadcn cn helper.
npx shadcn@latest add https://gauge-ui.dev/r/gauge.json
Or register the namespace once in components.json and install by name:
{
"registries": {
"@gauge-ui": "https://gauge-ui.dev/r/{name}.json"
}
}
npx shadcn@latest add @gauge-ui/gauge
The files land in components/gauge/.
import {
Gauge,
GaugeArc,
GaugeHub,
GaugeNeedle,
GaugeTickLabels,
GaugeTicks,
GaugeTrack,
GaugeValue,
GaugeZones,
zoneColor,
} from "@/components/gauge"
const zones = [
{ to: 60, color: "#22c55e" },
{ to: 80, color: "#f59e0b" },
{ to: 100, color: "#ef4444" },
]
export function Speed({ value }: { value: number }) {
return (
<Gauge value={value} min={0} max={100} startAngle={40} endAngle={320}>
<GaugeTrack width={14} />
<GaugeZones zones={zones} width={14} />
<GaugeArc width={14} color={zoneColor(zones, value, "currentColor")} />
<GaugeTicks count={10} />
<GaugeTickLabels count={5} />
<GaugeNeedle style="pointer" />
<GaugeHub />
<GaugeValue decimals={0} />
</Gauge>
)
}
Gauge owns the domain and geometry. Every child reads value, angles and radius from context and positions itself against the reference radius, so parts can be added, removed or reordered freely. Angles are measured clockwise from six o'clock.
| Primitive | Draws |
|---|---|
GaugeTrack | Background arc across the whole sweep |
GaugeArc | Filled arc anchored at from, growing either way to the current value or to; reverse grows it back from the end, wrap carries it past the seam of a closed ring |
GaugeStack | The value arc split into parts that sweep as one, by weight shares or fixed to cutoffs |
GaugeZones | Coloured bands between zone cutoffs |
GaugeTicks | Evenly spaced tick marks |
GaugeTickFill | Ticks as the fill: those between the ends of the value or range are lit, the rest dim, or with mode="nearest" only the tick at each end. Lit and dim colours can be functions of the value |
GaugeMarks | Marks at arbitrary values, or at the count equal cuts of the domain |
GaugeTickLabels | Numeric, compass or clock labels along the arc |
GaugeNeedle | Line, pointer, compass or arrow needle |
GaugeHub | Centre cap for the needle |
GaugeDot | A disc riding the arc at the current value, with an optional halo to stand it off a band |
GaugeTooltip | An HTML tooltip shown on hover, pinned to the value, the pointer or a fixed spot on the dial: the value with a label and unit by default, or any content you give it |
GaugeValue | Formatted current value |
GaugeText | Free text at any position, for units and titles |
GaugeInset | A whole gauge nested inside another, at its own scale |
GaugeControl | Wraps a gauge as an accessible slider: drag round the ring, turn the face with knob, or use the keyboard to set its value |
Gauge and GaugeInset also take wrap, which treats the domain as circular so a heading animates the shortest way round, and rotate, which turns the whole scale under a fixed pointer the way a compass card turns.
LinearGauge lays the scale out straight instead of round, as a horizontal or vertical bar. It publishes the same context as Gauge, so the same parts draw along it. The only exception is GaugeNeedle, which needs a centre to pivot on.
<LinearGauge value={temp} min={-20} max={40} orientation="vertical" padding={{ left: 80, right: 32, top: 24, bottom: 48 }} className="h-80">
<GaugeTrack width={22} />
<GaugeArc width={22} color={zoneColor(zones, temp, "currentColor")} />
<GaugeDot end="from" radius={28} />
<GaugeTicks count={12} offset={22} length={8} />
<GaugeTickLabels count={6} offset={34} anchor="end" format={(v) => `${v}°`} />
</LinearGauge>
length SVG units long (default 400) and centred on the origin. A horizontal bar runs left to right. A vertical bar runs bottom to top.offset moves a part outward, the same as on a dial: up from a horizontal bar, or left of a vertical one. Use a negative offset for labels below or to the right.padding takes one number, or { top, right, bottom, left } for a different amount on each side. The SVG gets a matching aspect-ratio, so a horizontal bar fills its width and a vertical bar fills its height.GaugeControl takes orientation (and length, if it isn't 400) to work as a slider along the bar. LinearGaugeInset puts a bar inside another gauge.wrap domain, a range that runs past the end of the bar continues from its start.Pass a [from, to] pair as value to show a span of the domain instead of a single reading: a forecast's low and high, a target band, a sleep window. Both ends animate with the gauge's transition.
<GaugeControl value={range} onChange={setRange} label="Level">
<Gauge value={range} transition>
<GaugeTrack />
<GaugeArc />
<GaugeDot end="from" />
<GaugeDot />
<GaugeValue />
<GaugeTooltip unit="%" />
</Gauge>
</GaugeControl>
GaugeArc and GaugeStack fill between the two ends rather than from min.GaugeDot and GaugeNeedle follow the far end. Use end="from" to put one on the start.GaugeValue and GaugeTooltip show both ends, joined by separator (default –), with format applied to each. The tooltip points at the middle of the range. GaugeValue takes end to show just one end.GaugeControl gets two thumbs. A press on the ring moves the nearer one, and each thumb can be tabbed to and moved with the keys on its own. Name them with thumbLabels, and pass one valueText per end. With knob, turning the face moves the whole range and keeps its width.GaugeTicks, GaugeMarks and GaugeTickLabels take a function as color, called with each mark's value, so marks inside a range can stand out: color={(v) => (v >= 50 && v <= 70 ? "green" : "gray")}.
GaugeTickFill builds a gauge from ticks alone. It ticks the whole sweep and lights the ticks the fill covers, following the value or range the way GaugeArc does:
<GaugeTickFill count={40} length={36} width={8} color="dodgerblue" />
<GaugeTickFill count={64} litLength={30} color={(v) => (v > 120 ? "red" : "white")} />
<GaugeTickFill count={72} mode="nearest" length={0} litLength={40} dimColor="transparent" />
On an open domain, the ends are sorted and the control's thumbs stop when they meet. On a wrap domain the order matters, because the range runs forward from from: [22, 6] on a 24-hour ring is the night and [6, 22] is the day. Pass wrap to GaugeControl too, so its thumbs can cross the seam.
Helpers zoneColor, zoneCutoffs, compassLabel, clockLabel, clockTime, durationLabel and fadeColor are exported from the same module and are what the studio's generated code uses.
GaugeTooltip is an HTML tooltip that shows while the pointer is over the gauge. By default it reads the value, with an optional label above it and unit after it, styled like the shadcn tooltip.
<GaugeTooltip label="Used" unit="GB" decimals={1} />
position decides what it is pinned to:
position | Pinned to |
|---|---|
value | The arc at the current value, or at at when given. The default |
pointer | The cursor, following it round the gauge |
center | The middle of the dial |
top, right, bottom, left | The middle of that edge of the box round the sweep |
top-left, top-right, bottom-left, bottom-right | That corner of the same box |
side sets which way the bubble stands off that point: top, right, bottom, left or center. The default, auto, points outward along the radius at the value, so the bubble swings round as the value moves. It sits above the pointer, centres on the centre, and points away from the dial at an edge or corner. offset moves the point in or out from the reference radius in SVG units, as it does for arcs and dots, and gap is the room between the point and the bubble in CSS pixels. It also stays up while a press that started on the gauge is held, wherever the pointer goes, so it follows a drag on a GaugeControl and shows on touch screens, and while the gauge or the nearest focusable element round it, such as a GaugeControl, has keyboard focus. Pass open to hold it open, or shut, instead of following hover, presses and focus.
The bubble avoids the edges of the viewport without leaving its point. When it would cross an edge, it flips to the other side of the point if that side has more room. If it still crosses the left or right edge, it slides sideways, but only as far as it keeps overlapping the point. It never slides up or down, so a tooltip whose gauge is below the fold stays with it and scrolls into view alongside it. collisionPadding is the room kept clear of the edge, 8 pixels by default, and avoidCollisions={false} turns this off.
Children replace the default content with any HTML. Pass a function to get the value being shown, animated frame by frame, and className to restyle the bubble:
<GaugeTooltip position="pointer" className="bg-popover text-popover-foreground">
{(v) => (
<div className="flex items-center gap-2">
<span className="size-2 rounded-full" style={{ background: zoneColor(zones, v, "gray") }} />
{v < 60 ? "Normal" : "High"} · {v.toFixed(0)}°
</div>
)}
</GaugeTooltip>
The bubble is portalled to document.body and placed through the SVG's screen transform. The gauge never clips it, and it keeps its CSS size however the gauge, or an inset it sits in, is scaled. Hover is taken over the whole <svg>, so a tooltip inside a GaugeInset shows when the pointer is anywhere over the host gauge. The entrance animation uses the tw-animate-css classes that shadcn projects already include.
GaugeInset is a second gauge drawn inside the first: a fuel dial under a speedometer, a ring inside a ring. It takes its own value and domain and publishes the same context Gauge does, so every primitive works inside it unchanged.
<Gauge value={speed} min={0} max={240} startAngle={40} endAngle={320}>
<GaugeTicks count={12} />
<GaugeNeedle style="pointer" />
<GaugeHub />
<GaugeInset
value={fuel}
y={144}
scale={0.28}
min={0}
max={100}
startAngle={90}
endAngle={270}
>
<GaugeTrack width={26} />
<GaugeArc width={26} />
</GaugeInset>
</Gauge>
It draws in the host gauge's coordinate system rather than a viewport of its own, so it takes no padding or fit. x and y place its centre in the host's SVG units, and scale sizes the whole thing at once: strokes, ticks and text shrink with the dial, so parts meant to be read at a quarter size are worth drawing heavier than they would be at full size. Insets can be stacked for concentric rings, and each one animates its own value.
The Speed & fuel, Nested rings and System templates in the studio are built this way. The studio edits every gauge in a composition: the chips above the value slider pick which one the panels, the slider and the play modes are pointed at, and the same row adds and removes them. An inset's Gauge panel trades padding, fit and the stage colour for where it sits and how big it is drawn. The generated code takes a prop per gauge.
Value changes are instant by default. Pass transition to Gauge and every part that reads the value, including the arc, needle and value text, animates together. No animation library is involved; the gauge steps its own spring or tween per frame and snaps when the viewer prefers reduced motion.
// A gentle spring with the default settings
<Gauge value={value} transition>
// Tune the spring by feel
<Gauge value={value} transition={{ type: "spring", visualDuration: 0.4, bounce: 0.25 }}>
// Or by physics, which takes precedence when given
<Gauge value={value} transition={{ type: "spring", stiffness: 120, damping: 14, mass: 1 }}>
// A timed tween with a named curve, cubic-bezier points, or your own function
<Gauge value={value} transition={{ type: "tween", duration: 0.6, ease: "easeOut" }}>
<Gauge value={value} transition={{ type: "tween", duration: 0.6, ease: [0.22, 1, 0.36, 1] }}>
// Sweep in from the minimum on mount
<Gauge value={value} initialValue={0} transition>
| Option | Applies to | Meaning |
|---|---|---|
visualDuration | spring | Seconds the spring visibly takes to arrive. Default 0.5 |
bounce | spring | 0 settles without overshoot, 1 rings freely. Default 0.1 |
stiffness, damping, mass | spring | Raw physics. Defaults 100, 10 and 1 when any is set |
duration | tween | Seconds. Default 0.5 |
ease | tween | linear, easeIn, easeOut, easeInOut, [x1, y1, x2, y2] or (t) => number. Default easeInOut |
Inside custom primitives, useGauge() exposes both value, the animated figure being drawn this frame, and target, the value the gauge is heading for. On a range, from and targetFrom are the same for its start, and range is true. The studio's Value panel has the same controls, and the code it generates carries your transition.
This repository is also the site: a landing page at / and the studio at /studio. Run it locally with:
npm install
npm run dev
registry.json at the repository root defines the single gauge item. npm run registry:build writes the installable JSON to public/r/, and the production build runs it automatically so the deployed site always serves the current source.