In May, we released SVAR Svelte Calendar, a lightweight open-source event calendar that brings a Google Calendar-like scheduler to Svelte and SvelteKit apps: day, week, and month views, drag-and-drop event editing, multiple calendars, a custom edit form, and event customization. An optional PRO Edition adds resource management views and recurring events.
Like other SVAR components, the calendar doesn’t force a styling approach on your project. It ships with prebuilt styles via customizable CSS variables, its own light and dark themes, and requires no CSS framework to look great. At the same time, it can coexist comfortably with the CSS pattern and UI library your project already uses. This article demonstrates that.
Calendar Demo with Tailwind CSS & daisyUI
Recently, our user Fernando Humanes shared on our forum that he’d built a working demo that pairs SVAR Svelte Calendar with Tailwind CSS and daisyUI. We thought it was a great chance to show how our Svelte calendar component can be used with these popular libraries.
The example renders database records as calendar events, with day, week, and month views, event details displayed in daisyUI drawers, and data loaded on demand from a MySQL database through a PHP Slim backend.
Fernando documented the whole exercise in a detailed tutorial on his blog in Spanish, and this post walks through it in English with the actual code.
Frontend: Svelte 5, Tailwind CSS, daisyUI
Backend: PHP Slim 4, MySQL
Tutorial: Integración SVAR Calendar con Tailwind CSS y Daisy UI en Svelte 5
Demo: Live demo
Tailwind CSS and daisyUI in brief
Tailwind CSS is a utility-first CSS framework, used broadly, from headless libraries like Melt UI to component kits like Skeleton and daisyUI. It allows you to compose styles directly in your markup from small utility classes like flex, p-4, rounded-lg – instead of writing custom CSS classes. This keeps styles close to the code that uses them and eliminates a whole class of CSS maintenance.
daisyUI is a component library built on top of Tailwind. It includes class-based components (like buttons, cards, modals, drawers, simple tables) with a set of ready-made themes, so you get a consistent interface without writing component CSS or JavaScript.
The combination of both is popular with Svelte developers because it makes UI iteration fast: markup stays readable, and design stays consistent.
The Integration, Step by Step
Fernando’s starting point was a Svelte 5 project from his earlier tutorial: a reusable DataGrid component styled with daisyUI, powered by a PHP Slim API. The idea for this exercise was simple — take the same data and render it as a calendar instead of a grid. The whole example is written with Svelte 5 runes ($state, $derived, $props).
Swapping the component
The project reuses everything from the DataGrid solution; the DataGrid component is replaced with the SVAR Calendar, and the rest of the app — pages, navigation, drawers — stays untouched. Here’s what the calendar module imports from the SVAR packages:
<script> import { Calendar, Willow, ContextMenu } from "@svar-ui/svelte-calendar"; import { Locale } from "@svar-ui/svelte-core"; import { en as enCore, es as esCore } from "@svar-ui/core-locales"; import { en, es } from "@svar-ui/calendar-locales";
import EventCard from "./EventCard.svelte"; import EventDrawer from "./EventDrawer.svelte";</script>Note the localization setup: Locale (SVAR Core) receives merged dictionaries from @svar-ui/calendar-locales and @svar-ui/core-locales, so the calendar UI is fully bilingual (Spanish/English) with a single state variable. The component is wrapped in {#key locale}, which re-renders the calendar when the language switches.
Views and weekend highlighting
The calendar component comes with day, week, and month views out of the box. Fernando narrows the working hours for the time-based views and uses the cellCss callback to highlight weekends and holidays:
const views = [ { id: "month", sections: { month: { yScale: { visible: true, format: "weekNumberFormat" } } } }, { id: "week", sections: { timeGrid: { yScale: { startHour: 9, endHour: 17 } } } }, { id: "day", sections: { timeGrid: { yScale: { startHour: 9, endHour: 17 } } } }];
// Configuración de celdas para resaltar fines de semana y días festivosfunction cellCss(ctx) { const { date } = ctx; if (!date) return ""; const day = date.getDay(); if (day === 0 || day === 6) return "weekend"; if (day === 5) return "holiday"; return "";}The returned class names are styled with a couple of global rules: light purple for weekends, light red for holidays.
Loading events per visible range
Fernando also shared a performance pattern that keeps the calendar fast even with large numbers of events.
In his demo, the calendar doesn’t fetch all events at once: when the view changes or the user navigates to another date range, the app computes the visible range and requests only the events that fall into it. getRangeFromView turns the current view into MySQL-formatted start/end timestamps:
function getRangeFromView(baseDate, view) { const start = new Date(baseDate); const end = new Date(baseDate);
if (view === "day") { // Día completo start.setHours(0, 0, 0, 0); end.setHours(23, 59, 59, 999); } else if (view === "week") { // Semana completa (lunes a domingo) const day = start.getDay(); // 0=domingo, 1=lunes... const diff = (day === 0 ? -6 : 1 - day); // mover al lunes start.setDate(start.getDate() + diff); start.setHours(0, 0, 0, 0);
end.setTime(start.getTime()); end.setDate(start.getDate() + 6); end.setHours(23, 59, 59, 999); } else if (view === "month") { // Primer día del mes start.setDate(1); start.setHours(0, 0, 0, 0);
// Último día del mes end.setMonth(start.getMonth() + 1); end.setDate(0); // día 0 = último del mes anterior end.setHours(23, 59, 59, 999); }
return { start: toMySQL(start), end: toMySQL(end) };}The onnavigateto hook fires on every view or date change, so the handler updates the base date and view, recomputes the range, and refetches:
<Calendar events={eventsCalendar} eventCss={cssByCalendar} view="month" views={views} date={new Date()} readonly={true} init={handleInit} eventPopup={EventCard} onnavigateto={p => { fechaBase = p.date ?? fechaBase; vistaCalendar = p.view ?? vistaCalendar;
const range = getRangeFromView(fechaBase, vistaCalendar); start = range.start; end = range.end;
fetchEvents(); // llamada a tu backend para recargar eventos según el nuevo rango }} {cellCss}/>And fetchEvents sends that range to the backend:
async function fetchEvents() { try { const resEvents = await apiRest.post("/event/list", { start: start, end: end });
events = resEvents.data; filteredData = filterData(); } catch (err) { errorMessage = "No se pudieron cargar los eventos."; } finally { loading = false; }}This keeps the initial load light and the calendar fast as the dataset grows.
Mapping data to the calendar’s structure
Fernando feeds the same data to both the grid and the calendar. The original records stay untouched for the daisyUI drawers; a thin mapping layer reshapes them into what the SVAR Calendar component expects: text, start, end, allDay, calendarId.
One detail worth mentioning is the handling of all-day events. If you pass an all-day event as a plain date, SVAR Calendar will stretch it across the full 24 hours; if you clamp it to 01:00–23:00, the calendar renders as one tidy block (as it should).
eventsCalendar = filtered.map(e => { const isAllDay = e.all_day === "1";
// Convertimos fechas const start = new Date(e.start); const end = new Date(e.end);
if (isAllDay) { // Ajuste para evitar que SVAR Calendar reparta el día en 24 horas start.setHours(1, 0, 0, 0); // 01:00:00 end.setHours(23, 0, 0, 0); // 23:00:00 }
return { ...e, id: e.id, text: e.title, start, end, allDay: isAllDay, calendarId: e.id_calendar, // clave para el color };});Event cards with daisyUI
Clicking an event opens a popup card built with daisyUI, providing the same visual language as the rest of the app. In Fernando’s demo, the card shows event details, such as start/end time, description, calendar group (team, personal, celebrations), priority, location, and users assigned.
The component receives the event and a close callback through Svelte 5 $props. Below is a trimmed version with the key parts. The complete EventCard (including the Spanish date formatting) is in the source files on his blog:
<script> // The popup is a plain Svelte component: it receives the event // and a close callback via Svelte 5 props const { event, close } = $props();
const headerStyle = `background-color: ${event.calendar_color}`; const borderStyle = `border-color: ${event.calendar_color}`; const people = event.names_people ? event.names_people.split(",").map(p => p.trim()) : [];</script>
<div class="event-card w-80 rounded-lg shadow-xl bg-base-100 p-0 overflow-hidden border-2" style={borderStyle}>
<!-- The header uses the event's calendar color --> <header class="text-center text-white font-bold py-3" style={headerStyle}> {event.title} </header>
<div class="p-4 space-y-3"> <!-- daisyUI badges for people and resources --> {#each people as p} <span class="badge badge-primary badge-sm">{p}</span> {/each}
<button class="btn btn-sm btn-outline" onclick={close}>Cerrar</button> </div></div>The rest is plain daisyUI markup — you can style the popup with any classes from your design system.
Adding and editing events
The calendar runs in readonly mode — Fernando turns off the built-in event editor and reuses the add/edit drawer from his DataGrid solution, so event management behaves exactly like in the grid. The ”+” button opens a daisyUI drawer with an empty event form. Saving the form posts the data to the same PHP Slim API (POST /event, PUT /event/{id}).
Right-clicking an event brings up a context menu with view, edit, and delete options:
const contextOptions = [ { id: "view", text: "Ver", icon: "wxi-eye" }, { id: "edit", text: "Editar", icon: "wxi-edit" }, { id: "delete", text: "Eliminar", icon: "wxi-delete-outline" }];
Deleting asks for confirmation through SweetAlert2 before calling the API. The ContextMenu component wraps the calendar and wires these actions to the same handlers the grid uses.
Per-calendar colors
The demo includes 3 types of calendars: Personal, Team, and Celebrations. Each calendar in the app has its own color. Instead of hardcoding rules, Fernando generates them at runtime: handleInit runs when the calendar initializes and injects a <style> block with one rule per calendar, covering all event renderers:
function handleInit(api) { console.log("Calendar inicializado, generando CSS dinámico…"); const style = document.createElement("style");
style.innerHTML = calendars.map(c => ` .cal-${c.idsvar_calendar}.wx-box-event, .cal-${c.idsvar_calendar}.wx-bar-event, .cal-${c.idsvar_calendar}.wx-month-event, .cal-${c.idsvar_calendar}.wx-month-box-event { background-color: ${c.color} !important; color: white !important; } `).join("\n");
document.head.appendChild(style);}Fernando notes that he deliberately wrote the code with comments throughout, so it works as a learning reference. All sources are available for download on his blog (find them at the bottom of the page):
- Svelte 5 front end
- PHP Slim 4 backend
- MySQL database backup
The takeaway
SVAR Svelte Calendar can be easily integrated into a pure Svelte 5 or SvelteKit app. It works out of the box with its own themes — no CSS framework required.
However, if your project is already built around Tailwind CSS, daisyUI, or similar libraries, the calendar component fits right in without conflicts. The tutorial shows that you can also attach daisyUI components to adjust calendar functionality to fit your needs, like showing a popup with details on click. Fernando’s demo is a great reference for that.
Try the demo, download the sources, and see how SVAR Svelte Calendar behaves in your own stack:
Build great scheduling UIs with SVAR Svelte Calendar and your libraries of choice!
This guide is based on “S-028 – Integración SVAR Calendar con Tailwind CSS y Daisy UI en Svelte5” by Fernando Humanes, published with the author’s permission.