Reticle@etched/reticle 0.0.0

Hover card

A card of extra detail that opens on hover and on keyboard focus, after a delay: notes, activations, references. Keyboard users see it but cannot tab into it, and screen readers do not announce it, so its content must also be reachable another way, such as the trigger's own page.

2 notes
import { Link } from "@etched/reticle/link";
import { Text } from "@etched/reticle/text";
import { HoverCard } from "@etched/reticle/hover-card";

interface Note {
  author: string;
  date: string;
  text: string;
}

const notes: Note[] = [
  { author: "Sam Lee", date: "Oct 14", text: "Strong on clock trees. Wants to stay close to silicon bring-up." },
  { author: "Kai Ito", date: "Oct 9", text: "Asked about the on-call rotation and the Taipei trips." },
];

function NoteList({ items }: { items: Note[] }) {
  return (
    <div style={{ display: "grid", gap: "var(--et-space-3)", inlineSize: "18rem" }}>
      {items.map((note) => (
        <div key={note.date}>
          <Text as="p" size="sm" variant="secondary">
            {note.author} · {note.date}
          </Text>
          <Text as="p" size="sm">
            {note.text}
          </Text>
        </div>
      ))}
    </div>
  );
}

export function Preview() {
  return (
    <HoverCard content={<NoteList items={notes} />} render={<Link href="#notes" />}>
      2 notes
    </HoverCard>
  );
}

Installation

import { HoverCard } from "@etched/reticle/hover-card";

Examples

Notes

It opens on hover or keyboard focus after 600 ms, and closes 150 ms after the pointer leaves. The link's click stays its own.

Ana Ruiz has 2 notes from the last loop.

import { Link } from "@etched/reticle/link";
import { Text } from "@etched/reticle/text";
import { HoverCard } from "@etched/reticle/hover-card";

interface Note {
  author: string;
  date: string;
  text: string;
}

const notes: Note[] = [
  { author: "Sam Lee", date: "Oct 14", text: "Strong on clock trees. Wants to stay close to silicon bring-up." },
  { author: "Kai Ito", date: "Oct 9", text: "Asked about the on-call rotation and the Taipei trips." },
];

function NoteList({ items }: { items: Note[] }) {
  return (
    <div style={{ display: "grid", gap: "var(--et-space-3)", inlineSize: "18rem" }}>
      {items.map((note) => (
        <div key={note.date}>
          <Text as="p" size="sm" variant="secondary">
            {note.author} · {note.date}
          </Text>
          <Text as="p" size="sm">
            {note.text}
          </Text>
        </div>
      ))}
    </div>
  );
}

export function Notes() {
  return (
    <p>
      Ana Ruiz has{" "}
      <HoverCard content={<NoteList items={notes} />} render={<Link href="#notes" />}>
        2 notes
      </HoverCard>{" "}
      from the last loop.
    </p>
  );
}

Long content scrolls

The card is at most 24 rem high; the rest scrolls inside it.

import { HoverCard } from "@etched/reticle/hover-card";

const activations = Array.from({ length: 30 }, (_, i) => `Run ${1284 - i} ${i % 5 ? "passed" : "failed at 71 °C"}`);

export function LongContentScrolls() {
  return (
    <HoverCard
      side="right"
      content={
        <ol style={{ margin: 0, paddingInlineStart: "var(--et-space-5)" }}>
          {activations.map((activation) => (
            <li key={activation}>{activation}</li>
          ))}
        </ol>
      }
    >
      30 activations
    </HoverCard>
  );
}

Wide card

size="lg" is a fixed 32 rem card for long previews, such as a submitted referral.

import { Text } from "@etched/reticle/text";
import { HoverCard } from "@etched/reticle/hover-card";

interface Note {
  author: string;
  date: string;
  text: string;
}

const notes: Note[] = [
  { author: "Sam Lee", date: "Oct 14", text: "Strong on clock trees. Wants to stay close to silicon bring-up." },
  { author: "Kai Ito", date: "Oct 9", text: "Asked about the on-call rotation and the Taipei trips." },
];

function NoteList({ items }: { items: Note[] }) {
  return (
    <div style={{ display: "grid", gap: "var(--et-space-3)", inlineSize: "18rem" }}>
      {items.map((note) => (
        <div key={note.date}>
          <Text as="p" size="sm" variant="secondary">
            {note.author} · {note.date}
          </Text>
          <Text as="p" size="sm">
            {note.text}
          </Text>
        </div>
      ))}
    </div>
  );
}

export function WideCard() {
  return (
    <HoverCard size="lg" content={<NoteList items={notes} />}>
      Referral from Sam Lee
    </HoverCard>
  );
}

API reference

PropTypeDefaultDescription
content *ReactNode–The card. The pointer can move onto it, so links and selectable text in it work. Long content scrolls.
side"top" | "bottom" | "left" | "right""bottom"
align"start" | "center" | "end""start"
size"base" | "lg""base"lg is a fixed 32 rem card for long previews, such as a submitted referral. base fits its content up to 22 rem.
delaynumber600Milliseconds before it opens on hover or keyboard focus.
closeDelaynumber150Milliseconds before it closes after the pointer leaves the trigger and the card.
renderComponentPropsWithoutRef<typeof BasePreviewCard.Trigger>["render"]–The trigger element, for example render={<Link href="/notes" />}. Its click stays its own: the card does not open or close on click. Without it, children go in a plain button.
childrenReactNode–
classNamestring–Class on the trigger, for layout.