Releases: TEDI-Design-System/react
react-17.1.0-rc.5
17.1.0-rc.5 (2026-05-14)
Features
Calendar migration guide — Community → TEDI-Ready
The legacy Community Calendar wraps MUI X CalendarPicker and operates on
Dayjs values (Dayjs | null). The TEDI-Ready Calendar is built on
react-day-picker and operates on plain JavaScript Date objects (with
the option of Date[] or DateRange for the multi/range modes).
Migrating means three things at once:
- Switch the value type from
DayjstoDate(.toDate()/dayjs(date)is
the per-call bridge if you still need Dayjs elsewhere). - Bring your own state for
currentMonth+view. TEDI-Ready Calendar is
strictly controlled — there's no internal "open month" tracking. The
Community Calendar managed both internally; you'll needuseStatecalls. - Replace
shouldDisableXcallbacks with thedisabledMatchersarray
(react-day-picker's matcher format).
If you only need a date input (i.e. you were using Community Calendar
embedded in a form), the right migration target is DateField, not
Calendar. See the DateField migration guide.
1. Update the import
// Before
import { Calendar, type CalendarValue } from '@tedi-design-system/react/community';
// After
import { Calendar } from '@tedi-design-system/react';
import type { DateRange } from 'react-day-picker';CalendarValue (Dayjs | null) is gone — use Date | undefined for single
mode, Date[] for multiple, DateRange for range.
2. The mandatory state plumbing
Where Community Calendar held the visible month and the active view
internally, TEDI-Ready Calendar requires them as props:
const [view, setView] = useState<'days' | 'months' | 'years'>('days');
const [currentMonth, setCurrentMonth] = useState(new Date());
const [date, setDate] = useState<Date | undefined>();
<Calendar
mode="single"
value={date}
handleSelect={(value) => setDate(value as Date | undefined)}
applyValue={setDate} // commits month / year picks
view={view}
setView={setView}
currentMonth={currentMonth}
setCurrentMonth={setCurrentMonth}
/>;| Community | TEDI-Ready |
|---|---|
| none (internal) | currentMonth: Date + setCurrentMonth: (date) => void |
| none (internal) | view?: 'days' | 'months' | 'years' + setView |
onChange: (Dayjs | null) => void |
handleSelect: OnSelectHandler<...> + applyValue: (date) => void |
handleSelect fires when a day is clicked. applyValue fires when a
month / year cell commits a selection via selectionLevel.
3. Prop name mapping
| Community prop | TEDI-Ready prop | Notes |
|---|---|---|
value: Dayjs | null |
value: Date | Date[] | DateRange | undefined |
Shape matches the active mode |
defaultValue |
none (controlled-only) | Track the seed in your own useState |
onChange |
handleSelect + applyValue |
See above |
disabled |
disabledMatchers={[() => true]} (or omit the cell to hide) |
Community's disabled greyed the whole picker — pass a function matcher that returns true everywhere, or render conditionally |
disableFuture |
disabledMatchers={[{ after: new Date() }]} |
|
disablePast |
disabledMatchers={[{ before: new Date() }]} |
|
minDate: Dayjs |
disabledMatchers={[{ before: minDate.toDate() }]} |
|
maxDate: Dayjs |
disabledMatchers={[{ after: maxDate.toDate() }]} |
|
shouldDisableDate?: (day) => boolean |
disabledMatchers={[(date) => fn(date)]} |
Receives a JS Date, not Dayjs |
shouldDisableMonth / shouldDisableYear |
Same matcher array — react-day-picker evaluates per-day, so accept a Date and check getMonth() / getFullYear() |
|
view: 'day' | 'month' | 'year' |
view: 'days' | 'months' | 'years' |
Note the plural rename |
views: Array<...> |
none — controlled via view + selectionLevel |
TEDI-Ready uses one current view; selectionLevel decides the commit-level |
showDaysOutsideCurrentMonth |
showOutsideDays |
Default flipped to true (was false) |
disableHighlightToday |
none | The "today" highlight is always on in TEDI-Ready; suppress via CSS if needed |
loading |
none | Render your own skeleton above / instead of <Calendar> |
readOnly |
none | Use disabledMatchers={[() => true]} to block clicks |
shouldHighlightDate |
availableDays?: Date[] | ((date) => boolean) |
Marks days as highlighted-but-still-selectable |
shouldShowStatusOnDate |
none | Compose via a custom day cell or render an overlay marker outside the Calendar |
Brand-new in TEDI-Ready
mode: 'single' \| 'multiple' \| 'range'— Community Calendar was single-only.unavailableDays?: Date[] \| ((date) => boolean)— overlay for "blocked" days that aren't fully disabled.selectionLevel: 'days' \| 'months' \| 'years'— coarser commit level:'years'selects Jan 1 of the picked year and closes,'months'selects day 1 of the picked month.monthYearSelectType: 'dropdown' \| 'grid'— switches the header pickers between a<Select>dropdown (default) and a full grid.footer: ReactNode— slot below the calendar grid for action buttons.locale/localeCode— react-day-picker locale; defaults to Estonian (et/et-EE).
4. Worked example: single-date calendar with a constraint
Before — Community
import { Calendar } from '@tedi-design-system/react/community';
import dayjs, { Dayjs } from 'dayjs';
const [date, setDate] = useState<Dayjs | null>(dayjs());
<Calendar
value={date}
onChange={setDate}
disableFuture
shouldDisableDate={(day) => day?.day() === 0} // disable Sundays
view="day"
views={['year', 'month', 'day']}
/>;After — TEDI-Ready
import { Calendar } from '@tedi-design-system/react';
const [view, setView] = useState<'days' | 'months' | 'years'>('days');
const [currentMonth, setCurrentMonth] = useState(new Date());
const [date, setDate] = useState<Date | undefined>(new Date());
<Calendar
mode="single"
value={date}
handleSelect={(value) => setDate(value as Date | undefined)}
applyValue={setDate}
view={view}
setView={setView}
currentMonth={currentMonth}
setCurrentMonth={setCurrentMonth}
disabledMatchers={[
{ after: new Date() }, // disableFuture
(d) => d.getDay() === 0, // disable Sundays
]}
/>;5. Worked example: status / highlighted dates
Before — Community
<Calendar
value={date}
onChange={setDate}
shouldHighlightDate={(d) => availableDays.includes(d?.format('YYYY-MM-DD'))}
shouldShowStatusOnDate={(d) => booked.includes(d?.format('YYYY-MM-DD')) ? 'error' : undefined}
/>After — TEDI-Ready
<Calendar
mode="single"
value={date}
handleSelect={(value) => setDate(value as Date | undefined)}
applyValue={setDate}
view={view}
setView={setView}
currentMonth={currentMonth}
setCurrentMonth={setCurrentMonth}
availableDays={availableDates} // highlights but allows selection
unavailableDays={(d) => booked.has(d.toDateString())} // marks visually; still in tree
disabledMatchers={[(d) => booked.has(d.toDateString())]} // ...and blocks selection
/>There's no direct mapping for shouldShowStatusOnDate's three-way status
('error' \| 'success' \| 'inactive') — TEDI-Ready only models a binary
"available vs unavailable" overlay. For richer day-cell decoration, render
custom content alongside the Calendar rather than inside its cell.
DateField migration guide — Community DatePicker → TEDI-Ready DateField
The legacy Community DatePicker wraps MUI X DatePicker and operates on
Dayjs values (Dayjs | null). The TEDI-Ready DateField is built on
react-day-picker and operates on plain JavaScript Date objects, with
optional Date[] or DateRange shapes for the new 'multiple' / 'range'
modes.
Migrating means three things:
- Switch the value type from
Dayjs | nulltoDate | undefined(or
Date[]/DateRangeif you adopt the new modes). - Rename the change callback —
onChange→onSelect. Its argument is a
Date | Date[] | DateRange | undefinedwhose shape matches the activemode. - Restructure the disable knobs — most of the per-day / per-month /
per-year predicates carry over (withDayjs → Date), but a few flags are
gone and replaced by matcher functions.
1. Update the import
// Before
import { DatePicker, type DatepickerValue } from '@tedi-design-system/react/community';
// After
import { DateField } from '@tedi-design-system/react';
import type { DateRange } from 'react-day-picker';The DatepickerValue (Dayjs | null) alias is gone. Use Date | undefined for
the default single-mode case.
2. Required props
The single mandatory change is the rename of two props:
| Community | TEDI-Ready |
|---|---|
id (via TextFieldProps) |
id (required) |
label (via TextFieldProps) |
label (required) |
value (Dayjs | null) |
selected (`Date | Date[] | DateRange | ... |
react-17.1.0-rc.4
17.1.0-rc.4 (2026-05-08)
Features
Pagination migration guide — Community → TEDI-Ready
The legacy Community Pagination ships inside @tedi-design-system/react/community
as an internal child of Table (community/components/table/components/pagination/)
and is automatically rendered for any Community Table. The new TEDI-Ready
Pagination is a standalone, prop-driven component exported from
@tedi-design-system/react with no implicit Table coupling.
This guide covers both migration paths:
- If you're migrating a Community
Table— drop the implicit pagination
and use the TEDI-ReadyTable's built-inpaginationprop instead. - If you're using Community Pagination standalone — switch to the TEDI-Ready
Paginationwith explicitpage/pageCount/onPageChangeprops.
1. API at a glance
Community Pagination
import Pagination from '@tedi-design-system/react/community/.../pagination';
// Only public prop — everything else is read from <TableContext>.
interface PaginationProps {
totalRows: number;
}The component hard-codes:
- Page-size options:
[5, 10, 20, 50, 100]. - Read of
pageIndex,pageSize,pageCountfromTableContext. - The
setPageSizemutation through TanStack'stable.setPageSize(...). - The results count label via
getLabel('pagination.results', totalRows).
You can't use it without a Community Table providing the context.
TEDI-Ready Pagination
import { Pagination } from '@tedi-design-system/react';
export interface PaginationProps {
pageCount: number; // required
page?: number; // controlled current page (1-based)
defaultPage?: number; // uncontrolled (1-based)
onPageChange?: (page: number) => void;
totalItems?: number; // shown as "X results"
pageSize?: number; // controls the page-size select
pageSizeOptions?: number[]; // omit to hide the select
onPageSizeChange?: (pageSize: number) => void;
boundaryCount?: number; // pages near the edges (default 1)
siblingCount?: number; // pages around current (default 1)
labels?: Partial<PaginationLabels>; // override aria-labels / format strings
className?: string;
}Plus full forwardRef<HTMLDivElement> support and a built-in mobile (< md)
collapse to a single page-jump <Select>.
2. Path A — migrating a Community Table (recommended)
This is the case for nearly every consumer: you weren't really using "Community
Pagination" so much as "the pagination Community Table ships with you". The
clean fix is to switch to TEDI-Ready Table, which has its own pagination
footer built in.
Before
import { Table } from '@tedi-design-system/react/community';
<Table
data={rows}
columns={columns}
totalRows={rows.length}
// Pagination renders automatically unless hidden:
// hidePagination={true}
/>;After
import { Table } from '@tedi-design-system/react';
<Table
data={rows}
columns={columns}
pagination // built-in pagination footer
// or fine-grained:
// pagination={{ pageSize: 10, pageSizeOptions: [10, 25, 50] }}
/>;| Community | TEDI-Ready Table |
|---|---|
| Pagination is implicit | pagination prop on Table (true / false / options object) |
hidePagination |
omit pagination, or pass false |
totalRows={...} |
rowCount={total} (only needed for manualPagination) |
defaultPagination={{ pageIndex, pageSize }} |
defaultState={{ pagination: { pageIndex, pageSize } }} |
Fixed [5, 10, 20, 50, 100] page sizes |
pagination={{ pageSize, pageSizeOptions }} — pass your own |
pageSize: number change → setPageSize via TanStack |
Lives in state.pagination; control via state / onStateChange |
Controlled state — Community vs TEDI-Ready
// Community
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
<Table
data={page.rows}
columns={columns}
pagination={pagination}
onPaginationChange={setPagination}
totalRows={total}
manualPagination
/>;
// TEDI-Ready
const [state, setState] = useState<TableState>({ pagination: { pageIndex: 0, pageSize: 10 } });
<Table
data={page.rows}
columns={columns}
state={state}
onStateChange={(next) => setState((prev) => ({ ...prev, ...next }))}
rowCount={total}
pageCount={Math.ceil(total / (state.pagination?.pageSize ?? 10))}
manualPagination
pagination={{ pageSize: 10, pageSizeOptions: [10, 25, 50] }}
/>;Note: manualPagination is now an explicit boolean. The Community Table
auto-derived it from the presence of pagination state. TEDI-Ready requires
the flag so it doesn't silently re-slice your already-paginated server data.
3. Path B — using Pagination standalone (e.g. above a list, gallery, etc.)
If you imported the Community Pagination directly (the rare case — it depended
on TableContext), you'll need to rewrite the call site.
Before — relied on TableContext
// Inside a <Table> tree:
<Pagination totalRows={total} />After — fully prop-driven
import { Pagination } from '@tedi-design-system/react';
const [page, setPage] = useState(1);
<Pagination
page={page}
pageCount={Math.ceil(total / pageSize)}
onPageChange={setPage}
totalItems={total}
pageSize={pageSize}
pageSizeOptions={[10, 25, 50]}
onPageSizeChange={setPageSize}
/>;For uncontrolled use (e.g. a small list where you just want the chrome):
<Pagination pageCount={20} defaultPage={1} />Prop mapping for standalone use
| Community (read from context) | TEDI-Ready prop |
|---|---|
getState().pagination.pageIndex |
controlled page (1-based!) or defaultPage |
getPageCount() |
pageCount |
getState().pagination.pageSize |
pageSize |
setPageSize(value) |
onPageSizeChange={(size) => setPageSize(size)} |
props.totalRows |
totalItems |
Hard-coded [5, 10, 20, 50, 100] |
pageSizeOptions — your own array |
pageIndex is 0-based, page is 1-based. When porting controlled state,
convert: page = pageIndex + 1, setPage(p) → setPageIndex(p - 1).
4. What's new in TEDI-Ready Pagination
These have no Community equivalent.
Configurable page-window
<Pagination pageCount={50} page={25} boundaryCount={2} siblingCount={2} />boundaryCount— pages always rendered at the start and end (default1).siblingCount— pages around the current page (default1).
Mobile collapse (< md)
Below the md breakpoint the numbered list collapses to a single page-jump
<Select> with the prev/next arrows on either side. No work for the consumer
— same component, breakpoint-driven.
Label overrides
Every visible string and aria-label is overridable via the labels prop:
<Pagination
pageCount={5}
labels={{
ariaLabel: 'Search results',
previous: 'Previous',
next: 'Next',
results: (count) => `Showing ${count} hits`,
pageStatus: (page, total) => `Result page ${page} of ${total}`,
}}
/>labels.pageStatus is the new screen-reader announcement string — TEDI-Ready
Pagination renders a visually-hidden role="status" aria-live="polite" region
that updates with the current page on every navigation, so SR users hear the
page change. There's no Community equivalent.
forwardRef
const ref = useRef<HTMLDivElement>(null);
<Pagination ref={ref} pageCount={5} />;Accessibility upgrades
- Every page button has a generated
aria-labellike "Go to page 3" /
"Current page, page 4" (labels.pageAriaLabel/currentPageAriaLabel). - The current page carries
aria-current="page". - Arrow buttons are hidden (not just disabled) when at the start/end, so SR
users don't tab to inert controls. - Live-region announcement (
pageStatus) on every navigation. - Mobile page-jump uses an accessible Select.
react-17.1.0-rc.3
17.1.0-rc.3 (2026-05-07)
Features
Migration guide: Placeholder → EmptyState
The community Placeholder (@tedi-design-system/react/community) has been
re-implemented as the TEDI-Ready EmptyState
(@tedi-design-system/react/tedi). The new component is built directly,
without wrapping <Card> / <Row> / <Col>, has a tighter prop surface,
and adds first-class support for headings and CTA actions.
Import change
- import { Placeholder } from '@tedi-design-system/react/community';
+ import { EmptyState } from '@tedi-design-system/react';Prop mapping
Placeholder (community) |
EmptyState (TEDI-Ready) |
Notes |
|---|---|---|
children |
children |
Same. Body text describing why there is nothing to show. |
icon |
icon |
Default still 'spa'. Type narrowed — see Icon prop changes below. |
className |
className |
Same. |
cardProps |
(removed — use type + size) |
The wrapping <Card> is no longer exposed. Use type for border/radius variants and size for padding scale. |
isNested |
type="inside" |
Replaces the boolean shortcut for nested usage (no border, no radius, sits inside a parent <Card> / <Table>). |
rowProps |
(removed) | The internal layout is fixed. Customise via className if absolutely necessary. |
| — | type (new) |
'separate' (default) / 'attached' / 'inside'. |
| — | size (new) |
'default' (24 px padding) / 'small' (16 px padding). |
| — | heading (new) |
Optional H3 above the body text in brand-primary color. |
| — | actions (new) |
Slot below the text for CTA <Button> (or two) / <Link>. |
Icon prop changes
Placeholder.icon accepted string | IconProps | React.ReactNode.
EmptyState.icon is narrower: string | IconWithoutBackgroundProps | null.
| Was | Now |
|---|---|
icon="event_busy" |
icon="event_busy" (unchanged) |
icon={{ name: 'inbox', color: 'danger' }} |
icon={{ name: 'inbox', color: 'danger' }} (unchanged — same shape minus background) |
icon={{ name: 'inbox', background: 'primary' }} |
Not supported. Drop the background field. |
icon={<MyCustomSvg />} (raw JSX) |
Not supported. Use the icon object shape with a Material icon name. |
| Hide the icon (no native option) | icon={null} |
If you previously relied on a raw SVG/React.ReactNode, replace it with a
Material icon name or a structured IconWithoutBackgroundProps object. If
you genuinely need a custom illustration, render it outside EmptyState
(or pass it as heading / children).
Layout changes
Placeholder always wrapped its content in a <Card> (with optional
borderless, padding, and background variants via cardProps).
EmptyState no longer wraps — it renders its own minimal container with
type + size controlling the surface.
Old Placeholder setup |
New EmptyState equivalent |
|---|---|
| Default (full-bordered card) | type="separate" (default) |
cardProps={{ borderless: true }} or isNested |
type="inside" |
cardProps={{ padding: 0 }} |
type="inside" (or size="small" if you only wanted tighter padding) |
cardProps={{ padding: 1 }} (small layout default) |
size="small" |
| Sitting flush under a preceding card / table | type="attached" |
Examples
Basic empty state
- <Placeholder>You have no data to display</Placeholder>
+ <EmptyState>You have no data to display</EmptyState>With a custom Material icon
- <Placeholder icon="event_busy">No appointments yet</Placeholder>
+ <EmptyState icon="event_busy">No appointments yet</EmptyState>Nested inside a <Card> / <Table>
- <Card>
- <CardContent>
- <Placeholder isNested>No rows to show</Placeholder>
- </CardContent>
- </Card>
+ <Card>
+ <CardContent>
+ <EmptyState type="inside">No rows to show</EmptyState>
+ </CardContent>
+ </Card>Adding a heading and CTA (new in EmptyState)
<EmptyState
icon="add_box"
heading="Nothing here yet"
actions={
<Button type="button" iconLeft="add">
Create new
</Button>
}
>
Get started by creating your first item.
</EmptyState>There was no first-class way to do this with Placeholder; consumers
typically composed it inside their own layout next to a <Heading> and
<Button>. With EmptyState, both the heading and the actions row are
built in.
Hiding the icon
- // Placeholder always rendered an icon — no native way to hide it
- <Placeholder>Plain text with no glyph</Placeholder>
+ <EmptyState icon={null}>Plain text with no glyph</EmptyState>What goes away
cardProps,isNested,rowProps— no replacement;
EmptyState's container styling is owned by the component.- Raw
React.ReactNodeicon — replaced by the structured
IconWithoutBackgroundPropsshape. Pass a Material icon name or an
Icon-props object instead. with-backgroundicon variant (background: 'primary' | …) —
intentionally dropped; the centred-illustration aesthetic of
EmptyStatedoesn't pair with the chip-style icon background.
What's new
heading— optional H3 above the body text in brand color.actions— CTA slot below the text (typically<Button>/
<Link>).type='attached'— top-border-less variant for sitting flush
beneath a preceding card or table.size='small'— tighter padding (16 px instead of 24 px).
react-17.1.0-rc.2
react-17.1.0-rc.1
react-17.0.0
17.0.0 (2026-04-29)
Bug Fixes
- checkbox: invalid indicator hover border fix #605 (#609) (f1d62c6)
- select: select placeholder no longer blocks context menu interactions #584 (#585) (e8d86ab)
- variables: update core version, update variable names #592 (#598) (1f15b36)
Features
- button-group: add mobile variant #448 (#606) (54dee90), closes #94 #94 #94 #94 #94 #94 #94 #94 #94 #94 #94 #94 #94 #94
- card: add more control to borderRadius usage, add examples #444 (#597) (deac9db)
- print: introduce PrintingProvider + context-based usePrint #99 (#497) (a58cb70)
- spinner: add new sizes #586 (#589) (fbea0c3)
- tabs: new tedi-ready component #555 (#557) (9c06c51)
- textarea: add autoGrow, height and maxHeight props #588 (#593) (2c86740)
- toggle: new TEDI-Ready component #305 (#594) (6f28045)
Deprecations
The following are now deprecated and targeted for removal in October 2026.
| Deprecated | Replacement | Since | Migration guide |
|---|---|---|---|
Toggle (community) |
Toggle (TEDI) |
17.0.0-rc.4 | 17.0.0-rc.4 |
Tabs (community) |
Tabs (TEDI) |
17.0.0-rc.6 | 17.0.0-rc.6 |
BREAKING CHANGES
PrintProvider changes (since 17.0.0-rc.1, Migration guide)
react-17.0.0-rc.8
react-17.0.0-rc.7
17.0.0-rc.7 (2026-04-23)
Bug Fixes
react-17.0.0-rc.6
17.0.0-rc.6 (2026-04-15)
Features
MIGRATION GUIDE
Migration from Community Tabs → TEDI Tabs
The TEDI-Ready Tabs replaces the community Tabs / TabsItem pair with a compound layout that mirrors the WAI-ARIA tabs pattern more closely:
Tabs.List (the tablist), Tabs.Trigger (each tab button), and
Tabs.Content (each panel). The community version remains in the package but is @deprecated and should not be used in new code.
Imports
- import { Tabs, TabsItem } from '@tedi-design-system/react/community';
+ import { Tabs } from '@tedi-design-system/react/tedi';TabsItem no longer exists. The closest equivalents are Tabs.Trigger (the clickable tab) and Tabs.Content (the panel content) — see below.
Prop renames
Community (<Tabs>) |
TEDI (<Tabs>) |
Notes |
|---|---|---|
currentTab |
value |
Controlled active tab id. |
defaultCurrentTab |
defaultValue |
Uncontrolled initial tab id. |
onTabChange |
onChange |
Fires with the new tab id. |
aria-labelledby (on <Tabs>) |
aria-labelledby (on Tabs.List) |
The label moves down to the tablist itself. |
hideNavOnPrint |
hideOnPrint (on Tabs.List) |
Same idea, but scoped to the nav bar element. |
Structure
Before (community)
<Tabs
defaultCurrentTab="overview"
onTabChange={(id) => console.log(id)}
aria-labelledby="my-heading"
>
<TabsItem id="overview" label="Overview">
<Overview />
</TabsItem>
<TabsItem id="details" label="Details">
<Details />
</TabsItem>
<TabsItem id="history" label="History" disabled>
<History />
</TabsItem>
</Tabs>After (TEDI)
<Tabs defaultValue="overview" onChange={(id) => console.log(id)}>
<Tabs.List aria-labelledby="my-heading">
<Tabs.Trigger id="overview">Overview</Tabs.Trigger>
<Tabs.Trigger id="details">Details</Tabs.Trigger>
<Tabs.Trigger id="history" disabled>
History
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content id="overview">
<Overview />
</Tabs.Content>
<Tabs.Content id="details">
<Details />
</Tabs.Content>
<Tabs.Content id="history">
<History />
</Tabs.Content>
</Tabs>Conceptual mapping:
<TabsItem id label>...</TabsItem>splits into two elements:<Tabs.Trigger id>{label}</Tabs.Trigger>— the tab button itself<Tabs.Content id>...</Tabs.Content>— the panel body
- The shared
idis what links a trigger to its panel, the same way
idworked onTabsItem.
Why two elements per tab?
Splitting trigger from content is the standard ARIA pattern and unlocks a few capabilities the community component couldn't express cleanly:
- The tablist is now a real DOM container (
role="tablist") so
aria-labelledbyand roving-tabIndexkeyboard navigation belong on it,
not on the wrapper. Tabs.Contentwithout anidalways renders its children — useful when
the panel is, say, a<Routes>outlet and the active tab is determined
by the URL rather than by tab id (see Usage with React Router).- Triggers can carry their own visuals (icons via
iconprop,disabled)
without polluting the panel'sTabsItemprops.
Card padding / background
Community TabsItem accepted padding / background (forwarded to an internal Card). The TEDI equivalent does not wrap content in a Card; do that explicitly if you still need the chrome:
- <TabsItem id="overview" label="Overview" padding={2} background="white">
- <Overview />
- </TabsItem>
+ <Tabs.Trigger id="overview">Overview</Tabs.Trigger>
+ ...
+ <Tabs.Content id="overview">
+ <Card padding={2} background="white">
+ <CardContent>
+ <Overview />
+ </CardContent>
+ </Card>
+ </Tabs.Content>This keeps the layout decision in your code rather than baked into the tabs primitive — most apps don't actually want the Card wrapper.
Print behaviour
- <Tabs hideNavOnPrint="hide" ...>
- <TabsItem ...>...</TabsItem>
- </Tabs>
+ <Tabs ...>
+ <Tabs.List hideOnPrint="hide" aria-labelledby="...">
+ ...
+ </Tabs.List>
+ <Tabs.Content ...>...</Tabs.Content>
+ </Tabs>Controlled mode
- <Tabs currentTab={tab} onTabChange={setTab} aria-labelledby="h">
- <TabsItem id="a" label="A">…</TabsItem>
- </Tabs>
+ <Tabs value={tab} onChange={setTab}>
+ <Tabs.List aria-labelledby="h">
+ <Tabs.Trigger id="a">A</Tabs.Trigger>
+ </Tabs.List>
+ <Tabs.Content id="a">…</Tabs.Content>
+ </Tabs>Tabs.Content without an id always renders, so router-driven tabs (where the active tab is the current route) work cleanly — see Usage with React Router for the full pattern.