Month and week grid that places records on their dates as chips or milestone diamonds. A chip moves by pointer drag, a keyboard lift or move(), each raising one cancelable suiMove. An agenda view lists a week by day, and a narrow week draws as the agenda, so a phone gets rows it can read.
A grid of six weeks with the weekday heads as column headers and one tab stop. Today sits on the brand wash, weekends on the sunken surface, the neighbouring months quiet. Drag a chip to another day, or focus one and press Space to lift it: every step is announced and one cancelable suiMove fires on the drop.
view="week" draws the anchor's week as seven tall columns with room for every chip. first-day-of-week overrides the locale, and hide-weekends drops the resting days; which days those are also comes from the locale.
marker draws a milestone as a small brand diamond and its title, so the accent stays with the page's one action. A chip dated before today carries a clock and a danger subtitle; overdue on a record overrides the date, so the closed run on 4 Sep wears none. severity paints from the status ladder.
A page that gives the calendar a height shares it over six rows, and a fixed max-per-day then clips a full cell. max-per-day auto shows what fits, with the more link for the rest: two chips at 98px, three from 114px. chip-subtitle auto hides the key under a 162px cell, where a two-word title stops fitting beside it.
chipRender draws the chip content from the record; the calendar keeps the fill, the glyph, the drag and the lift. Its second argument says whether the key belongs in view, by chip-subtitle and the cell width, and whether the chip is in the day list; a key drawn as a chip-sub code takes has-sub as a built-in one does.
<sui-calendar id="cal-custom" aria-label="Launch calendar with priorities" date="2026-09-11" today-date="2026-09-11" max-per-day="2" chip-subtitle="auto"></sui-calendar>
view="agenda" lists the week by day, skipping empty days. A row leads with the severity glyph or a milestone flag and says overdue in red. agendaRender returns parts to draw in place of the built-in ones: here the owner joins the key, the priority sits at the end, and a priority mark fills an empty disc.
// setup(root): properties and events
setup(root) {
const el = root.querySelector('#cal-agenda');
el.events = work();
el.agendaRender = (task) => {
if (task.marker) return undefined;
let mark;
if (!task.severity) {
mark = root.ownerDocument.createElement('i');
mark.className = PRIORITY_ICON[task.priority];
if (task.priority === 'urgent') mark.style.color = 'var(--danger)';
}
return { detail: `${task.subtitle} · ${task.owner}`, meta: PRIORITY_LABEL[task.priority], mark };
};
}
A week on a phone
Seven columns on a phone are too narrow for a title. Below breakpoint (640px by default) of its own width, the week draws as the agenda; view stays week and the grid returns when the calendar widens. breakpoint="0" keeps the week grid at every width. The month keeps its grid.
<div style="max-width:390px"><sui-calendar id="cal-phone" aria-label="This week on a phone" view="week" date="2026-09-11" today-date="2026-09-11" first-day-of-week="1"></sui-calendar></div>
loading draws shimmer rows in the cells. no-drag removes the pointer drag for a touch surface where a long press means something else; the keyboard lift and move() remain, so drag is never the only path. hide-toolbar hands navigation to the page, which calls previous(), today() and next().
suiMove fires once per move from a drop, a keyboard lift or move(), with the record and both days, and is cancelable: here a Shipped task is refused any new date. suiDayClick carries a day, suiEventClick an opened chip, suiDateChange the anchor, suiViewChange the view, and suiOverflow the day behind a more link.
Move WS-101 to 18 SepNothing happened yet.
<div style="display:grid;gap:10px">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<sui-button id="cal-move" variant="outline" size="sm">Move WS-101 to 18 Sep</sui-button>
<code id="cal-log" style="font:460 12px/1.6 var(--font-sans);color:var(--ink-muted)">Nothing happened yet.</code>
</div>
<sui-calendar id="cal-events" aria-label="Launch calendar with a log" date="2026-09-11" today-date="2026-09-11"></sui-calendar>
</div>
// setup(root): properties and events
setup(root) {
const el = root.querySelector('#cal-events');
const log = root.querySelector('#cal-log');
el.events = work();
el.addEventListener('suiMove', (e) => {
const { id, from, to } = e.detail;
const task = el.events.find((t) => t.id === id);
if (task.severity === 'positive') {
e.preventDefault();
log.textContent = `Refused: ${id} has shipped and keeps ${from}.`;
return;
}
log.textContent = `${id} moved from ${from} to ${to}.`;
});
el.addEventListener('suiDayClick', (e) => { log.textContent = `Day ${e.detail.date} clicked.`; });
el.addEventListener('suiEventClick', (e) => { log.textContent = `Opened ${e.detail.id}: ${e.detail.event.title}.`; });
el.addEventListener('suiDateChange', (e) => { log.textContent = `Showing the ${el.view} around ${e.detail}.`; });
el.addEventListener('suiViewChange', (e) => { log.textContent = `Switched to the ${e.detail} view.`; });
el.addEventListener('suiOverflow', (e) => { log.textContent = `${e.detail.events.length} records on ${e.detail.date}; the list opens.`; });
root.querySelector('#cal-move').addEventListener('click', async () => {
const moved = await el.move('WS-101', '2026-09-18');
if (!moved) log.textContent = 'WS-101 is already on 18 Sep.';
});
}
Keyboard
Key
Action
Tab
Steps through the toolbar, then to the one tab stop: the current day cell, chip or more link, or the current agenda row.
ArrowRightArrowLeft
Moves to the next or previous day; past the edge of the grid the month or week turns. With a chip lifted, carries it one day.
ArrowDownArrowUp
Moves down a day: its cell, its chips, its more link, then the same weekday next week. With a chip lifted, carries it one week.
HomeEnd
Moves to the first or last day of the week. With a chip lifted, carries it there.
PageUpPageDown
Moves a month, or a week in the week view; with Shift, a year. With a chip lifted, carries it the same distance.
EnterSpace
On a day cell, raises suiDayClick for that date.
Space
On a chip, lifts it; on a lifted chip, drops it and raises one cancelable suiMove.
Enter
On a chip, opens it (suiEventClick); on a lifted chip, drops it.
Escape
Cancels a lift and returns the chip to its day, or closes the more list.
ArrowDownArrowUpEnterSpaceEscape
In the more list: move between its chips, open one, lift one into the grid, or close the list.
ArrowDownArrowUpHomeEnd
In the agenda: move to the next or previous record, across the day headings, or to the first or last record of the week.
EnterSpace
On an agenda row, opens the record (suiEventClick). The agenda has no lift.
Draws an agenda row's parts (event, row) => { mark?, title?, detail?, meta? }, so a row can carry what a chip has no room for, an owner on the second line or a priority at the end. Each part is a string or an element and takes the row's own place and look; a part left out keeps the built-in one. The row still adds "Milestone" and "overdue" after detail, and keeps its focus, its keys and its click. Return a string or an element instead to draw the whole text column yourself: the row keeps its lead disc and tells a reader the date, the severity and whether the record is overdue. Return nothing to keep the built-in row. row gives the day, whether it is today and whether the record is overdue. It is called on every draw, so return a fresh node each time. A callback that throws keeps the built-in row, and the error is logged once. Property, not attribute.
breakpoint
breakpoint
number
640
Width of the calendar in px under which the week view draws as the agenda, measured by the calendar's own observer, so a calendar in a narrow column switches whatever the screen is. Seven columns there are too narrow for a title. view keeps week, so the grid comes back when the calendar widens, and the view switch offers Month and Agenda only. Zero keeps the week grid at every width. The month view keeps its grid at every width.
Draws a chip's content (event, chip) => string | HTMLElement; strings render as text. The chip keeps its fill, its overdue clock, its drag and its lift; the clock is not a glyph a priority icon would use, so an urgent task that is overdue shows both. Paint a mark you draw, an urgent priority say, in var(--sui-calendar-mark): the danger ink on a plain chip and the chip's own ink on a toned one, which is what the clock takes, so a red mark never sits on a green, gold or navy chip. In the week view the content is the column after the glyph, where a long title wraps. chip says whether the subtitle belongs in view here, by chipSubtitle and the cell width, and whether the chip is in the day list. Mark the subtitle you draw with the class chip-sub and the chip takes has-sub, as a built-in chip does. It is called on every draw, so return a fresh node each time. A callback that throws costs only its chip, which draws the built-in text, and the error is logged once. Property, not attribute.
chipSubtitle
chip-subtitle
"always" | "auto" | "never"
'always'
Whether a chip shows its subtitle. auto drops it from view in a cell narrower than 162px, the width under which a two-word title no longer fits beside a key and the overdue glyph, and keeps it for a reader; never drops it at every width. The overdue glyph stays either way, since it is the chip's one status mark. The day list under a "more" link is wide enough and shows the subtitle unless never. A custom chip is told the decision through chipRender.
date
date
stringstring | undefined
The anchor, ISO YYYY-MM-DD: the month or week on show is the one that holds it. Defaults to today when unset. Mutable; next(), previous(), today() and the keyboard write it and suiDateChange fires. A host write moves the grid silently. A date, not a state, so it does not reflect: read the property.
emptyMessage
empty-message
stringstring | undefined
Text shown under the toolbar when no record falls in the period on show; the grid is still drawn, and the agenda has no rows to draw. The calendar filters nothing, so it carries no emptyFilterMessage.
events
property only
SuiCalendarEvent[]
[]
The records, { id, date, title, subtitle?, severity?, marker?, overdue?, disabled? }. Mutable: a completed move writes the record back with its new date, so a host that keeps its own copy updates it in suiMove. A record whose date does not parse is not drawn. Property, not attribute.
firstDayOfWeek
first-day-of-week
numbernumber | undefined
First column of the grid, 0 Sunday to 6 Saturday. Unset, it comes from locale: Sunday in the United States, Monday in Europe, Saturday in the Gulf.
hideToolbar
hide-toolbar
boolean
false
Hides the calendar's own toolbar (the period, previous, Today, next and the view switch), for a page that draws its own header and drives next(), previous() and today(). The period stays in a polite live region. Reflected.
hideWeekends
hide-weekends
boolean
false
Leaves the weekend columns out of the grid and the weekend's records out of the agenda, for a working-week calendar. Which days rest comes from locale. Reflected.
loading
loading
boolean
false
Draws shimmer rows in the cells instead of the chips, and in the agenda instead of its rows. Reflected, so a page can style the wait.
locale
locale
stringstring | undefined
BCP 47 tag for the weekday heads, the month and week headings, the agenda's day headings, the day names the grid announces, the first day and the weekend. Unset, the page's locale (setLocale() or <html lang>), else en-US; never the browser's, so a screen reads the same on every machine.
maxPerDay
max-per-day
SuiCalendarChipCap"auto" | number
3
Chips a month cell shows before it folds the rest behind a "more" link. auto measures the cell and shows what fits its height, with the link for the rest, so a calendar the page has given a height never clips a chip; until the cell has been measured it shows three. The week view has the height and shows every chip.
noDrag
no-drag
boolean
false
Turns off the pointer drag between days. The keyboard lift and move() remain, so drag is never the only path. The agenda has no drag or lift; move() works in every view. Reflected.
todayDate
today-date
stringstring | undefined
Pins "today" (the marked cell, the Today button and the overdue rule) for demos and tests, e.g. "2026-09-11".
view
view
SuiCalendarView"agenda" | "month" | "week"
'month'
month draws six weeks around the anchor; week draws the anchor's week as seven tall day columns; agenda lists the anchor's week as rows under a heading for each day that holds records, and steps a week at a time as week does. Under breakpoint a week draws as the agenda and keeps its value here. Mutable and reflected; the toolbar switches it and suiViewChange fires.
Events
Event
Detail
React
Vue
Description
suiDateChange
string
onSuiDateChange
@sui-date-change
Fires after the calendar moves its own anchor, from the toolbar, the methods or the keyboard; detail is the new date, ISO. A host write to date does not echo. A page that loads the records on show listens to this and suiViewChange. Does not bubble.
suiDayClick
SuiCalendarDayDetail
onSuiDayClick
@sui-day-click
Fires on a click, Enter or Space on a day cell away from its chips; detail is { date }.
suiEventClick
SuiCalendarEventClickDetail
onSuiEventClick
@sui-event-click
Fires on a click or Enter on a chip that is not lifted, and on a click, Enter or Space on an agenda row; detail is { id, event }.
suiMove
SuiCalendarMoveDetail
onSuiMove
@sui-move
Fires before a record moves, from a drop, a keyboard lift or move() alike; detail is { id, from, to } with both days ISO. Cancelable: preventDefault() refuses the move and the chip stays where it was. Otherwise the calendar writes events and moves the chip.
suiOverflow
SuiCalendarOverflowDetail
onSuiOverflow
@sui-overflow
Fires before the "more" link opens the calendar's own list of a day's records; detail is { date, events }. Cancelable: preventDefault() keeps that list closed so the host can open its own surface.
suiViewChange
SuiCalendarView"agenda" | "month" | "week"
onSuiViewChange
@sui-view-change
Fires after the toolbar switches the view; detail is the new view. A week drawn as the agenda under breakpoint keeps week, so a change of width does not fire it, and neither does a host write. Does not bubble.
Methods
Method
Signature
Description
move()
move(id: SuiKey, to: string) => Promise<boolean>
Moves a record to the day to (ISO or any date string) through the same cancelable suiMove a drop raises. Resolves true when it moved, false when the id or the day is unknown, the record is disabled or already there, or a listener refused it.
next()
next() => Promise<void>
Moves the anchor a month or a week forward, by the view, and fires suiDateChange.
previous()
previous() => Promise<void>
Moves the anchor a month or a week back, by the view, and fires suiDateChange.
setFocus()
setFocus() => Promise<void>
Moves keyboard focus to the grid's tab stop, the current day cell, chip or "more" link, or to the agenda's current row.
today()
today() => Promise<void>
Moves the anchor to today and fires suiDateChange; the tab stop lands on today's cell.
Types
SuiCalendarAgendaContextinterfaceWhat agendaRender is told about the agenda row it is drawing.
date
string
The day the row sits under, ISO YYYY-MM-DD.
today
boolean
True for a row under today's heading.
overdue
boolean
Whether the row counts the record as overdue: its own overdue, else a date before today. A row drawn from parts says "overdue" itself; a row you draw whole should show it.
SuiCalendarAgendaPartsinterfaceThe parts of an agenda row that agendaRender can replace. Each one is a string or an element. A part left out keeps the built-in one; null or '' leaves its place empty.
mark?
SuiRenderResult
Drawn in the lead disc in place of the severity glyph, the milestone flag or the dot. The disc keeps the severity's tint, or the neutral one without a severity.
title?
SuiRenderResult
The first line. Built in: title.
detail?
SuiRenderResult
The second line. Built in: subtitle. The row adds "Milestone" and "overdue" after it.
meta?
SuiRenderResult
Short text at the end of the first line: a priority, a time. Built in: none.
SuiCalendarChipCaptypeHow many chips a month cell shows: a number, or auto to fit the cell's height.
No members.
SuiCalendarChipContextinterfaceWhat chipRender is told about the chip it is drawing.
subtitle
boolean
Whether the subtitle belongs in view here, the decision the built-in chip makes from chipSubtitle and the cell width: true under always, under auto in a cell at least 162px wide, and in the day list unless never. Draw the key when it is true; when it is false leave it out, or keep it for a reader as visually hidden text, as the built-in chip does.
inPanel
boolean
True when the chip is drawn in the day list under a "more" link rather than in its cell. The same record is drawn in both at once, so return a separate node for each.
SuiCalendarDayDetailinterfacesuiDayClick detail: the day, ISO.
date
string
SuiCalendarEventinterfaceOne record on the grid. The default chip draws subtitle then title; marker draws a milestone diamond instead of a chip; an agenda row draws title over subtitle. Any other field is the host's to read in chipRender and agendaRender.
id
SuiKey
Identity, unique in events, kept as given: a numeric id stays a number.
date
string
The day it sits on, as ISO YYYY-MM-DD (any string new Date accepts also works).
title
string
subtitle?
string
Short text before the title: a key, a time, a due phrase. A plain overdue chip paints it in the danger ink. In the agenda it is the row's second line.
severity?
SuiSeverity
Paints the chip from the shared status ladder; without it the chip is a plain surface. In the agenda it picks the row's lead glyph and tint, and a reader hears its name.
marker?
boolean
Draws a small diamond and the title in place of a chip, for a milestone. An agenda row leads with a flag and says "Milestone".
overdue?
boolean
Whether the chip wears the overdue glyph and reads "Overdue" to a reader, with its subtitle in the danger ink; an agenda row says "overdue" on its second line, in the same ink. Unset, a record dated before today is overdue; set it to say so yourself, so a closed record dated last week is not.
disabled?
boolean
Inert: no focus, no drag, no lift, no click. move() refuses it too.
SuiCalendarEventClickDetailinterfacesuiEventClick detail: the record's id and the record.
id
SuiKey
event
SuiCalendarEvent
SuiCalendarMoveDetailinterfacesuiMove detail: one record, the day it leaves and the day it lands on, both ISO.
id
SuiKey
from
string
to
string
SuiCalendarOverflowDetailinterfacesuiOverflow detail: the day and every record on it, in events order.
date
string
events
SuiCalendarEvent[]
SuiCalendarViewtypeThe two grids and the list: a month of six weeks, one week as seven day columns, or the agenda, the same week as a list with a heading for each day that holds records.
No members.
SuiKeytypeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
No members.
SuiRenderResulttype
No members.
SuiSeveritytype
No members.
Theming
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.