# Paragraph with a reading ruler

> A band one line tall follows the pointer or arrow keys and dims the rest of the paragraph so you keep your place.

- Category: [Text & scroll](https://library.aimran.es/components/text-scroll)
- Built with: HTML + CSS
- Live editor: https://library.aimran.es/blocks/uv-tx-reading-ruler
- Library: AIMRAN Library (https://library.aimran.es), free, no sign-up, by Aimran Oukham

## Where to use it

Reading rulers are a simple aid for people with dyslexia, attention difficulties or tired eyes: they keep one line in focus and soften everything else. This block measures the line height of the paragraph and snaps a tinted band to it, with blurred veils above and below. It suits long articles, documentation, learning platforms and accessibility settings panels.

Write the heading in the text field and any paragraph in the HTML; the ruler adapts to its lines. The main color draws the edges and the secondary one tints the band. A switch turns it off, and the arrow keys, Home and End move it line by line.

## Code

### HTML

```html
<div class="uvq14-rul"><div class="hd"><h3>Read without losing your place</h3><button type="button" class="tg" aria-pressed="true">Reading ruler</button></div><div class="ar" tabindex="0" role="group" aria-label="Text with ruler: use the up and down arrows"><p>A reading ruler covers what you don't need and leaves a single line in view. It helps people who skip lines, people reading on a small screen and anyone who gets to the end of the day tired. Move the mouse, tap the text or use the arrow keys to go down line by line.</p><i class="bd" aria-hidden="true"></i><i class="ov a" aria-hidden="true"></i><i class="ov b" aria-hidden="true"></i></div></div>
```

### CSS

```css
.uvq14-rul {
  --c1: #0891b2;
  --c2: #fde047;
  --s: 16px;
  /* tamaño */
  --k: 1;
  /* velocidad */
  --fg: #1d1d1f;
  /* texto */
  --bg: #f5f5f7;
  /* fondo */
}

.uvq14-rul {
  font-family: -apple-system,BlinkMacSystemFont,"SF Pro Text","Inter",system-ui,sans-serif;
  font-size: var(--s);
  color: var(--fg);
  -webkit-tap-highlight-color: transparent;
  width: 20em;
}

.uvq14-rul .hd {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .8em;
  margin-bottom: .7em;
}

.uvq14-rul h3 {
  margin: 0;
  font-size: 1.1em;
  font-weight: 800;
  letter-spacing: -.02em;
}

.uvq14-rul .tg {
  font: inherit;
  color: inherit;
  border: 0;
  margin: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  display: inline-flex;
  align-items: center;
  gap: .45em;
  padding: .4em .75em;
  border-radius: 2em;
  font-size: .7em;
  font-weight: 700;
  color: color-mix(in srgb,var(--fg) 58%,transparent);
  background: color-mix(in srgb,var(--fg) 7%,transparent);
  transition: background calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1),color calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1),transform calc(0.2s / var(--k)) cubic-bezier(.34,1.56,.64,1);
}

.uvq14-rul .tg::before {
  content: "";
  width: 1.6em;
  height: .9em;
  border-radius: 1em;
  background: color-mix(in srgb,var(--fg) 10%,transparent);
  box-shadow: inset .45em 0 0 -.05em color-mix(in srgb,var(--fg) 58%,transparent);
  transition: box-shadow calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1),background calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1);
}

.uvq14-rul .tg[aria-pressed=true] {
  color: var(--fg);
}

.uvq14-rul .tg[aria-pressed=true]::before {
  background: var(--c1);
  box-shadow: inset -.7em 0 0 -.05em #fff;
}

.uvq14-rul .tg:active {
  transform: scale(.95);
}

.uvq14-rul .tg:focus-visible,.uvq14-rul .ar:focus-visible {
  outline: 2px solid var(--c1);
  outline-offset: 3px;
}

.uvq14-rul .ar {
  position: relative;
  isolation: isolate;
  border-radius: .6em;
  outline: 0;
  cursor: ns-resize;
  touch-action: pan-x;
}

.uvq14-rul p {
  margin: 0;
  padding: .2em .5em;
  font: 400 1em/1.75 "Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
}

.uvq14-rul .bd,.uvq14-rul .ov {
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
  transition: top calc(0.18s / var(--k)) cubic-bezier(.25,.1,.25,1),height calc(0.18s / var(--k)) cubic-bezier(.25,.1,.25,1),opacity calc(0.25s / var(--k)) cubic-bezier(.25,.1,.25,1);
}

.uvq14-rul .bd {
  z-index: -1;
  top: var(--t,0);
  height: var(--l,1.75em);
  border-radius: .3em;
  background: color-mix(in srgb,var(--c2) 38%,transparent);
  box-shadow: inset 3px 0 0 var(--c1),inset -3px 0 0 var(--c1);
}

.uvq14-rul .ov {
  background: color-mix(in srgb,var(--bg) 74%,transparent);
  -webkit-backdrop-filter: blur(1.2px);
  backdrop-filter: blur(1.2px);
}

.uvq14-rul .ov.a {
  top: 0;
  height: var(--t,0);
  border-radius: .6em .6em 0 0;
}

.uvq14-rul .ov.b {
  top: calc(var(--t,0) + var(--l,1.75em));
  bottom: 0;
  border-radius: 0 0 .6em .6em;
}

.uvq14-rul.off .bd,.uvq14-rul.off .ov {
  opacity: 0;
}

@media (prefers-reduced-motion:reduce) {
  .uvq14-rul,.uvq14-rul *,.uvq14-rul *::before,.uvq14-rul *::after {
    animation-duration: .01ms!important;
    animation-iteration-count: 1!important;
    transition-duration: .01ms!important;
  }
}
```

### JavaScript

```js
document.querySelectorAll(".uvq14-rul").forEach(el => {
  const ar=el.querySelector(".ar"),p=ar.querySelector("p"),tg=el.querySelector(".tg");let n=0;
const geo=()=>{const L=parseFloat(getComputedStyle(p).lineHeight)||24,pt=p.offsetTop+parseFloat(getComputedStyle(p).paddingTop),N=Math.max(1,Math.round((p.clientHeight-parseFloat(getComputedStyle(p).paddingTop)*2)/L));return{L,pt,N}};
const set=i=>{const g=geo();n=Math.max(0,Math.min(g.N-1,i));ar.style.setProperty("--t",(g.pt+n*g.L).toFixed(1)+"px");ar.style.setProperty("--l",g.L.toFixed(1)+"px")};
const at=e=>{const r=ar.getBoundingClientRect(),s=r.height/ar.offsetHeight||1,g=geo();set(Math.floor(((e.clientY-r.top)/s-g.pt)/g.L))};
ar.addEventListener("pointermove",at);ar.addEventListener("pointerdown",at);
ar.addEventListener("keydown",e=>{const m={ArrowDown:1,ArrowUp:-1}[e.key];if(m){e.preventDefault();set(n+m)}if(e.key==="Home"){e.preventDefault();set(0)}if(e.key==="End"){e.preventDefault();set(99)}});
tg.addEventListener("click",()=>{const on=tg.getAttribute("aria-pressed")!=="true";tg.setAttribute("aria-pressed",String(on));el.classList.toggle("off",!on)});
const ro=new ResizeObserver(()=>set(n));ro.observe(p);set(1);return()=>ro.disconnect()
});
```

### React

```tsx
// @ts-nocheck: setup() es JavaScript del DOM sin tipos, el mismo de la pestaña JS.
import { useEffect, useRef, type CSSProperties } from "react";

const css = `
.uvq14-rul {
  font-family: -apple-system,BlinkMacSystemFont,"SF Pro Text","Inter",system-ui,sans-serif;
  font-size: var(--s);
  color: var(--fg);
  -webkit-tap-highlight-color: transparent;
  width: 20em;
}

.uvq14-rul .hd {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .8em;
  margin-bottom: .7em;
}

.uvq14-rul h3 {
  margin: 0;
  font-size: 1.1em;
  font-weight: 800;
  letter-spacing: -.02em;
}

.uvq14-rul .tg {
  font: inherit;
  color: inherit;
  border: 0;
  margin: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  display: inline-flex;
  align-items: center;
  gap: .45em;
  padding: .4em .75em;
  border-radius: 2em;
  font-size: .7em;
  font-weight: 700;
  color: color-mix(in srgb,var(--fg) 58%,transparent);
  background: color-mix(in srgb,var(--fg) 7%,transparent);
  transition: background calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1),color calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1),transform calc(0.2s / var(--k)) cubic-bezier(.34,1.56,.64,1);
}

