Skip to main content

Tooltips

This guide covers the two ways to show event details without opening the editor: a hover tooltip through the tooltip prop, and a clickable card through eventPopup. Both take a Svelte component, and the calendar owns when it mounts and where it goes.

hover tooltip floating above an event with the event title and time range

How tooltips work​

The tooltip prop takes a Svelte component, not a DOM string. The calendar render layer owns its lifecycle:

  • It listens for hover only when a tooltip component is configured.
  • It resolves the event under the cursor and instantiates the component with the right props.
  • It dismisses the tooltip on mouse leave or when an event card opens.

Two render paths exist:

SurfaceTriggerPositionProps
boxes, bars, grid, listHover an event elementFloats with the cursor{ event }
yearHover a marked day cellAnchored to the cell{ events: CalendarEvent[] }

Year view passes a list because a single day can hold multiple events. Every other view passes one event at a time. A tooltip designed for both should accept both shapes.

The widget never injects a close callback. Tooltips are display-only, with pointer-events: none in the regular overlay path. If you need clicks or buttons inside the popup, use eventPopup instead.

Passing a component​

Import a component and hand it to the tooltip prop:

<script lang="ts">
import { Calendar } from "@svar-ui/svelte-calendar";
import EventTooltip from "./EventTooltip.svelte";
</script>

<Calendar events={data} view="month" {date} tooltip={EventTooltip} />

Inside the tooltip component, declare both possible payloads so the same file works in every view:

<script lang="ts">
const { event, events } = $props<{
event?: any;
events?: any[];
}>();
</script>

{#if event}
<div class="title">{event.text}</div>
<div class="time">
{event.start.toLocaleTimeString()} - {event.end.toLocaleTimeString()}
</div>
{:else if events}
<div>{events.length} events</div>
{#each events as ev}
<div>{ev.text}</div>
{/each}
{/if}

If you only use month/week/day, the event branch is enough. Add the events branch when you also want year view to use the same component.

Positioning and styling​

The render layer handles positioning - your component only needs to render the inner content.

  • In boxes, bars, grid, and list sections, the tooltip is wrapped in a fixed-position layer that follows the pointer.
  • In year sections, it mounts inside a Popup anchored to the day cell.

Style the inside of the component - width, padding, background, typography - and let the calendar place it. The wrapping element sets pointer-events: none, so hover styles inside the tooltip will not trigger.

<style>
.event-tooltip {
background: #1e293b;
color: #f1f5f9;
border-radius: 6px;
padding: 8px 12px;
font-size: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
min-width: 140px;
max-width: 260px;
}
</style>

When no custom tooltip is provided, year view falls back to a built-in list of events for that day. Setting a custom component replaces the built-in entirely - there is no partial override.

Clickable cards with eventPopup​

A tooltip disappears on mouse leave and cannot hold buttons. When the preview needs to stay open and react to clicks, use the eventPopup prop instead: the calendar mounts your component inside an anchored Popup whenever the user clicks an event.

custom event card popup anchored to a clicked event with title, time range, and a close button

A few rules drive how it behaves:

  • Click detection is movement-aware. The internal click directive listens for mousedown / mouseup on the section container and treats the interaction as a click only when the pointer stays within a 3 px threshold. This is what separates a click from the start of a drag.
  • Resolution is store-based. Event elements expose a data-id. The directive resolves it through api.getEvent(...) to find the stored event, then opens the popup with that event.
  • eventPopup overrides the default click path. Without eventPopup, an event click dispatches select-event (which opens the editor when one is mounted). With eventPopup, the click opens the card instead - select-event is not dispatched.
  • Empty-space clicks dismiss. Clicking outside an event closes the current card. Clicking another event swaps the card to that event.
  • The component renders inside the calendar subtree. It can read any Svelte context exposed by the calendar's parent components - useful for passing app-level data such as resource lists.

The card receives two props: the resolved event object and a close callback. You decide what the card looks like and when it dismisses itself.

<!-- App.svelte -->
<script lang="ts">
import { Calendar } from "@svar-ui/svelte-calendar";
import EventCard from "./EventCard.svelte";
import { getData } from "./data";

const { data, date } = getData();
</script>

<Calendar
events={data}
{date}
view="week"
eventPopup={EventCard}
views={["day", "week", "month"]}
/>

The card component itself reads its props with $props():

<!-- EventCard.svelte -->
<script lang="ts">
const { event, close } = $props<{
event: any;
close: () => void;
}>();
</script>

<div class="event-card">
<header>{event.text}</header>
<p>{event.start.toLocaleString()} - {event.end.toLocaleString()}</p>
<button onclick={close}>Close</button>
</div>

close is the supported way to dismiss the popup from inside the card - use it after the user confirms an action, navigates away, or clicks an explicit close control.

Reading app data through context​

Because the card renders inside the calendar's subtree, it inherits Svelte context from any ancestor - including contexts you set in the parent that mounts <Calendar>:

<!-- parent component -->
<script lang="ts">
import { setContext } from "svelte";
import { Calendar } from "@svar-ui/svelte-calendar";

const resources = [
{ id: "alice", label: "Alice" },
{ id: "bob", label: "Bob" },
];

setContext("resources", resources);
</script>

<Calendar events={data} {date} eventPopup={EventCard} />

The card pulls the same context with getContext:

<script lang="ts">
import { getContext } from "svelte";

const { event } = $props<{ event: any; close: () => void }>();
const resources = getContext<Array<{ id: string; label: string }>>("resources");

const assignee = resources?.find(r => r.id === event.unit_id)?.label;
</script>

This pattern keeps domain data out of every event and lets the card resolve it on demand.

Coexisting with the editor​

The default click path opens the editor (when <Editor> is mounted) by dispatching select-event. The eventPopup prop replaces that path, so the editor will not auto-open on click while the card is active.

If you want the card and the editor side by side, route the editor explicitly from a button inside the card. Call api.exec("select-event", { id }) to open the editor for the same event the card is showing:

<!-- EventCard.svelte -->
<script lang="ts">
import { getContext } from "svelte";
import type { CalendarContextApi } from "@svar-ui/svelte-calendar";

const { event, close } = $props<{ event: any; close: () => void }>();
const api = getContext<CalendarContextApi>("calendar-api");

function openEditor() {
api.exec("select-event", { id: event.id });
close();
}
</script>

<button onclick={openEditor}>Edit</button>
<button onclick={close}>Close</button>

calendar-api is the Svelte context the calendar exposes for child components; see the API reference for the full surface (getState, getReactiveState, exec, fmt, getEvent).

If you prefer a card-only flow, leave the editor out of the tree - eventPopup does not require it.

When to use what​

  • Tooltip - a read-only preview that follows the pointer. tooltip set. No clicks inside, no editor involvement.
  • Editor only - users edit events through the form. No eventPopup. Click selects, editor opens.
  • Card only - users see a read-only or action-driven preview on click. eventPopup set, no <Editor>.
  • Card plus editor - card is the entry point, editor opens from a card action. eventPopup set, <Editor {api} /> mounted, card calls api.exec("select-event", ...).