---
title: SC Ruler
description: A canvas ruler with units, zoom, pointer read-out and drag measurement.
url: https://secretocho.design/lab/sc-ruler/
type: SoftwareSourceCode
status: working
published: 2026-10-05
---

# SC Ruler

> A canvas ruler with units, zoom, pointer read-out and drag measurement.

component 19 / 28 · WORKING

- **type**: Code Component
- **platform**: Framer
- **license**: SecretoCho Design

![A still of the SC Ruler demo, as its module draws it with reduced motion.](https://secretocho.design/_astro/still.XVY8Xc_4_KQu6e.webp)

*fig 00 · live demo, ported from **SC\_Ruler.tsx***

High-DPI canvas ruler with px/in/cm/mm units, zoom, offset, pointer read-out and a drag-to-measure mode.

SCRuler draws a measurement ruler on a high-DPI canvas: major and minor ticks, labels in px, inches, cm or mm, zoom and offset, normal or reversed direction, horizontal or vertical. Move the pointer over it and a guide line with the value follows; turn on Measurement Mode and drag to read a distance.

Made as a HUD element for the old site’s pages, it fills whatever frame it sits in and redraws on resize.

- Utilities
- Debug
- Visual

## Main

### Overview

- **Built With**: React (hooks), Canvas 2D, ResizeObserver, Framer Code Component API
- **Layout Support**: Fills the frame (min 200 × 40 px); redraws on resize
- **Orientation**: Horizontal or vertical; normal or reversed direction
- **Ideal Use Case**: Blueprint-style page chrome, design-tool mockups, measuring overlays in prototypes

### Tags

- ruler
- canvas
- measurement
- ticks
- hud
- units
- dpi
- blueprint

### Code comment

*SC\_Ruler.tsx · lines 1–1 · the file’s header comment*

```tsx
// SC_Ruler.tsx — High-DPI Canvas Ruler for Framer with Measurement Mode
```

### Dependencies

- **Technologies**: None (Canvas 2D + ResizeObserver)
- **Notes**: Resolves var(--…) colours through getComputedStyle on documentElement, then body and a Framer component element; a fallback in the var() is returned at the first step.

### Complexity

- **Logic Level**: Medium (unit maths, tick loop, two interaction modes, token colour resolution)
- **Runtime Cost**: Low (one requestAnimationFrame per change; no continuous loop; tick loop capped at 2,000 steps)
- **Component Size**: ~28 KB TSX (28,218 bytes, 947 lines)
- **Notes**: Draws at max(devicePixelRatio, 2) for crisp one-pixel ticks.

## Guide

### Instructions

1. Drop **SCRuler** into a frame at least 200 × 40 px.
2. **Basic**: choose the axis, the direction (Normal → or Reverse ←) and the unit.
3. **Scale**: zoom, offset, pixels per unit (px mode) and screen DPI (in / cm / mm modes).
4. **Ticks**: major interval, subdivisions, and both tick lengths as a percentage of the ruler’s thickness.
5. Turn **Show Labels** on, set Label Every N, padding and the label font; set the four colours.
6. **Pointer** draws a guide and value on hover; **Measurement Mode** lets the visitor drag a distance.

### Examples / Presets

- Default
  
  The source defaults: horizontal, px, a major tick every 100 with 9 subdivisions, ticks 30 % / 15 %, labels on, a dashed blue pointer, measurement off.
  
- Centimetres
  
  - **basic.unit**: cm
  - **ticks.majorInterval**: 1
  - **ticks.minorDivisions**: 9 (millimetre subdivisions at 96 dpi)
  
- Inches
  
  - **basic.unit**: in
  - **ticks.majorInterval**: 1
  - **ticks.minorDivisions**: 7
  
- Measure (site)
  
  - **basic.axis**: vertical
  - **ticks.majorLength**: 10
  - **ticks.minorLength**: 5
  - **pointer.color**: rgb(0, 250, 146)
  - **measurement.enabled**: true
  - **measurement.color**: #FF3366 (the old site’s contact page)
  
- Sidebar (site)
  
  - **basic.axis**: vertical
  - **ticks.majorLength**: 10
  - **ticks.minorLength**: 5
  - **showLabels**: false
  - **pointer.enabled**: false
  - **measurement.enabled**: false (a 96 px strip on the old site’s services and about pages, ticks in the B-W 20 / 10 / 24 % tokens)
  

## Mechanics

### How it Works

- **Scale**: pixelsPerUnit = (px ? Pixels/Unit : dpi per unit) × zoom; unitToPx maps in → dpi, cm → dpi / 2.54, mm → dpi / 25.4.
- **Canvas**: a ResizeObserver tracks the container (min 200 × 40); the canvas is sized at max(devicePixelRatio, 2) and the context scaled back, so ticks land on device pixels.
- **Ticks**: step = major / (subdivisions + 1); the loop starts two steps before the offset and stops at 2,000 steps; every tick is drawn at Math.round(pos) + 0.5 for a crisp 1 px line; majors every (subdivisions + 1) steps get a label when their index divides by Label Every N.
- **Labels**: px are rounded; mm show 0 or 1 decimal; in / cm show 2 decimals under 10, else 1.
- **Pointer**: on hover a guide line (solid or dashed, 70 % alpha) crosses the ruler and a filled tooltip shows the value at the pointer.
- **Measurement**: mousedown starts a line, the pointer extends it, mouseup or leaving ends it; the label shows the distance along the ruler’s axis in the current unit.
- **Colours**: resolveFramerColor turns Framer colour objects (r g b a / h s l a) and var(--token) strings into CSS colours by reading computed styles.

### Performance & Tuning

- The ruler redraws once per state change (one requestAnimationFrame), not continuously; pointer moves trigger a redraw each.
- Very small steps (tiny major interval × many subdivisions × high zoom) hit the 2,000-step cap; raise the interval or lower the subdivisions.
- At 2× or more DPR a full-width ruler is a large canvas; keep the frame as thin as the design allows.

### Compatibility & Limits

- Evergreen browsers with Canvas 2D and ResizeObserver; requests a desynchronized 2D context where supported.
- Mouse events only: there are no touch handlers, so the pointer read-out and measuring do not work on touch screens.
- Inches, cm and mm depend on the Screen DPI value, not the real screen.
- Reverse ← draws ticks only in the first two steps (about 20 px at the defaults), and with an offset its pointer value disagrees with the labels.
- On a vertical ruler the pointer label is drawn left-aligned from its box’s centre, so it spills out of the box.
- A vertical ruler is still at least 200 px wide; tick lengths are capped at 50 px (major) and 30 px (minor).

## Quality

### Accessibility

- The canvas has no text alternative or role: treat it as decorative (aria-hidden on the layer) unless the measurement matters.
- Labels are drawn pixels; they do not scale with browser text size.

### Security & Privacy

- No network calls, no storage, no permissions, no third-party libraries.
- Reads getComputedStyle on documentElement first to resolve var(--token) colours (when the var() has a fallback it returns the fallback at once and never reaches body or the component element); writes nothing to the DOM outside its own canvas.
- Mouse coordinates are read from React events only.

## Reference

### Props

| name | type | default | description |
| --- | --- | --- | --- |
| basic | object | axis=horizontal, direction=clockwise, unit=px | Basic. Axis horizontal \| vertical; Direction Normal → (clockwise) \| Reverse ← (counter-clockwise); Unit Pixels \| Inches \| CM \| MM. |
| scale | object | zoom=1, offset=0, pixelsPerUnit=1, dpi=96 | Scale. Zoom 0.1–10; Offset −10000–10000; Pixels/Unit 0.1–100 (px mode); Screen DPI 72–300 (physical units). |
| ticks | object | majorInterval=100, minorDivisions=9, majorLength=30, minorLength=15 | Ticks. Major Every 1–1000 units; Subdivisions 0–20; Major / Minor Length 10–100 % and 5–100 % of the ruler’s thickness. |
| showLabels | boolean | true | Show Labels. |
| labelFrequency | number | 1 | Label Every N. Label every Nth major tick, 1–10 (shown when labels are on). |
| labelPadding | number | 4 | Label Padding. 0–20 px between tick and label. |
| font | FontStyle | -apple-system … system-ui, sans-serif · 11 · 400 · normal | Font. Label typeface (shown when labels are on). |
| colors | object | background=transparent, majorTick=#000000, minorTick=rgba(0, 0, 0, 0.3), text=#000000 | Colors. Background, Major Ticks, Minor Ticks, Text; Framer colour tokens are resolved. |
| pointer | object | enabled=true, color=#0066FF, lineWidth=1, lineStyle=dashed | Pointer. Hover guide line and value tooltip; width 0.5–5; Solid \| Dashed. |
| measurement | object | enabled=false, color=#FF3366, showDelta=true | Measurement Mode. Enable Measure (drag to measure), Measure Color, Show Distance label. |

The Basic group’s axis, direction and unit controls have no defaultValue in the control tree; their defaults come from SCRuler.defaultProps.

### Notes & Tips

- For a drawing-style ruler set unit to cm, Major Every 1 and Subdivisions 9: the minors are millimetres.
- Offset shifts the zero point in pixels.
- Colours are resolved once per change of the colours prop, and a var(--token) with a fallback usually comes out as the fallback, so they do not follow a theme switch.

### Troubleshooting / FAQ

The canvas says “Configure ruler settings”

The computed step is zero or not finite: check Major Every, Subdivisions, Zoom and Pixels/Unit.

Labels overlap

Raise Label Every N or the major interval, or lower the zoom.

Nothing happens on a phone

The pointer and measurement handlers are mouse events only.

Token colours come out black

With a fallback in the var() you get the fallback colour; black only when there is no fallback and nothing resolves (the canvas rejects the raw var() string). Use a literal colour.

## Source

### Header comment

*SC\_Ruler.tsx · lines 1–1 · header comment*

```tsx
// SC_Ruler.tsx — High-DPI Canvas Ruler for Framer with Measurement Mode
```

[props \[REF\]](#reference)

### Key excerpt

The tick loop: each tick lands on a half-pixel for a crisp one-device-pixel line, majors fall every (subdivisions + 1) steps, and anything outside the visible axis is skipped.

*SC\_Ruler.tsx · lines 370–402 · tsx*

```tsx
        // Draw ticks
        ctx.lineWidth = 1
        const numSteps = Math.min(Math.ceil(axisLength / stepPx) + 10, 2000)
        const majorsEvery =
            ticks.minorDivisions > 0 ? ticks.minorDivisions + 1 : 1

        for (let i = 0; i < numSteps; i++) {
            const tickIdx = startIdx + i
            const logicalPx = tickIdx * stepPx
            const viewPos = dir * logicalPx - scale.offset

            if (viewPos < -stepPx * 2 || viewPos > axisLength + stepPx * 2)
                continue

            const isMajor =
                ticks.minorDivisions === 0 || tickIdx % majorsEvery === 0
            const tickLen = isMajor ? majorTickLength : minorTickLength
            const pos = Math.round(viewPos) + 0.5

            // Draw tick
            ctx.strokeStyle = isMajor
                ? resolvedColors.majorTick
                : resolvedColors.minorTick
            ctx.beginPath()

            if (isHorizontal) {
                ctx.moveTo(pos, 0)
                ctx.lineTo(pos, tickLen)
            } else {
                ctx.moveTo(0, pos)
                ctx.lineTo(tickLen, pos)
            }
            ctx.stroke()
```

## Credits

### Credits

© SecretoCho Design

- **license**: SecretoCho Design

SecretoCho Design

### Disclaimer

Provided “as is” for visual/UX purposes. Validate performance, accessibility, and licensing within your project.

## Proves

- [Creative coding](https://secretocho.design/about/#capabilities)

## Hiring a design engineer?

I’m open to senior design engineer roles: remote from Belgrade, or relocation with visa sponsorship. Write to me, or read the CV first. Client work stays selective, one project at a time.

[sc@secretocho.design](mailto:sc@secretocho.design)

[read the CV](https://secretocho.design/cv/) · [LinkedIn](https://www.linkedin.com/in/secretocho/)