.uvq14-rul .tg::before {
  content: "";
  width: 1.6em;
  height: .9em;
  border-radius: 1em;
  background: color-mix(in srgb,var(--fg) 10%,transparent);
  box-shadow: inset .45em 0 0 -.05em color-mix(in srgb,var(--fg) 58%,transparent);
  transition: box-shadow calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1),background calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1);
}

.uvq14-rul .tg[aria-pressed=true] {
  color: var(--fg);
}

.uvq14-rul .tg[aria-pressed=true]::before {
  background: var(--c1);
  box-shadow: inset -.7em 0 0 -.05em #fff;
}

.uvq14-rul .tg:active {
  transform: scale(.95);
}

.uvq14-rul .tg:focus-visible,.uvq14-rul .ar:focus-visible {
  outline: 2px solid var(--c1);
  outline-offset: 3px;
}

.uvq14-rul .ar {
  position: relative;
  isolation: isolate;
  border-radius: .6em;
  outline: 0;
  cursor: ns-resize;
  touch-action: pan-x;
}

.uvq14-rul p {
  margin: 0;
  padding: .2em .5em;
  font: 400 1em/1.75 "Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
}

.uvq14-rul .bd,.uvq14-rul .ov {
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
  transition: top calc(0.18s / var(--k)) cubic-bezier(.25,.1,.25,1),height calc(0.18s / var(--k)) cubic-bezier(.25,.1,.25,1),opacity calc(0.25s / var(--k)) cubic-bezier(.25,.1,.25,1);
}

