Lesson 7: Styling
Using data attributes to style the calendar with Tailwind or plain CSS.
Lesson 7 of 8: style every part of the calendar with data attributes — no CSS imports, no specificity wars.
The headless approach
@gentleduck/calendar ships zero CSS. It outputs data-* attributes on every element so you can hook styles in.
That means:
- No CSS imports to manage.
- No specificity wars.
- Full control over every pixel.
- Works with any CSS approach (Tailwind, CSS modules, styled-components, plain CSS).
Data attributes on day cells
Every day cell gets these attributes via getDayProps:
| Attribute | When set |
|---|---|
data-calendar-day | Always present on day cells |
data-selected="true" | Day is selected |
data-today="true" | Day is today |
data-disabled="true" | Day is disabled |
data-outside-month="true" | Day belongs to adjacent month |
data-hidden="true" | Day should be hidden (showOutsideDays is false) |
data-range-start="true" | First day in range |
data-range-middle="true" | Day is within range |
data-range-end="true" | Last day in range |
data-focused="true" | Day has keyboard focus |
data-weekend="true" | Day is Saturday or Sunday |
Tailwind example
function StyledCalendar() {
const { state, getDayProps, getGridProps } = useCalendar({
adapter,
mode: 'range',
})
return (
<div
{...getGridProps()}
className="grid grid-cols-7 gap-1"
>
{state.weeks.map(week =>
week.days.map(day => (
<button
key={day.date.getTime()}
{...getDayProps(day)}
className={[
'h-9 w-9 rounded-md text-sm',
'data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground',
'data-[today=true]:bg-accent data-[today=true]:text-accent-foreground',
'data-[disabled=true]:opacity-50 data-[disabled=true]:cursor-not-allowed',
'data-[outside-month=true]:text-muted-foreground',
'data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary',
'data-[range-middle=true]:bg-accent',
'data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary',
'data-[focused=true]:ring-2 data-[focused=true]:ring-ring',
].join(' ')}
>
{day.date.getDate()}
</button>
))
)}
</div>
)
}function StyledCalendar() {
const { state, getDayProps, getGridProps } = useCalendar({
adapter,
mode: 'range',
})
return (
<div
{...getGridProps()}
className="grid grid-cols-7 gap-1"
>
{state.weeks.map(week =>
week.days.map(day => (
<button
key={day.date.getTime()}
{...getDayProps(day)}
className={[
'h-9 w-9 rounded-md text-sm',
'data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground',
'data-[today=true]:bg-accent data-[today=true]:text-accent-foreground',
'data-[disabled=true]:opacity-50 data-[disabled=true]:cursor-not-allowed',
'data-[outside-month=true]:text-muted-foreground',
'data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary',
'data-[range-middle=true]:bg-accent',
'data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary',
'data-[focused=true]:ring-2 data-[focused=true]:ring-ring',
].join(' ')}
>
{day.date.getDate()}
</button>
))
)}
</div>
)
}CSS stylesheet example
[data-selected="true"] {
background: var(--color-primary);
color: white;
border-radius: 4px;
}
[data-today="true"] {
border: 2px solid var(--color-primary);
}
[data-disabled="true"] {
opacity: 0.4;
pointer-events: none;
}
[data-range-start="true"] {
border-radius: 4px 0 0 4px;
background: var(--color-primary);
}
[data-range-middle="true"] {
background: var(--color-accent);
border-radius: 0;
}
[data-range-end="true"] {
border-radius: 0 4px 4px 0;
background: var(--color-primary);
}[data-selected="true"] {
background: var(--color-primary);
color: white;
border-radius: 4px;
}
[data-today="true"] {
border: 2px solid var(--color-primary);
}
[data-disabled="true"] {
opacity: 0.4;
pointer-events: none;
}
[data-range-start="true"] {
border-radius: 4px 0 0 4px;
background: var(--color-primary);
}
[data-range-middle="true"] {
background: var(--color-accent);
border-radius: 0;
}
[data-range-end="true"] {
border-radius: 0 4px 4px 0;
background: var(--color-primary);
}Slot attributes (compound components)
@gentleduck/primitives/calendar adds a data-slot attribute to each component, so you can style by semantic role:
[data-slot="calendar"] { /* root */ }
[data-slot="calendar-header"] { /* month/year title */ }
[data-slot="calendar-nav"] { /* nav wrapper */ }
[data-slot="calendar-nav-button"] { /* prev/next buttons */ }
[data-slot="calendar-grid"] { /* day grid */ }
[data-slot="calendar-weekdays"] { /* weekday header row */ }
[data-slot="calendar-day"] { /* individual day cell */ }[data-slot="calendar"] { /* root */ }
[data-slot="calendar-header"] { /* month/year title */ }
[data-slot="calendar-nav"] { /* nav wrapper */ }
[data-slot="calendar-nav-button"] { /* prev/next buttons */ }
[data-slot="calendar-grid"] { /* day grid */ }
[data-slot="calendar-weekdays"] { /* weekday header row */ }
[data-slot="calendar-day"] { /* individual day cell */ }Next: Lesson 8 - Performance