I'm working on a custom layout using MUI where I want to display exam data in a compact scrollable table. The idea is to keep the Exam Name and Percentage columns static (not scrollable), while making the attendance data (Date, Day, Status) vertically scrollable.
Here’s the structure I’m aiming for:
- Exam name (T1, T2, T3...) – static
- Percentage – static
- Attendance records for each exam – scrollable (with Date, Day, Status)
- Each exam block (T1, T2...) is separated, with scrollable data in the center
- The status is shown using
MUI <Chip />s
Everything works except the header columns (Date, Day, Status) are not properly aligned with their respective data rows inside the scrollable area.
What I've tried:
- Gave fixed widths to each column container
- Used
display: flexwith nested Boxes for layout - Used
overflowY: autofor the scrollable part - Tried padding/margin adjustments to sync widths
- Tried wrapping header and data rows in shared
Boxcontainers
Problem:
Despite all that, the scrollable data columns do not align properly with the fixed headers above them — the cells shift slightly depending on padding/scroll/zoom.
Desired Output:
T1 ───────| Date | Day | Status | Percentage
|------|-----|--------|
| ... | ... | ✅ |
| ... | ... | ❌ | ← scrollable vertically
T2 ───────| ... | ... | ... | ...
...
Tools:
- React
- MUI v5
- CSS Modules (optional)
Question:
How can I ensure perfect alignment between the header columns and the scrollable data columns below them? Is there a better way to structure this layout using MUI Grid, Table, or Flex?
Any guidance or working example would be greatly appreciated!
CODE:
import {
Box,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Chip,
Typography,
} from '@mui/material';
const AttendanceTable = ({ data }) => {
return (
<Box
sx={{
width: '100%',
borderRadius: '16px',
p: '1.5rem',
bgcolor: 'background.default',
}}
>
<Typography variant="h3" sx={{ mb: 2 }}>
Attendance
</Typography>
<TableContainer
component={Paper}
sx={{
minWidth: '800px',
overflowY: 'auto',
boxShadow: 'none',
border: 'none',
borderRadius: '9px',
'&::-webkit-scrollbar': {
width: '6px',
},
'&::-webkit-scrollbar-track': {
background: 'transparent',
borderRadius: '8px',
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(0,0,0,0.3)',
borderRadius: '8px',
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: 'rgba(0,0,0,0.4)',
},
scrollbarWidth: 'thin',
scrollbarColor: 'rgba(0,0,0,0.3) transparent',
}}
>
<Table
size="small"
sx={{
'& td, & th': {
border: 'none',
},
}}
>
<TableHead>
<TableRow
sx={{
backgroundColor: 'background.paper',
borderBottom: '2px solid',
borderColor: 'divider',
}}
>
{['Exam', 'Date', 'Day', 'Status', 'Percentage'].map(
(heading, idx) => (
<TableCell
key={idx}
sx={{
textAlign: 'center',
fontWeight: 'bold',
py: 1.5,
px: 2,
}}
>
{heading}
</TableCell>
)
)}
</TableRow>
</TableHead>
<TableBody>
{data.exams.map((exam, examIndex) => {
const total = exam.attendanceRecords.length;
const present = exam.attendanceRecords.filter(
(r) => r.status === 'Present'
).length;
const percentage = ((present / total) * 100).toFixed(2);
return (
<TableRow
key={`exam-${examIndex}`}
sx={{
borderBottom: '2px solid',
borderColor: 'divider',
}}
>
{/* Fixed Exam Column */}
<TableCell
sx={{
textAlign: 'center',
py: 1.5,
px: 2,
verticalAlign: 'top',
width: '90px',
fontWeight: '500',
whiteSpace: 'nowrap',
}}
>
{exam.exam}
</TableCell>
{/* Scrollable Date/Day/Status Block */}
<TableCell colSpan={3} sx={{ p: 0 }}>
<Box
sx={{
maxHeight: '180px',
overflowY: 'auto',
pr: 1,
'&::-webkit-scrollbar': {
width: '4px',
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
borderRadius: '4px',
},
scrollbarWidth: 'thin',
scrollbarColor: 'rgba(0,0,0,0.3) transparent',
}}
>
{exam.attendanceRecords.map((record, idx) => (
<TableRow
key={`record-${examIndex}-${idx}`}
sx={{
backgroundColor: 'background.paper',
borderBottom:
idx === exam.attendanceRecords.length - 1
? 'none'
: '1px solid',
borderColor: 'divider',
}}
>
<TableCell
sx={{
textAlign: 'center',
py: 1.5,
px: 2,
}}
>
{record.date}
</TableCell>
<TableCell
sx={{
textAlign: 'center',
py: 1.5,
px: 2,
}}
>
{record.day}
</TableCell>
<TableCell
sx={{
textAlign: 'center',
py: 1.5,
px: 2,
}}
>
<Chip
label={record.status}
color={
record.status === 'Present'
? 'success'
: 'error'
}
size="small"
/>
</TableCell>
</TableRow>
))}
</Box>
</TableCell>
{/* Fixed Percentage Column */}
<TableCell
sx={{
textAlign: 'center',
py: 1.5,
px: 2,
verticalAlign: 'top',
whiteSpace: 'nowrap',
width: '90px',
fontWeight: '500',
}}
>
{percentage}%
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</Box>
);
};
export default AttendanceTable;
Demo Data
export const demoSubjectAttendance = {
[![enter image description here][1]][1]
exams: [
{
exam: 'T1',
startDate: '2025-07-01',
endDate: '2025-07-05',
percentage: '66.67%', // 2/3
attendanceRecords: [
{
date: '2025-07-01',
day: 'Tuesday',
status: 'Present',
},
{
date: '2025-07-02',
day: 'Wednesday',
status: 'Absent',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-07-03',
day: 'Thursday',
status: 'Present',
},
],
},
{
exam: 'T2',
startDate: '2025-08-01',
endDate: '2025-08-05',
percentage: '100.00%', // 3/3
attendanceRecords: [
{
date: '2025-08-01',
day: 'Friday',
status: 'Present',
},
{
date: '2025-08-02',
day: 'Saturday',
status: 'Present',
},
{
date: '2025-08-03',
day: 'Sunday',
status: 'Present',
},
],
},
{
exam: 'T3',
startDate: '2025-09-10',
endDate: '2025-09-15',
percentage: '66.67%', // 2/3
attendanceRecords: [
{
date: '2025-09-10',
day: 'Wednesday',
status: 'Absent',
},
{
date: '2025-09-11',
day: 'Thursday',
status: 'Present',
},
{
date: '2025-09-12',
day: 'Friday',
status: 'Present',
},
],
},
],
};

