This document provides comprehensive suggestions for improving the drag & drop user experience in the Radzen DataGrid component, including visual feedback, animations, accessibility, and modern UI patterns.
Current State: Default browser drag behavior
Improvement: Custom drag ghost with enhanced styling
/* Add to your CSS file */
.rz-drag-ghost {
position: fixed;
pointer-events: none;
z-index: 9999;
opacity: 0.8;
background: var(--rz-primary, #0d6efd);
color: white;
padding: 8px 16px;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.rz-drag-ghost::before {
content: '⋮⋮';
font-size: 16px;
opacity: 0.7;
}Purpose: Show users which columns are draggable
.rz-sortable-column:not(.rz-frozen-column) {
cursor: grab;
transition: background-color 0.2s ease;
}
.rz-sortable-column:not(.rz-frozen-column):hover {
background-color: rgba(13, 110, 253, 0.05);
}
.rz-sortable-column.rz-dragging {
cursor: grabbing;
opacity: 0.5;
}
/* Add drag handle icon */
.rz-sortable-column .rz-column-title::before {
content: '⋮⋮';
margin-right: 6px;
opacity: 0;
transition: opacity 0.2s ease;
color: #6c757d;
font-size: 12px;
}
.rz-sortable-column:hover .rz-column-title::before {
opacity: 0.6;
}Purpose: Make the drop zone more prominent and inviting
/* Enhanced drop zone styling */
.rz-group-header-drop {
position: relative;
padding: 24px;
border: 2px dashed #cbd5e0;
border-radius: 8px;
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
color: #718096;
text-align: center;
font-size: 14px;
transition: all 0.3s ease;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
.rz-group-header-drop::before {
content: '📋';
font-size: 24px;
margin-right: 8px;
opacity: 0.5;
}
/* Active drop zone state */
.rz-group-header-drop.rz-drag-over {
border-color: var(--rz-primary, #0d6efd);
border-style: solid;
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
color: var(--rz-primary, #0d6efd);
transform: scale(1.02);
box-shadow: 0 4px 16px rgba(13, 110, 253, 0.2);
}
.rz-group-header-drop.rz-drag-over::before {
opacity: 1;
animation: bounce 0.6s ease infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}Purpose: Show where the column will be dropped
/* Drop indicator line */
.rz-drop-indicator {
position: absolute;
width: 3px;
background: var(--rz-primary, #0d6efd);
height: 100%;
top: 0;
pointer-events: none;
z-index: 100;
animation: pulse 1s ease-in-out infinite;
}
.rz-drop-indicator::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 8px solid var(--rz-primary, #0d6efd);
}
.rz-drop-indicator::after {
content: '';
position: absolute;
bottom: -4px;
left: -4px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 8px solid var(--rz-primary, #0d6efd);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}Purpose: Provide a clear, touchable drag handle for rows
/* Add drag handle column */
.rz-row-drag-handle {
cursor: grab;
padding: 8px 12px;
color: #9ca3af;
transition: all 0.2s ease;
width: 40px;
text-align: center;
}
.rz-row-drag-handle:hover {
color: var(--rz-primary, #0d6efd);
background-color: rgba(13, 110, 253, 0.05);
}
.rz-row-drag-handle:active {
cursor: grabbing;
}
.rz-row-drag-handle::before {
content: '☰';
font-size: 18px;
}
/* Dragging row state */
tr.rz-row-dragging {
opacity: 0.5;
background-color: #f3f4f6 !important;
}
/* Drop target row */
tr.rz-row-drop-target {
border-top: 3px solid var(--rz-primary, #0d6efd);
position: relative;
}
tr.rz-row-drop-target::before {
content: '';
position: absolute;
left: 0;
top: -3px;
width: 100%;
height: 3px;
background: var(--rz-primary, #0d6efd);
box-shadow: 0 0 8px rgba(13, 110, 253, 0.5);
}Purpose: Animate columns when reordering
.rz-grid-table th,
.rz-grid-table td {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Slide animation for grouped items */
.rz-group-header-item {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Remove animation */
.rz-group-header-item.rz-removing {
animation: slideOut 0.3s ease-out forwards;
}
@keyframes slideOut {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(20px);
}
}Purpose: Add delightful feedback for user actions
/* Hover effect on group items */
.rz-group-header-item {
transition: all 0.2s ease;
position: relative;
}
.rz-group-header-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Remove button pulse on hover */
.rz-group-header-item:hover .rz-dialog-titlebar-close {
animation: subtle-pulse 0.3s ease;
}
@keyframes subtle-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}<!-- In your Razor component -->
<div class="rz-group-header-drop"
role="region"
aria-label="Drop columns here to group by"
aria-dropeffect="move"
tabindex="0">
@GroupPanelText
</div>
<!-- Draggable column headers -->
<th draggable="true"
role="columnheader"
aria-grabbed="false"
tabindex="0"
@onkeydown="HandleKeyboardDrag">
<!-- Column content -->
</th>// Add to @code section
private async Task HandleKeyboardDrag(KeyboardEventArgs e)
{
// Space or Enter to start drag
if (e.Key == " " || e.Key == "Enter")
{
// Toggle drag mode
isKeyboardDragActive = !isKeyboardDragActive;
}
// Arrow keys to move
if (isKeyboardDragActive)
{
if (e.Key == "ArrowLeft" || e.Key == "ArrowRight")
{
// Move column left or right
}
else if (e.Key == "Escape")
{
// Cancel drag
isKeyboardDragActive = false;
}
}
}Purpose: Show count when dragging multiple items
.rz-drag-ghost.rz-multi-drag::after {
content: attr(data-count);
position: absolute;
top: -8px;
right: -8px;
background: #dc3545;
color: white;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}Purpose: Show where items cannot be dropped
.rz-drop-zone-invalid {
position: relative;
}
.rz-drop-zone-invalid::after {
content: '🚫';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 32px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.rz-drop-zone-invalid.rz-drag-over::after {
opacity: 0.7;
}
.rz-drop-zone-invalid.rz-drag-over {
background-color: rgba(220, 53, 69, 0.05);
border-color: #dc3545;
}Purpose: Provide feedback and quick undo option
.rz-drag-toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: #1f2937;
color: white;
padding: 12px 24px;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
gap: 16px;
animation: toastSlideIn 0.3s ease-out;
z-index: 10000;
}
.rz-drag-toast button {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.rz-drag-toast button:hover {
background: rgba(255, 255, 255, 0.1);
border-color: white;
}
@keyframes toastSlideIn {
from {
opacity: 0;
transform: translate(-50%, 20px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}Purpose: Ensure drag handles work well on touch devices
@media (max-width: 768px) {
.rz-row-drag-handle {
width: 48px; /* Larger touch target */
padding: 12px;
}
.rz-group-header-drop {
min-height: 80px;
padding: 32px 16px;
font-size: 16px;
}
.rz-group-header-item {
padding: 12px 16px;
margin: 8px;
}
/* Disable hover effects on touch devices */
.rz-sortable-column:hover {
background-color: transparent;
}
}// Add to your JavaScript interop
let touchTimer;
const longPressDuration = 500; // ms
element.addEventListener('touchstart', (e) => {
touchTimer = setTimeout(() => {
element.classList.add('rz-long-press-active');
// Start drag operation
startDrag(e);
}, longPressDuration);
});
element.addEventListener('touchend', () => {
clearTimeout(touchTimer);
element.classList.remove('rz-long-press-active');
});.rz-long-press-active {
animation: vibrate 0.1s linear infinite;
}
@keyframes vibrate {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}Add a new JavaScript file radzen-datagrid-dragdrop.js:
window.RadzenDataGridDragDrop = {
createDragGhost: function (text, count) {
const ghost = document.createElement('div');
ghost.className = 'rz-drag-ghost';
ghost.textContent = text;
if (count > 1) {
ghost.classList.add('rz-multi-drag');
ghost.setAttribute('data-count', count);
}
document.body.appendChild(ghost);
return ghost;
},
updateGhostPosition: function (ghost, x, y) {
if (ghost) {
ghost.style.left = (x + 10) + 'px';
ghost.style.top = (y + 10) + 'px';
}
},
removeDragGhost: function (ghost) {
if (ghost && ghost.parentNode) {
ghost.style.animation = 'fadeOut 0.2s ease-out';
setTimeout(() => ghost.remove(), 200);
}
},
showDropIndicator: function (element, position) {
const indicator = document.createElement('div');
indicator.className = 'rz-drop-indicator';
indicator.style.left = position + 'px';
element.appendChild(indicator);
return indicator;
},
showToast: function (message, undoCallback) {
const toast = document.createElement('div');
toast.className = 'rz-drag-toast';
toast.innerHTML = `
<span>${message}</span>
<button onclick="this.parentElement.remove(); ${undoCallback}">Undo</button>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toastSlideOut 0.3s ease-out';
setTimeout(() => toast.remove(), 300);
}, 5000);
}
};@code {
private ElementReference dragGhostRef;
private bool isKeyboardDragActive = false;
private string? draggedColumnId;
private Stack<Action> undoStack = new();
private async Task OnColumnDragStart(RadzenDataGridColumn<TItem> column, DragEventArgs e)
{
draggedColumnId = column.UniqueID;
e.DataTransfer.EffectAllowed = "move";
// Create custom drag ghost
await JSRuntime.InvokeVoidAsync(
"RadzenDataGridDragDrop.createDragGhost",
column.Title,
1
);
}
private async Task OnColumnDragEnd(DragEventArgs e)
{
await JSRuntime.InvokeVoidAsync(
"RadzenDataGridDragDrop.removeDragGhost"
);
draggedColumnId = null;
}
private async Task OnDropZoneDragOver(DragEventArgs e)
{
e.DataTransfer.DropEffect = "move";
// Add visual feedback class
await JSRuntime.InvokeVoidAsync(
"eval",
"document.querySelector('.rz-group-header-drop').classList.add('rz-drag-over')"
);
}
private async Task OnDropZoneDragLeave(DragEventArgs e)
{
await JSRuntime.InvokeVoidAsync(
"eval",
"document.querySelector('.rz-group-header-drop').classList.remove('rz-drag-over')"
);
}
private async Task OnDrop(DragEventArgs e)
{
// Perform the drop operation
// ... existing logic ...
// Show toast with undo option
await JSRuntime.InvokeVoidAsync(
"RadzenDataGridDragDrop.showToast",
"Column grouped successfully",
"undoLastGrouping()"
);
}
}:root {
--rz-drag-ghost-bg: #0d6efd;
--rz-drag-ghost-color: white;
--rz-drop-zone-border: #cbd5e0;
--rz-drop-zone-border-active: #0d6efd;
--rz-drop-zone-bg: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
--rz-drop-zone-bg-active: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
--rz-drag-handle-color: #9ca3af;
--rz-drag-handle-color-hover: #0d6efd;
--rz-drop-indicator-color: #0d6efd;
--rz-drag-animation-duration: 0.3s;
}
/* Dark mode support */
[data-theme="dark"] {
--rz-drag-ghost-bg: #3b82f6;
--rz-drop-zone-border: #4b5563;
--rz-drop-zone-border-active: #3b82f6;
--rz-drop-zone-bg: linear-gradient(135deg, #1f2937 0%, #111827 100%);
--rz-drop-zone-bg-active: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
--rz-drag-handle-color: #6b7280;
--rz-drag-handle-color-hover: #3b82f6;
}- Test drag & drop with mouse
- Test drag & drop with touch on mobile devices
- Test keyboard navigation (Tab, Space, Enter, Arrow keys, Escape)
- Test screen reader announcements
- Test with multiple columns/rows
- Test undo/redo functionality
- Test drop zone validation (valid/invalid drops)
- Test animation performance (60fps target)
- Test on various browsers (Chrome, Firefox, Safari, Edge)
- Test with reduced motion preferences
- Test dark mode compatibility
/* Use transform instead of position for better performance */
.rz-sortable-column {
will-change: transform;
}
/* Reduce motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Use with drag over events
const handleDragOver = debounce((e) => {
// Update drop indicator position
}, 16); // ~60fpsThese enhancements will provide:
- ✅ Better Visual Feedback - Users can clearly see what's draggable and where items can be dropped
- ✅ Smooth Animations - Professional transitions and micro-interactions
- ✅ Improved Accessibility - Keyboard navigation and screen reader support
- ✅ Mobile Optimization - Touch-friendly interactions with long-press support
- ✅ Advanced Features - Undo/redo, multi-drag, and constraint visualization
- ✅ Themeable - CSS custom properties for easy customization
- Add the CSS styles to your global stylesheet or component-specific CSS file
- Implement the JavaScript interop functions
- Update the Razor component with the enhanced event handlers
- Test thoroughly across devices and browsers
- Adjust colors and animations to match your brand
Feel free to implement these changes incrementally, starting with the most impactful improvements like visual feedback and drop zone highlighting.