.uvq14-rul .bd {
  z-index: -1;
  top: var(--t,0);
  height: var(--l,1.75em);
  border-radius: .3em;
  background: color-mix(in srgb,var(--c2) 38%,transparent);
  box-shadow: inset 3px 0 0 var(--c1),inset -3px 0 0 var(--c1);
}

.uvq14-rul .ov {
  background: color-mix(in srgb,var(--bg) 74%,transparent);
  -webkit-backdrop-filter: blur(1.2px);
  backdrop-filter: blur(1.2px);
}

.uvq14-rul .ov.a {
  top: 0;
  height: var(--t,0);
  border-radius: .6em .6em 0 0;
}

.uvq14-rul .ov.b {
  top: calc(var(--t,0) + var(--l,1.75em));
  bottom: 0;
  border-radius: 0 0 .6em .6em;
}

.uvq14-rul.off .bd,.uvq14-rul.off .ov {
  opacity: 0;
}

@media (prefers-reduced-motion:reduce) {
  .uvq14-rul,.uvq14-rul *,.uvq14-rul *::before,.uvq14-rul *::after {
    animation-duration: .01ms!important;
    animation-iteration-count: 1!important;
    transition-duration: .01ms!important;
  }
}
`;

// Interacción en JavaScript del DOM: la misma que en la pestaña JS.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setup(el: any) {
  const ar=el.querySelector(".ar"),p=ar.querySelector("p"),tg=el.querySelector(".tg");let n=0;
const geo=()=>{const L=parseFloat(getComputedStyle(p).lineHeight)||24,pt=p.offsetTop+parseFloat(getComputedStyle(p).paddingTop),N=Math.max(1,Math.round((p.clientHeight-parseFloat(getComputedStyle(p).paddingTop)*2)/L));return{L,pt,N}};
const set=i=>{const g=geo();n=Math.max(0,Math.min(g.N-1,i));ar.style.setProperty("--t",(g.pt+n*g.L).toFixed(1)+"px");ar.style.setProperty("--l",g.L.toFixed(1)+"px")};
const at=e=>{const r=ar.getBoundingClientRect(),s=r.height/ar.offsetHeight||1,g=geo();set(Math.floor(((e.clientY-r.top)/s-g.pt)/g.L))};
ar.addEventListener("pointermove",at);ar.addEventListener("pointerdown",at);
ar.addEventListener("keydown",e=>{const m={ArrowDown:1,ArrowUp:-1}[e.key];if(m){e.preventDefault();set(n+m)}if(e.key==="Home"){e.preventDefault();set(0)}if(e.key==="End"){e.preventDefault();set(99)}});
tg.addEventListener("click",()=>{const on=tg.getAttribute("aria-pressed")!=="true";tg.setAttribute("aria-pressed",String(on));el.classList.toggle("off",!on)});
const ro=new ResizeObserver(()=>set(n));ro.observe(p);set(1);return()=>ro.disconnect()
}

export default function TxReadingRuler() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const el = ref.current?.firstElementChild;
    return el ? setup(el) : undefined;
  }, []);

  return (
    <>
      <style>{css}</style>
      <div
        ref={ref} style={{
          "--c1": "#0891b2",
          "--c2": "#fde047",
          "--s": "16px",
          "--k": "1",
          "--fg": "#1d1d1f",
          "--bg": "#f5f5f7",
        } as CSSProperties}
      >
        <div className="uvq14-rul"><div className="hd"><h3>Read without losing your place</h3><button type="button" className="tg" aria-pressed="true">Reading ruler</button></div><div className="ar" tabIndex={0} role="group" aria-label="Text with ruler: use the up and down arrows"><p>A reading ruler covers what you don't need and leaves a single line in view. It helps people who skip lines, people reading on a small screen and anyone who gets to the end of the day tired. Move the mouse, tap the text or use the arrow keys to go down line by line.</p><i className="bd" aria-hidden="true"></i><i className="ov a" aria-hidden="true"></i><i className="ov b" aria-hidden="true"></i></div></div>
      </div>
    </>
  );
}
```

