calendar day-view drag-drop task-scheduling time-blocking productivity planner drag drop task scheduling time blocks calendar planner sidebar drag task onto calendar schedule task by dropping on time slot time-blocking planner with task list
Calendar Day View with Task Drop
Fetch pattern JSON:
curl https://webspire.de/patterns/calendar-day/task-drop.json task-drop.html
<!-- Calendar Day View + Task Drop — drag tasks from sidebar onto calendar slots -->
<div class="ws-calendar-day bg-[var(--ws-color-surface)] rounded-2xl border border-slate-200 overflow-hidden flex" style="height:600px">
<!-- Task sidebar -->
<aside class="w-52 border-r border-slate-100 flex flex-col flex-shrink-0">
<div class="px-3 py-2.5 border-b border-slate-100">
<h3 class="text-[10px] font-bold uppercase tracking-widest text-slate-400">Aufgaben</h3>
</div>
<ul class="flex-1 overflow-y-auto p-2 flex flex-col gap-1" id="task-list">
<li draggable="true" data-task-id="t1" data-task-title="API-Dokumentation schreiben"
class="flex items-center gap-2 px-2.5 py-2 rounded-lg border border-slate-200 bg-[var(--ws-color-surface)] text-xs text-slate-700 cursor-grab active:cursor-grabbing hover:border-blue-400 hover:shadow-sm transition-all select-none">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500 flex-shrink-0"></span>
API-Dokumentation
</li>
<li draggable="true" data-task-id="t2" data-task-title="Code Review"
class="flex items-center gap-2 px-2.5 py-2 rounded-lg border border-slate-200 bg-[var(--ws-color-surface)] text-xs text-slate-700 cursor-grab active:cursor-grabbing hover:border-blue-400 hover:shadow-sm transition-all select-none">
<span class="w-1.5 h-1.5 rounded-full bg-violet-500 flex-shrink-0"></span>
Code Review
</li>
<li draggable="true" data-task-id="t3" data-task-title="Wochenplanung"
class="flex items-center gap-2 px-2.5 py-2 rounded-lg border border-slate-200 bg-[var(--ws-color-surface)] text-xs text-slate-700 cursor-grab active:cursor-grabbing hover:border-blue-400 hover:shadow-sm transition-all select-none">
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 flex-shrink-0"></span>
Wochenplanung
</li>
<li draggable="true" data-task-id="t4" data-task-title="E-Mails beantworten"
class="flex items-center gap-2 px-2.5 py-2 rounded-lg border border-slate-200 bg-[var(--ws-color-surface)] text-xs text-slate-700 cursor-grab active:cursor-grabbing hover:border-blue-400 hover:shadow-sm transition-all select-none">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 flex-shrink-0"></span>
E-Mails
</li>
<li draggable="true" data-task-id="t5" data-task-title="Tests schreiben"
class="flex items-center gap-2 px-2.5 py-2 rounded-lg border border-slate-200 bg-[var(--ws-color-surface)] text-xs text-slate-700 cursor-grab active:cursor-grabbing hover:border-blue-400 hover:shadow-sm transition-all select-none">
<span class="w-1.5 h-1.5 rounded-full bg-red-500 flex-shrink-0"></span>
Tests schreiben
</li>
</ul>
<p class="px-3 py-2 text-[10px] text-slate-400 border-t border-slate-100">↗ Auf Zeitslot ziehen</p>
</aside>
<!-- Calendar grid -->
<div class="flex-1 overflow-y-auto relative" id="calendar-grid">
<div class="relative" style="height:640px"><!-- 10h × 64px -->
<!-- Hour rows 8:00–18:00 -->
<div class="absolute inset-0 pointer-events-none select-none">
<div class="absolute left-0 right-0 flex items-start" style="top:0;height:64px"><span class="w-10 text-right pr-2 pt-1 text-[10px] text-slate-400">08:00</span><div class="flex-1 border-t border-slate-100 mt-[10px]"></div></div>
<div class="absolute left-0 right-0 flex items-start" style="top:64px;height:64px"><span class="w-10 text-right pr-2 pt-1 text-[10px] text-slate-400">09:00</span><div class="flex-1 border-t border-slate-100 mt-[10px]"></div></div>
<div class="absolute left-0 right-0 flex items-start" style="top:128px;height:64px"><span class="w-10 text-right pr-2 pt-1 text-[10px] text-slate-400">10:00</span><div class="flex-1 border-t border-slate-100 mt-[10px]"></div></div>
<div class="absolute left-0 right-0 flex items-start" style="top:192px;height:64px"><span class="w-10 text-right pr-2 pt-1 text-[10px] text-slate-400">11:00</span><div class="flex-1 border-t border-slate-100 mt-[10px]"></div></div>
<div class="absolute left-0 right-0 flex items-start" style="top:256px;height:64px"><span class="w-10 text-right pr-2 pt-1 text-[10px] text-slate-400">12:00</span><div class="flex-1 border-t border-slate-100 mt-[10px]"></div></div>
<div class="absolute left-0 right-0 flex items-start" style="top:320px;height:64px"><span class="w-10 text-right pr-2 pt-1 text-[10px] text-slate-400">13:00</span><div class="flex-1 border-t border-slate-100 mt-[10px]"></div></div>
<div class="absolute left-0 right-0 flex items-start" style="top:384px;height:64px"><span class="w-10 text-right pr-2 pt-1 text-[10px] text-slate-400">14:00</span><div class="flex-1 border-t border-slate-100 mt-[10px]"></div></div>
<div class="absolute left-0 right-0 flex items-start" style="top:448px;height:64px"><span class="w-10 text-right pr-2 pt-1 text-[10px] text-slate-400">15:00</span><div class="flex-1 border-t border-slate-100 mt-[10px]"></div></div>
<div class="absolute left-0 right-0 flex items-start" style="top:512px;height:64px"><span class="w-10 text-right pr-2 pt-1 text-[10px] text-slate-400">16:00</span><div class="flex-1 border-t border-slate-100 mt-[10px]"></div></div>
<div class="absolute left-0 right-0 flex items-start" style="top:576px;height:64px"><span class="w-10 text-right pr-2 pt-1 text-[10px] text-slate-400">17:00</span><div class="flex-1 border-t border-slate-100 mt-[10px]"></div></div>
</div>
<!-- 15-min invisible drop slots (8:00–18:00, 40 slots × 16px each) -->
<div class="absolute pointer-events-none" style="left:44px;right:6px;top:0;bottom:0" id="slot-container">
<!-- Slots generated by JS below; shown statically as example -->
<div class="absolute left-0 right-0 border-2 border-dashed border-transparent hover:border-blue-300 rounded transition-colors" data-time-slot="08:00" style="top:0;height:16px"></div>
<div class="absolute left-0 right-0 border-2 border-dashed border-transparent hover:border-blue-300 rounded transition-colors" data-time-slot="08:15" style="top:16px;height:16px"></div>
<div class="absolute left-0 right-0 border-2 border-dashed border-transparent hover:border-blue-300 rounded transition-colors" data-time-slot="08:30" style="top:32px;height:16px"></div>
<div class="absolute left-0 right-0 border-2 border-dashed border-transparent hover:border-blue-300 rounded transition-colors" data-time-slot="08:45" style="top:48px;height:16px"></div>
<!-- ... more slots ... -->
</div>
<!-- Example placed event -->
<div class="absolute rounded-lg bg-blue-50 border-l-[3px] border-blue-500 px-2.5 py-1.5 overflow-hidden"
style="left:44px;right:6px;top:64px;height:96px">
<p class="text-xs font-semibold text-blue-700">API-Dokumentation</p>
<p class="text-[10px] text-blue-500 mt-0.5">09:00 – 10:30</p>
</div>
<!-- Now line: 10:15 → (2.25)*64 = 144px from 8:00 -->
<div class="absolute left-0 right-0 z-10 pointer-events-none flex items-center" style="top:144px">
<span class="w-10 text-right pr-1 text-[10px] font-bold text-red-500 select-none">10:15</span>
<div class="flex-1 h-px bg-red-500"><div class="w-2 h-2 rounded-full bg-red-500 -mt-[3px] -ml-px"></div></div>
</div>
</div>
</div>
</div>
<script>
const taskList = document.getElementById('task-list');
const grid = document.getElementById('calendar-grid');
const DAY_START = 8;
const HOUR_PX = 64;
// Drag source
taskList?.addEventListener('dragstart', (e) => {
const li = e.target.closest('[data-task-id]');
if (!li) return;
e.dataTransfer.setData('task-id', li.dataset.taskId);
e.dataTransfer.setData('task-title', li.dataset.taskTitle);
e.dataTransfer.effectAllowed = 'copy';
});
// Drop target
grid?.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
const rect = grid.getBoundingClientRect();
const y = e.clientY - rect.top + grid.scrollTop;
const minutes = (y / HOUR_PX) * 60;
const snapped = Math.round(minutes / 15) * 15;
document.querySelectorAll('[data-drop-preview]').forEach(el => el.remove());
const preview = document.createElement('div');
preview.dataset.dropPreview = '1';
preview.className = 'absolute left-11 right-1.5 rounded-lg border-2 border-dashed border-blue-400 bg-blue-50/50 pointer-events-none';
preview.style.cssText = `top:${(snapped/60)*HOUR_PX}px;height:${HOUR_PX}px`;
grid.querySelector('.relative')?.appendChild(preview);
});
grid?.addEventListener('dragleave', () => {
document.querySelectorAll('[data-drop-preview]').forEach(el => el.remove());
});
grid?.addEventListener('drop', (e) => {
e.preventDefault();
document.querySelectorAll('[data-drop-preview]').forEach(el => el.remove());
const title = e.dataTransfer.getData('task-title');
if (!title) return;
const rect = grid.getBoundingClientRect();
const y = e.clientY - rect.top + grid.scrollTop;
const minutes = Math.round((y / HOUR_PX * 60) / 15) * 15;
const h = DAY_START + Math.floor(minutes / 60);
const m = minutes % 60;
const label = `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;
const block = document.createElement('div');
block.className = 'absolute rounded-lg bg-violet-50 border-l-[3px] border-violet-500 px-2.5 py-1.5 overflow-hidden';
block.style.cssText = `left:44px;right:6px;top:${(minutes/60)*HOUR_PX}px;height:${HOUR_PX}px`;
block.innerHTML = `<p class="text-xs font-semibold text-violet-700">${title}</p><p class="text-[10px] text-violet-500 mt-0.5">${label}</p>`;
grid.querySelector('.relative')?.appendChild(block);
});
</script>
Details
Dark Mode Tailwind Only Copy & Paste Requires JS
Stable Published
calendarday-viewdrag-droptask-schedulingtime-blockingproductivityplanner
Slots
| Name | Required | Description |
|---|---|---|
| task-list | Yes | Sidebar with draggable task items. Each item carries a data-task attribute. |
| calendar-grid | Yes | Day-view grid with data-time-slot drop targets on 15-minute intervals. |
Props
| Name | Type | Default | Description |
|---|---|---|---|
| dayStart | number | 6 | First visible hour. |
| dayEnd | number | 20 | Last visible hour. |
An enhanced two-panel layout combining the day-view calendar grid with a task sidebar. Users drag tasks from the list onto 15-minute time slots in the calendar to create scheduled time blocks. Uses the native HTML Drag and Drop API — no external dependencies required.