Date and time are among the most information-dense elements in any UI. A publication timestamp, a calendar grid, an event badge — each carries structural meaning that CSS alone cannot supply. Getting the CSS right starts with getting the HTML right, and the HTML starts with <time>.
The <time> Element: Semantic Foundation
Before a single line of CSS is written, the <time> element deserves attention. Defined in the HTML Living Standard, <time> wraps a human-readable date or time while its datetime attribute carries a machine-readable equivalent. This matters for search engines, screen readers, and any downstream process that might parse the page.
<time datetime="2025-05-08">May 8, 2025</time>
<time datetime="2025-05-08T09:00:00-05:00">9:00 AM CDT</time>
The datetime value follows ISO 8601 conventions. A date-only value (2025-05-08) is appropriate for publication timestamps. A full datetime with timezone offset (2025-05-08T09:00:00-05:00) is appropriate for events and appointments. Without the datetime attribute, the element’s text content must itself be a valid date string — but in practice, explicitly setting datetime is more reliable and parseable.
Screen readers do not typically announce datetime differently from visible text, but the attribute ensures that assistive technology, browser extensions, and structured data parsers can extract meaningful temporal information. If you are also implementing Schema.org structured data for articles or events, the <time> element’s datetime is the natural companion to datePublished and startDate.
The visual display — the part CSS controls — is entirely the inner text of the element. The datetime attribute is invisible to sighted users; it exists for machines. This separation of concerns is clean and worth preserving.
Calendar Grid Layouts with CSS Grid
A css calendar display date layout is, structurally, a seven-column grid. CSS Grid makes this both obvious and concise.
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background-color: var(--color-border);
}
.calendar-cell {
background-color: var(--color-surface);
padding: 0.5rem;
min-height: 5rem;
}
The background-color on the wrapper combined with gap: 1px produces hairline borders between cells without requiring border declarations on each cell — a technique that avoids the common double-border problem.
Placing the First Day of the Month
The structural challenge with a calendar grid is that months do not start on Sunday (or Monday, depending on locale). The first cell of the month must be offset. CSS Grid handles this directly via grid-column-start:
/* If the month starts on Wednesday (index 3, zero-based from Sunday) */
.calendar-cell:first-child {
grid-column-start: 4;
}
In practice, your framework or templating layer calculates this offset and applies it as an inline style or a utility class. The CSS itself stays simple — the grid engine does the placement work. What matters for the CSS pattern is that no table-based or float-based hacks are needed; the grid model was designed for exactly this kind of explicit placement.
Day Headers
Week-day labels (Sun, Mon, Tue…) belong in the same seven-column grid, either as a separate header row or as the first seven cells in a single continuous grid:
<div class="calendar-grid" role="grid" aria-label="May 2025">
<div class="calendar-header" role="columnheader" aria-label="Sunday">Su</div>
<div class="calendar-header" role="columnheader" aria-label="Monday">Mo</div>
<!-- ... -->
<div class="calendar-cell" role="gridcell">1</div>
<!-- ... -->
</div>
The role="grid" pattern with columnheader and gridcell satisfies the ARIA Grid Pattern requirements. Keyboard navigation within the grid — arrow keys moving between cells — requires JavaScript; the ARIA roles communicate intent, but the behavior must be implemented.
Today, Selected, and Range States
Three visual states appear in nearly every calendar component:
.calendar-cell[aria-current="date"] {
background-color: var(--color-today-bg);
font-weight: 600;
}
.calendar-cell[aria-selected="true"] {
background-color: var(--color-selected);
color: var(--color-selected-text);
}
/* Date range: cells between start and end */
.calendar-cell.in-range {
background-color: var(--color-range-bg);
border-radius: 0;
}
.calendar-cell.range-start {
border-radius: 50% 0 0 50%;
}
.calendar-cell.range-end {
border-radius: 0 50% 50% 0;
}
Using aria-current="date" for today and aria-selected="true" for the user’s selection connects visual styling directly to accessibility semantics — a good practice recommended in WCAG 2.2. The range-selection pattern using partial border-radius on start and end cells with a flat fill for interior cells is a common UI pattern seen in date pickers for travel booking and scheduling applications.
Date Badge Patterns
A date badge — the stacked day-of-month and abbreviated month display common in event listings and article cards — is a distinct pattern from a calendar grid. It condenses date information into a compact visual unit.
<time class="date-badge" datetime="2025-05-08">
<span class="date-badge__day">08</span>
<span class="date-badge__month">MAY</span>
</time>
.date-badge {
display: flex;
flex-direction: column;
align-items: center;
width: 3.5rem;
border: 1px solid var(--color-border);
border-radius: 4px;
overflow: hidden;
font-variant-numeric: tabular-nums;
}
.date-badge__month {
background-color: var(--color-accent);
color: var(--color-accent-text);
font-size: 0.625rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.25rem 0;
width: 100%;
text-align: center;
}
.date-badge__day {
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
padding: 0.25rem 0;
color: var(--color-text-primary);
}
The month strip at top with a contrasting background is the most legible arrangement — it groups the label with the number rather than placing it below, where it competes with surrounding content. The letter-spacing on the uppercase month abbreviation compensates for the optical tightening that uppercase letterforms create at small sizes.
One subtle detail: leading zeros on the day (08 rather than 8) maintain consistent badge width when numbers change. If your data layer provides single-digit days without padding, a CSS content trick cannot help here — zero-padding is a formatting concern, not a styling one. Apply it in your template.
Typography for Date and Time Display
Dates and times often appear in tabular contexts — columns of timestamps in a data table, event schedules, log output. When numbers shift between one and two digits (or between 9 and 10, 12 and 1), columns jump unless you use tabular figures.
.timestamp,
.event-time,
.calendar-cell {
font-variant-numeric: tabular-nums;
}
font-variant-numeric: tabular-nums instructs the font to use fixed-width number glyphs where available. Most system fonts and the majority of well-crafted web fonts support this feature. For a broader look at which OpenType features apply in which contexts, the web fonts loading guide covers the rendering pipeline from font-face declaration to painted glyphs.
For time displays specifically, the colon separator in HH:MM or HH:MM:SS can visually jump on fonts that apply optical size adjustments to punctuation. Using font-feature-settings: "tnum" 1 alongside tabular-nums provides a more explicit fallback for fonts that expose the feature through the low-level setting but not the higher-level keyword.
Size and Weight Hierarchy
In a calendar cell, the day number is primary information; the month and year are context. In an event card, the time may be more important than the date. CSS custom properties make it easy to establish a consistent date typography scale:
:root {
--date-primary-size: 1.25rem;
--date-primary-weight: 600;
--date-secondary-size: 0.75rem;
--date-secondary-weight: 400;
--date-secondary-color: var(--color-text-muted);
}
.event-date .day {
font-size: var(--date-primary-size);
font-weight: var(--date-primary-weight);
}
.event-date .month-year {
font-size: var(--date-secondary-size);
font-weight: var(--date-secondary-weight);
color: var(--date-secondary-color);
}
Consistent scale tokens prevent the ad-hoc size decisions that accumulate across a component library and make date displays feel typographically inconsistent.
Internationalization and Directionality
Calendars are not universal. The Gregorian calendar is the default assumption in most Western UI frameworks, but many users operate in contexts where the Hebrew, Hijri, Persian, or other calendar systems are primary. CSS cannot change calendar logic — that is entirely in the data layer — but CSS must accommodate the visual consequences of internationalization choices.
Right-to-Left Calendar Grids
A right-to-left (RTL) calendar reverses the column order: Saturday appears at the left, Friday at the right (for week-starting-Saturday locales common in the Middle East), or Sunday appears at the right (for some RTL locales that retain Sunday-start). CSS Logical Properties handle directional layout without requiring separate RTL stylesheets:
.calendar-cell {
padding-inline-start: 0.5rem;
padding-inline-end: 0.5rem;
}
For grid column placement in RTL, the dir="rtl" attribute on a parent element reverses the inline axis for Grid and Flexbox. A grid laid out with repeat(7, 1fr) will automatically present columns right-to-left when dir="rtl" is set — no separate grid definition is needed. Test this behavior in your target browsers; support for logical properties in grid contexts has been solid across evergreen browsers since 2022, but edge cases appear when mixing explicit grid-column placements with directionality.
Compact vs. Verbose Date Formats
Different locales format dates differently: 05/08/2025 means May 8 in the US and August 5 in most of Europe. The Intl.DateTimeFormat API in JavaScript produces locale-appropriate formatted strings; CSS then displays whatever string is provided. The implication for CSS design is that date strings vary significantly in character count across locales. A German Donnerstag, 8. Mai 2025 is considerably longer than an English Thu, May 8. Build date display containers that accommodate variable-length text without breaking layout — avoid fixed widths on date labels, and test with verbose locale strings, not just English abbreviations.
Timestamp Display in Data Tables and Feeds
Activity feeds, notification lists, and audit logs present timestamps at high density. Two patterns appear frequently:
Relative timestamps (“3 hours ago”, “yesterday”) are dynamically generated by JavaScript and change over time. The <time> element’s datetime attribute holds the absolute ISO value; the visible text is the relative form. CSS for relative timestamps is typically minimal — they integrate into running text or list items without special layout treatment.
Absolute timestamps in table columns benefit from a fixed-width monospace or tabular rendering so that times align vertically:
.log-timestamp {
font-variant-numeric: tabular-nums;
white-space: nowrap;
color: var(--color-text-muted);
font-size: 0.875rem;
}
white-space: nowrap prevents a timestamp from wrapping across two lines, which would break the scannable column structure. At narrow viewports, the table itself may need horizontal scrolling — wrapping the table in an overflow-x: auto container is the standard solution for responsive data tables.
Accessible Color Contrast for Date States
Calendar cells cycle through multiple visual states — default, today, selected, disabled, in-range — and each state must meet WCAG 2.2 success criterion 1.4.3 for text contrast (4.5:1 for normal text, 3:1 for large text). The disabled state is a common failure point: muted gray text on a white background often falls below 3:1.
.calendar-cell[aria-disabled="true"] {
color: var(--color-text-disabled); /* Ensure this meets 3:1 minimum */
cursor: not-allowed;
pointer-events: none;
}
Using aria-disabled="true" rather than the disabled attribute on non-form elements keeps the cell in the accessibility tree — screen reader users can still navigate to it and understand that the date is unavailable — while pointer-events: none and cursor: not-allowed provide the visual and interaction feedback for pointer users.
When choosing accent colors for today’s highlight or selection states, verify contrast in both light and dark color schemes. If your calendar supports prefers-color-scheme, test all states under both schemes. Dark-mode calendar selections frequently fail contrast checks when a design team tests only the light theme.
Putting the Patterns Together
A production calendar component combines all of these concerns: <time> elements with datetime attributes for each cell, a seven-column CSS Grid with explicit placement for the month’s start offset, tabular numeric typography, ARIA roles for the grid structure, state-based styling tied to aria-current and aria-selected, and logical properties for directional support.
The JavaScript in a date picker handles data — computing offsets, generating ISO strings, managing selection state, formatting locale strings. The CSS handles presentation — layout, hierarchy, states, directionality. Keeping that boundary clear makes both layers easier to test, maintain, and extend. For deeper coverage of how semantic HTML underpins component accessibility patterns, the HTML semantics article addresses the structural layer that sits beneath everything covered here.