## Prompt for AI assistants

Paste this into ChatGPT, Claude, Cursor, v0 or Lovable to adapt the block to your stack (Tailwind, Vue, Svelte…):

````text
I want to add the “Paragraph with a reading ruler” block (Text & scroll) from AIMRAN Library to my project.

## What it is
A band one line tall follows the pointer or arrow keys and dims the rest of the paragraph so you keep your place.

## Chosen settings
- Main color: #0891b2
- Secondary color: #fde047
- Size: 16px (from 12 to 24px)
- Speed: 1x (from 0.5 to 2x)
- Text: "Leer sin perderse"

## What I need
1. Add it to my project with the same look and animation as the reference code: same sizes, colours, radii, shadows, timings and easing.
2. It is built with plain HTML and CSS (the reference wraps it in a React component with its CSS). If my project uses something else (Vue, Svelte, Astro, plain HTML…), translate it without losing any behaviour. If I use Tailwind, turn the styles into classes; if not, keep the CSS in its own file.
3. Expose the settings above as props (or CSS variables) with those values as defaults.
4. Make it work well on phones: nothing that depends only on hover, touch targets of at least 44 px and no overflow on 320 px screens.
5. Make it look right in light and dark mode.
6. Keep it accessible: semantic HTML, roles and aria-* where needed, visible focus and keyboard support.
7. Respect prefers-reduced-motion: with reduced motion, no big animations but the same end result.

## Reference code
```tsx
// @ts-nocheck: setup() es JavaScript del DOM sin tipos, el mismo de la pestaña JS.
import { useEffect, useRef, type CSSProperties } from "react";

const css = `
.uvq14-rul {
  font-family: -apple-system,BlinkMacSystemFont,"SF Pro Text","Inter",system-ui,sans-serif;
  font-size: var(--s);
  color: var(--fg);
  -webkit-tap-highlight-color: transparent;
  width: 20em;
}

.uvq14-rul .hd {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .8em;
  margin-bottom: .7em;
}

.uvq14-rul h3 {
  margin: 0;
  font-size: 1.1em;
  font-weight: 800;
  letter-spacing: -.02em;
}

.uvq14-rul .tg {
  font: inherit;
  color: inherit;
  border: 0;
  margin: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  display: inline-flex;
  align-items: center;
  gap: .45em;
  padding: .4em .75em;
  border-radius: 2em;
  font-size: .7em;
  font-weight: 700;
  color: color-mix(in srgb,var(--fg) 58%,transparent);
  background: color-mix(in srgb,var(--fg) 7%,transparent);
  transition: background calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1),color calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1),transform calc(0.2s / var(--k)) cubic-bezier(.34,1.56,.64,1);
}

.uvq14-rul .tg::before {
  content: "";
  width: 1.6em;
  height: .9em;
  border-radius: 1em;
  background: color-mix(in srgb,var(--fg) 10%,transparent);
  box-shadow: inset .45em 0 0 -.05em color-mix(in srgb,var(--fg) 58%,transparent);
  transition: box-shadow calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1),background calc(0.2s / var(--k)) cubic-bezier(.25,.1,.25,1);
}

.uvq14-rul .tg[aria-pressed=true] {
  color: var(--fg);
}

.uvq14-rul .tg[aria-pressed=true]::before {
  background: var(--c1);
  box-shadow: inset -.7em 0 0 -.05em #fff;
}

.uvq14-rul .tg:active {
  transform: scale(.95);
}

.uvq14-rul .tg:focus-visible,.uvq14-rul .ar:focus-visible {
  outline: 2px solid var(--c1);
  outline-offset: 3px;
}

.uvq14-rul .ar {
  position: relative;
  isolation: isolate;
  border-radius: .6em;
  outline: 0;
  cursor: ns-resize;
  touch-action: pan-x;
}

.uvq14-rul p {
  margin: 0;
  padding: .2em .5em;
  font: 400 1em/1.75 "Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
}

