Skip to content

Releases: TEDI-Design-System/react

react-17.1.0-rc.5

14 May 05:08

Choose a tag to compare

react-17.1.0-rc.5 Pre-release
Pre-release

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:

  1. Switch the value type from Dayjs to Date (.toDate() / dayjs(date) is
    the per-call bridge if you still need Dayjs elsewhere).
  2. 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 need useState calls.
  3. Replace shouldDisableX callbacks with the disabledMatchers array
    (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:

  1. Switch the value type from Dayjs | null to Date | undefined (or
    Date[] / DateRange if you adopt the new modes).
  2. Rename the change callbackonChangeonSelect. Its argument is a
    Date | Date[] | DateRange | undefined whose shape matches the active mode.
  3. Restructure the disable knobs — most of the per-day / per-month /
    per-year predicates carry over (with Dayjs → 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 | ...
Read more

react-17.1.0-rc.4

08 May 04:42

Choose a tag to compare

react-17.1.0-rc.4 Pre-release
Pre-release

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:

  1. If you're migrating a Community Table — drop the implicit pagination
    and use the TEDI-Ready Table's built-in pagination prop instead.
  2. If you're using Community Pagination standalone — switch to the TEDI-Ready
    Pagination with explicit page / pageCount / onPageChange props.

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, pageCount from TableContext.
  • The setPageSize mutation through TanStack's table.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 (default 1).
  • siblingCount — pages around the current page (default 1).

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-label like "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

07 May 11:37

Choose a tag to compare

react-17.1.0-rc.3 Pre-release
Pre-release

17.1.0-rc.3 (2026-05-07)

Features

Migration guide: PlaceholderEmptyState

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.ReactNode icon — replaced by the structured
    IconWithoutBackgroundProps shape. Pass a Material icon name or an
    Icon-props object instead.
  • with-background icon variant (background: 'primary' | …) —
    intentionally dropped; the centred-illustration aesthetic of
    EmptyState doesn'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

07 May 07:27

Choose a tag to compare

react-17.1.0-rc.2 Pre-release
Pre-release

17.1.0-rc.2 (2026-05-07)

Features

react-17.1.0-rc.1

30 Apr 05:20

Choose a tag to compare

react-17.1.0-rc.1 Pre-release
Pre-release

17.1.0-rc.1 (2026-04-30)

Features

react-17.0.0

29 Apr 10:32

Choose a tag to compare

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


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

27 Apr 04:58

Choose a tag to compare

react-17.0.0-rc.8 Pre-release
Pre-release

17.0.0-rc.8 (2026-04-27)

Features

react-17.0.0-rc.7

23 Apr 05:13

Choose a tag to compare

react-17.0.0-rc.7 Pre-release
Pre-release

17.0.0-rc.7 (2026-04-23)

Bug Fixes

react-17.0.0-rc.6

15 Apr 11:37

Choose a tag to compare

react-17.0.0-rc.6 Pre-release
Pre-release

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 id is what links a trigger to its panel, the same way
    id worked on TabsItem.

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-labelledby and roving-tabIndex keyboard navigation belong on it,
    not on the wrapper.
  • Tabs.Content without an id always 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 icon prop, disabled)
    without polluting the panel's TabsItem props.

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.

react-17.0.0-rc.5

15 Apr 10:05

Choose a tag to compare

react-17.0.0-rc.5 Pre-release
Pre-release

17.0.0-rc.5 (2026-04-15)

Features