← Widget gallery

Clock

A wall clock that renders a complete timestamp on the server and starts ticking once the browser hydrates it.

hydration · timer · shadow dom · no params

Live example

Ticking now

In a .page.html file or a renderHTML() string:

<widget-clock></widget-clock>

In a .page.md file or a renderMarkdown() string:

```widget:clock
```

How it works

getData() captures new Date().toISOString() — on the server under /html/ and /md/, in the browser under /app/. renderHTML() emits a <time datetime> element carrying that instant, so the markup is already correct before any JavaScript runs.

hydrate() then starts a one-second interval that rewrites the element's text and datetime attribute, and destroy() clears the interval when the widget leaves the DOM. Without JavaScript the clock still renders — it just shows the moment the page was built.

hydrate() must be idempotent

hydrate() is not called once per element. It runs after every render that reaches the ready state, so a host calling the element's public reload() gets a second call — while destroy() only runs when the element leaves the DOM. Anything installed in hydrate() therefore has to be torn down at the top of hydrate(), not just in destroy().

That is what stop() is for here, and why hydrate() opens with it. Without that line each reload() would strand the previous interval, and every stranded one would keep writing to the same <time> node forever. Copy the shape for any widget that registers a timer, listener or observer.

Parameters

None. The widget takes no attributes.

The anchor attribute shown on the Anchor & Highlight page is not a widget parameter — emroute reads it off any widget host and turns it into an id, so :target highlighting works without the widget knowing.

Styling

clock.widget.css is scoped to the shadow root: it styles :host and the inner <time>. The colour comes from --brand-light, which is inherited from the host application, so the clock picks up your accent without being told about it.

Source

import { WidgetComponent } from "@emkodev/emroute";

interface ClockData {
  /** ISO timestamp from `getData()` — captured server-side in SSR, client-side in SPA. */
  iso: string;
}

class ClockWidget extends WidgetComponent<Record<string, never>, ClockData> {
  override readonly name = "clock";

  private timer?: number;

  override getData(): Promise<ClockData> {
    return Promise.resolve({ iso: new Date().toISOString() });
  }

  override renderHTML({ data }: this["RenderArgs"]): string {
    if (!data) return "<time>--:--:--</time>";
    return `<time datetime="${data.iso}">${formatTime(data.iso)}</time>`;
  }

  override renderMarkdown({ data }: this["RenderArgs"]): string {
    if (!data) return "`--:--:--`";
    return `\`${formatTime(data.iso)}\``;
  }

  override hydrate(): void {
    this.stop();

    const el = this.element?.shadowRoot?.querySelector("time");
    if (!el) return;

    this.timer = globalThis.setInterval(() => {
      const now = new Date();
      el.textContent = formatTime(now.toISOString());
      el.setAttribute("datetime", now.toISOString());
    }, 1000);
  }

  override destroy(): void {
    this.stop();
  }

  private stop(): void {
    if (this.timer !== undefined) {
      globalThis.clearInterval(this.timer);
      this.timer = undefined;
    }
  }
}

function formatTime(iso: string): string {
  const d = new Date(iso);
  const pad = (n: number) => String(n).padStart(2, "0");
  return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}

export default new ClockWidget();
:host {
  display: inline-flex;
  align-items: baseline;
  font-family: ui-monospace, "JetBrains Mono", SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
}

time {
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--brand-light, #6b88ff);
  text-shadow: 0 0 18px rgba(74, 108, 247, 0.35);
}

Compiled module

clock.widget.js — 1.6 kB. Component, companion files and styles in one ES module, byte-for-byte what emroute's own runtime builds when it transpiles the TypeScript above.

Save it as widgets/clock/clock.widget.js. The runtime picks it up on its next scan and <widget-clock> starts resolving — the inlined __files export carries the styles, so there is nothing else to copy.

It imports @emkodev/emroute. Resolve it through your import map or bundler; the module needs nothing else.

View clock.widget.js
import { WidgetComponent } from "@emkodev/emroute";
class ClockWidget extends WidgetComponent {
  name = "clock";
  timer;
  getData() {
    return Promise.resolve({ iso: (/* @__PURE__ */ new Date()).toISOString() });
  }
  renderHTML({ data }) {
    if (!data) return "<time>--:--:--</time>";
    return `<time datetime="${data.iso}">${formatTime(data.iso)}</time>`;
  }
  renderMarkdown({ data }) {
    if (!data) return "`--:--:--`";
    return `\`${formatTime(data.iso)}\``;
  }
  hydrate() {
    this.stop();
    const el = this.element?.shadowRoot?.querySelector("time");
    if (!el) return;
    this.timer = globalThis.setInterval(() => {
      const now = /* @__PURE__ */ new Date();
      el.textContent = formatTime(now.toISOString());
      el.setAttribute("datetime", now.toISOString());
    }, 1e3);
  }
  destroy() {
    this.stop();
  }
  stop() {
    if (this.timer !== void 0) {
      globalThis.clearInterval(this.timer);
      this.timer = void 0;
    }
  }
}
function formatTime(iso) {
  const d = new Date(iso);
  const pad = (n) => String(n).padStart(2, "0");
  return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
var stdin_default = new ClockWidget();
export {
  stdin_default as default
};

export const __files = {
  css: `:host {
  display: inline-flex;
  align-items: baseline;
  font-family: ui-monospace, "JetBrains Mono", SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
}

time {
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--brand-light, #6b88ff);
  text-shadow: 0 0 18px rgba(74, 108, 247, 0.35);
}
`
};