.uvq14-rul .bd,.uvq14-rul .ov {
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
  transition: top calc(0.18s / var(--k)) cubic-bezier(.25,.1,.25,1),height calc(0.18s / var(--k)) cubic-bezier(.25,.1,.25,1),opacity calc(0.25s / var(--k)) cubic-bezier(.25,.1,.25,1);
}

.uvq14-rul .bd {
  z-index: -1;
  top: var(--t,0);
  height: var(--l,1.75em);
  border-radius: .3em;
  background: color-mix(in srgb,var(--c2) 38%,transparent);
  box-shadow: inset 3px 0 0 var(--c1),inset -3px 0 0 var(--c1);
}

.uvq14-rul .ov {
  background: color-mix(in srgb,var(--bg) 74%,transparent);
  -webkit-backdrop-filter: blur(1.2px);
  backdrop-filter: blur(1.2px);
}

.uvq14-rul .ov.a {
  top: 0;
  height: var(--t,0);
  border-radius: .6em .6em 0 0;
}

.uvq14-rul .ov.b {
  top: calc(var(--t,0) + var(--l,1.75em));
  bottom: 0;
  border-radius: 0 0 .6em .6em;
}

.uvq14-rul.off .bd,.uvq14-rul.off .ov {
  opacity: 0;
}

@media (prefers-reduced-motion:reduce) {
  .uvq14-rul,.uvq14-rul *,.uvq14-rul *::before,.uvq14-rul *::after {
    animation-duration: .01ms!important;
    animation-iteration-count: 1!important;
    transition-duration: .01ms!important;
  }
}
`;

// Interacción en JavaScript del DOM: la misma que en la pestaña JS.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setup(el: any) {
  const ar=el.querySelector(".ar"),p=ar.querySelector("p"),tg=el.querySelector(".tg");let n=0;
const geo=()=>{const L=parseFloat(getComputedStyle(p).lineHeight)||24,pt=p.offsetTop+parseFloat(getComputedStyle(p).paddingTop),N=Math.max(1,Math.round((p.clientHeight-parseFloat(getComputedStyle(p).paddingTop)*2)/L));return{L,pt,N}};
const set=i=>{const g=geo();n=Math.max(0,Math.min(g.N-1,i));ar.style.setProperty("--t",(g.pt+n*g.L).toFixed(1)+"px");ar.style.setProperty("--l",g.L.toFixed(1)+"px")};
const at=e=>{const r=ar.getBoundingClientRect(),s=r.height/ar.offsetHeight||1,g=geo();set(Math.floor(((e.clientY-r.top)/s-g.pt)/g.L))};
ar.addEventListener("pointermove",at);ar.addEventListener("pointerdown",at);
ar.addEventListener("keydown",e=>{const m={ArrowDown:1,ArrowUp:-1}[e.key];if(m){e.preventDefault();set(n+m)}if(e.key==="Home"){e.preventDefault();set(0)}if(e.key==="End"){e.preventDefault();set(99)}});
tg.addEventListener("click",()=>{const on=tg.getAttribute("aria-pressed")!=="true";tg.setAttribute("aria-pressed",String(on));el.classList.toggle("off",!on)});
const ro=new ResizeObserver(()=>set(n));ro.observe(p);set(1);return()=>ro.disconnect()
}

export default function TxReadingRuler() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const el = ref.current?.firstElementChild;
    return el ? setup(el) : undefined;
  }, []);

  return (
    <>
      <style>{css}</style>
      <div
        ref={ref} style={{
          "--c1": "#0891b2",
          "--c2": "#fde047",
          "--s": "16px",
          "--k": "1",
          "--fg": "#1d1d1f",
          "--bg": "#f5f5f7",
        } as CSSProperties}
      >
        <div className="uvq14-rul"><div className="hd"><h3>Read without losing your place</h3><button type="button" className="tg" aria-pressed="true">Reading ruler</button></div><div className="ar" tabIndex={0} role="group" aria-label="Text with ruler: use the up and down arrows"><p>A reading ruler covers what you don't need and leaves a single line in view. It helps people who skip lines, people reading on a small screen and anyone who gets to the end of the day tired. Move the mouse, tap the text or use the arrow keys to go down line by line.</p><i className="bd" aria-hidden="true"></i><i className="ov a" aria-hidden="true"></i><i className="ov b" aria-hidden="true"></i></div></div>
      </div>
    </>
  );
}
```

Give me the complete final code, ready to paste, and tell me in one line where to put it in my project.
````
