Releases
v2.1.1
Maintenance release. Three fixes; no API changes.
Fixed
-
A scrolling
Tabsrow no longer scrolls the page - bringing the selected tab into view now scrolls the row itself, along its own axis. It used to scroll the whole page to the row as well, so a page with a scrollingTabsin it opened scrolled down to it. -
A
Menuthat is open on mount no longer scrolls the page - the focus it puts on its first row (or the checked one) when it opens is set without scrolling, so a page with a menu open by default, or opened from code, opens at its top and not scrolled down to the menu. -
A
Sidebarrail keeps its own width - itslayout(inlineorstacked) was also read as the page layout grid, which gave the railwidth: 100%and inline padding from the first breakpoint up. A stacked rail in a wide window took all the room and left its panel a sliver; it is nowstackedWidthwide, and an inline oneexpandedWidthorcollapsedWidth, at every size.
v2.1.0
Tooltip takes a disabled control, a description that holds while it is closed, and a point to be
placed at, all asked for by the design studio when it moved off the native title.
Added
-
TooltipisDescriptionPersistent- withshouldDescribeChild, the child is described the whole time, as by a nativetitle, not only while the tooltip is open: a visually hidden copy of thetitleis rendered beside the child foraria-describedbyto point at. It is also a theme default (theme.components.Tooltip.defaultProps). docs -
TooltipanchorPosition- a point in the window (client coordinates, as Popover's) to place the tooltip at, for a hint that depends on where the pointer is.childrenis optional then, andisOpenopens it; it keeps its gap from the point and flips for want of room. docs -
TooltipisInteractive- withfalsethe tooltip takes no pointer events, so a click goes to what is under it, and it closes as the pointer leaves the child: for a text hint that holds nothing to use. The default (true) keeps it hoverable (WCAG 1.4.13). Also a theme default. docs -
Tooltiphands what it is handed to its child - a parent that clones its child (aMenuwith aTooltipfor its trigger) now reaches the button: theref,id,aria-*and handlers go on to it, the child's own handler and the handed one both run, and a handed description is added to. A child hidden from assistive technology is not named by the title.shouldWrapDisabledChild(defaulttrue, also a theme default) leaves out the wrapper of a disabled child that still takes the pointer. -
TooltipGroup- tooltips inside it open with no enter delay while another is open, or withinskipDelayDuration(300 ms) of the last one closing, so a toolbar reads icon to icon; the one still showing closes at once. Tooltips outside a group, touch and controlled tooltips (isOpen) are unchanged.skipDelayDurationintheme.components.Tooltip.defaultPropsdoes the same for every tooltip under a theme, with no wrapper (unset or 0 is off; aTooltipGroupwins over it). docs -
ContextMenuopens from code -isOpenandanchorPosition(client coordinates, as Popover's) open it at a point whatever was right-clicked, withtargetoptional; it asks to close throughonClose, moves as the point does, and gives the focus back to where it was. -
MenuandContextMenuslotProps.list-data-*, class andaria-*for therole="menu"list, and for every submenu's; anonKeyDownruns before the list's own. Itsrole,idand name stay the menu's.
Fixed
- A press of a
Tooltip's child closes it - as a nativetitlegoes away, so an open tooltip is no longer left over what the pointer goes on to click. A touch is left to its long press. Tooltipover a disabled child - a disabled control gets no pointer events from React, so the tooltip never opened. It now wraps a disabled child in an inline-gridspanand listens and is placed on that; the child keeps its ownref.- No native
titleinTabsandMenuItem- the close and overflow buttons ofTabsshow their labels, and aMenuItem'snote, as non-interactive tooltips; the note still describes the row, open or closed, and opens over a disabled row.
v2.0.0
The theme holds the page's layout system - the window size classes as breakpoints, and the layout tokens per class - so an app and the design studio work from the same values. Everything responsive is CSS: pages work with JavaScript off. (#344)
Action menus - a trigger and the list of commands it opens, with its own keyboard, and MenuItem
as a menu row (#11); tabs, a row of
tabs with content of its own around it, as a tablist or a navigation
(#15); and a component catalog for
tools, shipped with the package (#21).
Inputs - a rating and a number field first (#31).
Breadcrumbs, the first of the navigation components
(#29).
Added
- More spacing steps -
theme.spacegains0.125(1px),0.25(2px),0.75(6px),1.25(10px) and1.75(14px), andspacing()rounds to whole pixels with a 1px floor for any non-zero step (every default step is exact on the 8px unit).0.125and0.25are for hairline gaps; lines below 4px are border widths. docs theme.breakpoints- each key a range, from its value until the next:xs0-599px (compact, the base),sm600-839 (medium),md840-1199 (expanded),lg1200-1599 (large),xl1600+ (extra-large). Responsive props use them by name (p={{ xs: 2, md: 4 }}), by position (p={[2, 3, 4]}) or as container queries (cqSm).BreakpointOverridesadds, renames or removes breakpoints with type safety. docs- Query helpers -
theme.breakpoints.up/down/only/between/notandtheme.breakpoints.containerQueries, rebuilt from the theme's own sizes bycreateThemeandThemeProvider; plainup,down,only,between,notandcontainerQueriesexports forbaseTheme, andwithBreakpointHelpersfor a scale of your own. theme.layout- layout tokens per window class:content(columns 1-16, margin and gutter astheme.spacekeys, fluid or fixed withmaxWidth),panels(start/end, eachinline,overlay,collapsedorhiddenwithwidthandrailWidth) andbars(a 64px top bar on every class).baseThemeuses 4 / 8 / 12 / 12 / 12 columns, the start panel as a 72px rail fromsmand inline atxl, the end panel as an overlay atmdandlgand inline atxl.layoutprop - on anyView-based component,Flexincluded, it is the page frame: the theme's margin and, in fixed mode, its centred max width per class - the element keeps its own display (<View as="article" layout>,<Flex layout gap={3}>). OnGridit adds the column tracks. The grid's owngap/columnGapis its gutter - single or responsive - and the theme's gutter applies where it is not set.layout={{ lg: { columns: 16 } }}replaces the theme's columns fromlgup,layout={{ maxWidth: '1280px' }}its max width at every class, and the two mix. A frame spans its parent in any context (a grid item, a flex column or row), capped by its max width in fixed mode, rather than shrinking to its content.DrawerToggle- the button that opens and closes aDrawerthroughpopovertarget.LayoutProvider- a page-level layout on the./themesubpath:<LayoutProvider layout={{ lg: { content: { maxWidth: '1376px' } } }}>merges the page's values ontotheme.layoutfor everything inside, field by field and mobile-first, and changes nothing else - colours and light/dark switching stay the surrounding theme's. Providers nest.@soroush.tech/design-system/testing-installPopover(), a Popover API stand-in for jsdom unit tests: show, hide, thetoggleevent, Escape andpopovertargetclicks. It returns a function that removes it, and does nothing where the real API exists.spanonView- how many columns of a grid an element takes (span={{ xs: 4, md: 6 }}), orspan="full"for the whole row whatever the column count; everyView-based component takes it.Menu- a trigger and theMenuListit opens. Not modal: nothing is laid over the page and focus isn't trapped. The list is placedposition: fixedunder the trigger (alignstart or end, mirrored right to left), flips above it near the bottom of the window, staysmarginThresholdinside it and follows the trigger on scroll and resize - so a panel's overflow can't clip it - while staying inside the menu's own box in the DOM. ArrowDown/ArrowUp on the trigger open it; Escape, a chosen row, the trigger again, or focus leaving close it (onClose(event, reason)), focus going back to the trigger unless it left. Uncontrolled (defaultOpen) or controlled (isOpen,onOpen), withonStep(-1 | 1)for a menu bar;emptyLabelfor a menu with no rows;elevationandlayer. (#13) docsMenuList- arole="menu"list that owns the keyboard: the arrow keys, Home and End move focus between its own rows, skipping disabled ones and wrapping (shouldWrap); typing jumps to a row by its label.shouldAutoFocusfocuses the checked row, or the first enabled one.onSelect(value)reports the chosen row;color,textColor,sizeanddenseset every row's defaults. Rows are found by their nearestrole="menu", so a nested list keeps its own. (#12) docsMenuItemmenu rows - inside aMenuList, aMenuItemis amenuitem,menuitemradioormenuitemcheckbox(role), withselectedasaria-checked, a leadingstartIconcolumn kept on every row, a trailingshortcut(alsoaria-keyshortcuts), anotethat tells why a disabled row is off,secondaryAction- a control of its own beside it, the pointer's, hidden from assistive technology - andonClick. The pointer moves focus as it goes, so one row is highlighted at a time. Inside aSelectit is the sameoptionrow as before. docs- Submenus -
submenuon aMenuItemopens a nestedMenuListbeside its row, to any depth;nullor[]is an empty one, which showsemptyLabel. The row and its submenu sit side by side in arole="none"entry, the rowaria-haspopup="menu"witharia-expandedand a chevron that mirrors right to left. Focus decides the submenu: a focused row - by key or by the pointer - has it open, a sibling taking focus closes it. Right, Enter or Space go in; Left or Escape come back (swapped right to left). A choice in a submenu is the outer list'sonSelectand closes the wholeMenu. Placed beside the row on the side away from the menu'salignedge, or the other side when the window has no room - its own submenus then go on that way, never back over the menu; with room on neither side, under the row (or over it), never covering it. (#14) docs ContextMenu- the same list opened by a right-click over itstarget, at the pointer, or by the context-menu key / Shift+F10 at the focused element; placed and closed asMenuis, with focus going back to where it was. The browser's own menu is held back only when this one opens. (#20) docs- Menus from data -
MenuItemsrenders rows fromMenuItemData[](rows,{ type: 'divider' }, submenus as nested entries), andMenuandContextMenutake the same array asitems. (#20) docs MenuDivider- arole="separator"line between groups of rows. docs- One subpath for the menus -
Menu,MenuList,MenuDivider,MenuItemsandContextMenuare one family, imported from@soroush.tech/design-system/Menu, asTable's parts are. It also exportsMenuListContextanduseMenuListContext- present inside aMenuList, the rows' look and how a row reports its choice - and theMenuItemValueandMenuItemDatatypes. - Theme customization -
theme.components.Menu(root,paper),.ContextMenu(root,paper- its surface alone, overMenu's),.MenuListand.MenuDivider, andMenuItem's newstartIcon,label,shortcut,submenuIcon,entryandsecondaryActionslots.defaultPropsforMenu(align,marginThreshold,elevation,layer,borderRadiusand the rows' look),ContextMenu,MenuList(shouldWrapand the rows' look) andMenuItem- a list's own values win. Tabs,Tab- a row of tabs. Atablistby default: one tab in the page's tab order, the arrow keys of itsorientationmoving between tabs (skipping disabled ones, coming round, swapped right to left), Home/End, Enter or Space selecting - or moving focus selecting, withshouldSelectOnFocus.as="nav"gives the same look as a navigation of buttons or links, markedaria-current="page".value/defaultValue/onChange,nullfor none; a tab without a value takes its position.variantunderline(a sliding indicator),boxed(outlined on a raised ground),enclosed(outlined on its top and sides, open onto the divider) ortonal(the tabs in a rounded container, the selected one a tinted pill) - an augmentableTabsVariants;color,textColor,size,fullWidth,isCentered;hasDivider, a line the row's whole length - round the selected tab when enclosed, which without it is outlined all round.startContent/endContenthold the row's own components outside the arrow keys, a space from the tabs. ATabtakeslabel, aniconplaced byiconPosition(topby default) at the size it is given - every label in a row on one line -isWrapped,disabled,hrefandas.action.updateIndicator()re-measures. (#16) docs ·Tab- Closable tabs - a
Tabwith acloseLabelhas a ✕ of its own beside it, out of the tab order, shown on the selected tab and under the pointer and keeping its room on the others; it, a middle click and Delete fireTabs'onTabClose(value). The ✕ is the pointer's - a tablist owns tabs alone, so it is hidden from assistive technology - and the tab announces Delete (aria-keyshortcuts).secondaryActionputs a control of the tab's own after its label. The selected outline and raised ground are drawn around the whole tab, its controls included. (#17) - Scrolling tabs -
overflowBehavior="scroll"scrolls the list along its orientation when its tabs don't fit, withscrollButtonsauto(each shown while tabs are out of view its way),always(disabled at an end) ornever; hidden on narrow screens unlessshouldShowScrollButtonsOnMobile, the scrollbar hidden unlesshasVisibleScrollbar. A button scrolls a view's worth (smoothly, unless reduced motion is preferred), the selected tab is scrolled fully into view, and right to left everything is mirrored.action.updateOverflow(). The buttons areTabScrollButtons - exported, and replaced withslots={{ scrollButton }}. (#19) docs - Tabs that don't fit go to a menu -
overflowBehavior="menu"draws the tabs that fit whole, in order - the selected one, or the first enabled one, always - and lists the rest in aMenubehind a button at the list's end, named byoverflowLabel. Nothing scrolls and no tab is cut by the edge. Choosing a row selects its tab, which brings it into the row; a closable tab's row has its ✕ beside it, and Delete on the row closes it too. Measured after every render and whenever the list resizes, down a column too; prerendered, every tab is drawn.action.updateOverflow()measures again. (#18) docs TabContext,TabList,TabPanel- aTabContextholds the value (value/defaultValue/onChange) for aTabList- theTabsthat shows it, each tab linked to its panel - and theTabPanels beside it, wherever they sit; aTabPanelis arole="tabpanel"named by its tab, shown while it is selected (shouldKeepMountedkeeps hidden content mounted,bgits ground).TabContext·TabList·TabPanel- One subpath for the tabs -
Tabs,Tab,TabContext,TabList,TabPanelandTabScrollButtonare imported from@soroush.tech/design-system/Tabs. - Grounds -
bgcolors the tab strip andselectedBgthe selected tab (paperby default in the boxed look, and in the enclosed look without a divider - which draws no border then); give theTabPanelthe samebgand tab and panel read as one. ATabtakes its ownbgwhile unselected, andborderRadius- only the corners away from the divider when enclosed; aTabPaneltakesborderRadiustoo. - Theme customization -
theme.components.Tabs(root,list,indicator,startContent,endContent,scrollButton,overflowButton,overflowClose, withdefaultPropsforvariant/color/size/hasDivider/selectedBg),.Tab(root,label,icon,secondaryAction,close, withdefaultPropsforisWrapped/bg/borderRadius/color/textColor/size/selectedBg),.TabScrollButton(defaultPropsforcolor/textColor) and.TabPanel(defaultPropsforshouldKeepMounted). - A tab's own look - like a table cell, a
Tabtakes its row'scolor,textColor,sizeandselectedBg, and its own value wins: the tab's prop, then the row's, thentheme.components.Tab.defaultProps, then the row's default. The underline indicator, the enclosed divider under every other tab and the overflow button take the selected tab's color. ATabScrollButtontakescolor(its stretch of the divider) andtextColor(its chevron) the same way. A tab'ssizesets its type and its sides; across a row every tab takes the row's tallest top and bottom padding, so a smaller tab keeps its label on the others' line. Breadcrumbs- the trail of links from a site's root to the current page: anavlandmark named byaria-label, its crumbs - the app'sLinks andTypography- an ordered list with anaria-hiddenseparatorbetween two (/by default; an icon separator mirrors right to left). PastmaxItems(8) only the firstitemsBeforeCollapseand lastitemsAfterCollapsecrumbs show, with a button named byexpandLabelthat shows the whole trail and moves focus to its first link;slots.collapsedIconandslotProps.collapsedIconreplace its icon. The root is aTypography:color(secondaryby default),typography- its variant -align,as.theme.components.Breadcrumbsstylesroot,list,item,separator,collapsedandcollapsedIcon, withdefaultPropsforseparator,maxItems,itemsBeforeCollapse,itemsAfterCollapse,expandLabel- a trail's language - andcolor.Icongainsmore_horiz. (#30) docsRating- a row of icons the user sets as a rating, or a read-only display of one. A group of visually hidden native radios, so the keys (Tab to the rating, the arrows to change it), assistive technology and forms work as the platform's.value/defaultValue/onChange(nullwhen cleared),max,precision(0.5for halves - the rating is rounded to it),highlightSelectedOnly,icon/emptyIcon,size,color,readOnly(onerole="img"),disabled,required,error,name. Hovering previews the value (onChangeActive), pressing the chosen item clears it. Every label is the app's:getLabelTextnames each item andemptyLabelTextthe "no rating" radio. In aFormControlit takes the field's size, color, error, disabled and required state, and itsFormLabelnames the group.theme.components.Ratingstylesroot,item,icon,decimal,input,textandemptyLabel, withdefaultPropsformax,precision,highlightSelectedOnly,sizeandcolor.Icongainsstarandstar_border;FormLabelregisters with itsFormControl(labelIdfromuseFormControl), the wayFormHelperTextdoes. (#32) docsNumberInput- a field for a number. A text input - so what is typed is the user's until it is settled - that reads and writes the number in its locale (locale,format- anIntl.NumberFormat, a percent read as its fraction), clamps it tomin/max, and steps by the arrow keys (step; AltsmallStep; ShiftlargeStep), Home/End, the wheel (shouldScrubOnWheel) and buttons that repeat while held (spinButtons:stacked,sidesornone).value/defaultValue/onChange(as typed) /onChangeCommitted(on blur, Enter or a step),shouldSnapOnStep,variant,size,color,error,disabled,readOnly,required,fullWidth,name(a hidden input carries the raw number to a form). Arole="spinbutton"; the buttons' names (incrementLabel,decrementLabel) are the app's. In aFormControlit takes the field's size, color, error, disabled and required state, and itsFormLabeltargets the field.theme.components.NumberInputstylesroot,input,spinButtons,incrementanddecrement, withdefaultPropsfor the frame, the steps and the two labels.Icongainsaddandremove. (#33) docsRangeInput- a slider: one value on a range, or a thumb for each value of an array. Each thumb is a visually hidden native<input type="range">, so the arrows, Page, Home and End, assistive technology and forms work as the platform's; the pointer drags a thumb, touch included.value/defaultValue/onChange(event, value, activeThumb)/onChangeCommitted,min,max,step(nulllimits the value tomarks),shiftStep,marks,orientation,track('normal','inverted'orfalse),scale,valueLabelandformatValueLabel,shouldSwapThumbs,size,color,error,disabled,name. Every name is the app's:getAriaLabelfor each thumb andgetAriaValueTextfor its value. In aFormControlit takes the field's size, color, error and disabled state, and itsFormLabeltargets and names the thumbs.theme.components.RangeInputstylesroot,rail,track,thumb,mark,markLabel,valueLabelandinput. (#34) docsTransferList- two lists, and the buttons that move the checked items from one to the other. Data-driven:items({ value, label, disabled? }),value/defaultValue(the values on the right) andonChange. Each list is arole="list"of rows whose checkbox is the row - one tab stop, named by its label; move right and left act on the checked items and are off while none is checked,shouldShowMoveAlladds the buttons that move every enabled item, andhasSelectAlladds a header checkbox - all, some or none - with the count (getCountLabel). A disabled item stays where it is. Every name is the app's:leftLabel,rightLabeland the four buttons' labels. In aFormControlit takes the field's size, color, error and disabled state, and itsFormLabelnames the group.theme.components.TransferListstylesroot,list,header,title,count,items,item,controlsandicon, withdefaultPropsfor the buttons' labels, the move-all and select-all switches,sizeandcolor. (#35) docsCollapse,Fade,Grow,SlideandZoom- show and hide content by moving it. Each takesisShown,shouldAppear,shouldMountOnEnter,shouldUnmountOnExit,timeout(a number or{ enter, exit }),easingand the lifecycle callbacks (onEnter...onExited, each told the node), over one shareduseTransition.Fade,Grow,Slide(withdirection, mirrored right to left, measured from where the child sits) andZoomwrap one child that holds arefand takes astyle; out, it is hidden.Collapseopens by the height - or,orientation="horizontal", the width - of its content, measured as it moves and left to the content once open, with acollapsedSize. Time and easing come from the newtheme.transitions(duration:short,standard,long;easing:standard,enter,exit); a user who asked for less motion sees it all change at once, and the callbacks still fire.theme.components.Collapsestylesroot,wrapperandinner, withdefaultPropsfor its orientation, collapsed size, time, easing andshouldAppear. (#41) docsAccordion- a heading that opens and closes a region:AccordionSummary(a button in a heading -headingLevel,expandIcon),AccordionDetails,AccordionActionsandAccordionGroup. The summary carriesaria-expandedandaria-controls; the rest is arole="region"named by it, opened and closed byCollapseor thetransitiongiven. Open or closed it owns, untilisExpandedtakes it over (onChange(event, isExpanded));defaultExpanded,disabledandhasGutters. In anAccordionGroupthe arrows, Home and End move between summaries (one that is off is skipped),isExclusivekeeps one open at a time andvariant="attached"joins them into one block with no gap.theme.componentsstylesAccordion(root,region),AccordionSummary(root,button,content,icon) and therootof the rest, withdefaultPropsforhasGutters,headingLevel,isExclusiveandvariant. (#42) docsChip- a compact tag:label(truncated when long),variant(contained,outlinedortext),color,size,shape(square,roundedorpill, the default) with aborderRadiusfor theroundedshape, aniconor anavatar.onClickmakes it a button - one tab stop;onDeleteshows a delete control, named bydeleteLabel, that is a button of its own and out of the tab order, and deletes on Backspace and Delete on the chip too (a chip that only deletes takes the focus for them). A filled chip is the theme'scontrastTexton the color'smain.theme.components.Chipstylesroot,body,label,icon,avataranddeleteIcon, withdefaultPropsforvariant,color,size,shape,borderRadiusanddeleteLabel. (#43) docsBadge- a count or a dot over another element:badgeContent,max(a count past it showsmax+),variant(standardordot),color,isHidden,shouldShowZero(a count of zero shows no badge otherwise),anchorOrigin(the corner;endis the left right to left) andoverlap(rectangularorcircular). The badge isaria-hidden;badgeLabelis the text a screen reader hears, read with the child and, over one element, added to itsaria-describedby.theme.components.Badgestylesroot,badgeandlabel, withdefaultPropsformax,variant,color,anchorOriginandoverlap. (#44) docsAlert(withAlertTitle) - an inline message with aseverity(success,info,warningorerror- picks the icon and the palette color), avariant(standardtinted,containedoroutlined), anicon(falsedraws none), anactionand, givenonClose, a close control named bycloseLabel.roleisalert(announced at once) orstatus(politely). Tinted and outlined, its text is the theme'stext.initial, checked against AA for each severity; filled, it is the severity'scontrastText.Icongainscheck_circle,errorandinfo.theme.components.Alertstylesroot,icon,message,actionandcloseButton;AlertTitleitsroot; withdefaultPropsforseverity,variant,closeLabelandrole. (#45) docsFab- a floating action button: the one main action of a page,circular(its icon) orextended(a pill with a label and astartIcon). It is aButton- its focus ring, press state and disabled behavior - raised on a theme shadow, as tall as an avatar of itssize, in thecolorgiven. Where it floats is the page's: no position is built in. A circular fab is named byaria-labeloraria-labelledby. A fab keeps a touch target of at leasttheme.space[6]square, moves its shadow withtheme.transitions(not at all under reduced motion) and is no higher pressed than at rest; margin and padding aretheme.spacekeys that win over its own, and an extended one hasspace[2]before its icon,space[3]after its label,space[1.5]between, and a one-line label cut with an ellipsis.isCollapseddraws an extended fab as a circle - its label moving out overtheme.transitions- and back, the label staying in the tree.theme.components.Fabstylesroot,labelandlabelText, withdefaultPropsforvariant,colorandsize. (#46) docsSnackbar- a transient message in a corner of the window, through aPortal:isOpen/defaultOpen,messageandaction(orchildren, such as anAlert),anchorOrigin(startandendswap right to left), atransition(Growby default) and alayer- the newsnackbarkey oftheme.zOrder, beside the newtooltipone. It hides itself afterautoHideDuration; the clock stops while the pointer or the focus is on it and starts again forresumeHideDuration.onClose(event, reason)is toldtimeout,escapeKeyDownorclickaway; controlled, the owner closes it, uncontrolled it closes itself - but not on a click away. Arole="status"that never takes the focus.theme.components.Snackbarstylesroot,content,messageandaction, withdefaultPropsforanchorOrigin, the two times andlayer. (#47) docsFabMenu,FabMenuItem- aFabthat opens a stack of actions from the corner of a page. The trigger is a circular or - with alabel- extended fab, a disclosure button (aria-expanded,aria-controls) whoseiconturns to acloseIconwhile open; the actions areFabMenuItems, extended pills (asecondaryFabof the trigger's size, aniconand a label) that zoom in overtheme.transitions, the one next to the trigger first, and are out of the tree while closed.isOpen/defaultOpenwithonOpenandonClose,direction(up,down,leftorright- a column or a row, the sides the page's start and end),align(startorend, for a column),colorandsize. Escape, a press away or an action chosen closes it, the focus returning to the trigger for the first and the last; the arrows of the axis it goes along (ArrowUp and ArrowDown, or ArrowLeft and ArrowRight) open a closed trigger and walk what is drawn, Home and End go to its ends. The actions are measured against the window as it opens and as the window or the page moves: a row wider than the room beside the trigger becomes a column (above it where there is more room, else below), a column is no wider than its room - a long label is cut with an ellipsis - and no taller, scrolling instead.theme.components.FabMenustylesrootandlist, withdefaultPropsfordirection,align,colorandsize. AFabis also thetriggerof aMenu. docsTooltip- a short description of one child, shown on hover, keyboard focus and a long touch: arole="tooltip"in aPortal, placed beside its child (placement: a side -top,bottom,left,right, orautofor the side with the most room - and-startor-end;hasArrowpoints at the child's middle) and flipped for want of room, by theuseAnchoredPlacementMenushares.enterDelay,leaveDelayand the touch delays; it stays open while the pointer is on the tooltip, closes on Escape and never takes the focus. A texttitlenames the child (aria-label);shouldDescribeChilddescribes it (aria-describedby) instead.isOpen/defaultOpenwithonOpenandonClose,shouldOpenOnHover,shouldOpenOnFocus,shouldOpenOnTouch,layer, andslotsandslotPropsfor its surface and arrow.theme.components.Tooltipstylesrootandarrow, withdefaultPropsfor each prop but the title andisOpen. (#49) docsAutocomplete- a field whose text finds an option in a list: arole="combobox"input with arole="listbox"in aPortalbelow it, placed byuseAnchoredPlacementand kept as wide as the field, witharia-activedescendanton the highlighted option. One choice, or several withmultipleasChips (Backspace in the empty field removes one);shouldAllowFreeTexttakes a typed text as the value.optionsof any type withgetOptionLabel,isOptionEqualToValue,filterOptions,groupByandrenderOption;value/defaultValuewithonChangeandinputValuewithonInputChange, each with its reason;isLoadingandloadingText,noOptionsText,clearLabel,openLabel,closeLabel- no built-in English. Arrow keys wrap, Home and End walk the list while it is walked, Enter chooses, Escape closes. In aFormControlit takes itsid, size, color, error, disabled and helper text.theme.components.Autocompletestyles twelve slots, withdefaultPropsfor the frame, the labels,shouldAllowFreeTextandlayer. (#50) docs- Surfaces take the theme's corners and colors -
Snackbaralso takes ashape(square,rounded,pill), withborderRadiussetting aroundedone.Accordion,Alert,Snackbar,Tooltipand theAutocompletelist are square by default, asPaperis, and round with the theme'sdefaults.surfaceRadiusor aborderRadiusof their own.Accordion,Snackbar,Tooltipand theAutocompletelist take abg, andAccordion,Snackbar,TooltipandAutocompleteatextColor;Autocompletetakes aborderRadiuson its field too. Each is adefaultPropskey. ScrollArea- a scrolling area whose scrollbars are drawn over its content rather than beside it, so a rounded panel keeps its corner clean, and show only while the pointer is over it or focus is inside. Scrolling stays native (wheel, keys, touch); each axis that overflows (axes:y,xorboth) gets a track whose thumb drags and whose track pages. AmaxHeightcaps one that grows with its content;tabIndex={0}with anaria-labelmakes a scroller with nothing focusable keyboard scrollable. New theme tokensbackground.scrollbarandbackground.scrollbarTrack(required in a full theme;createThemeinherits them), the track and thumb sized fromtheme.spaceand the fade fromtheme.transitions.theme.components.ScrollAreastylesroot,viewport,trackandthumb, withdefaultPropsforaxes. docsMenuandContextMenutake aborderRadius- a key oftheme.radii, by default the theme'sdefaults.surfaceRadius(square), so a menu is as round as the other surfaces; adefaultPropskey.- For the studio's swaps -
ScrollAreatakes aviewportRef;MenuItemtakes anaria-keyshortcutsof its own, which wins over a stringshortcut, andshouldAnnounceSecondaryAction;TabstakesshouldAnnounceClose, which names each closable tab's close button (and the overflow menu's) by itscloseLabelfor assistive technology - a sibling of the tab, out of the tab order;NumberInputtakesstartContentandendContentinside its frame (a newcontentslot) and reads12pxand12 pxas 12. - Inputs share a least width and one focus ring -
TextInput,NativeSelect,Select,NumberInputandAutocompleteare at leasttheme.defaults.inputMinWidthwide (default12rem) unless they fill their row, and aminWidthof the instance wins. Their keyboard focus ring is the border's color: the error color while in error, else the field'scolor-TextInputandSelectused the theme's default color. AFormControlis a column - label, control, helper text - whose children keep their own width unless it isfullWidth; aFormLabelis a block above its control withspace[0.5]under it and aFormHelperTextthe same above it. The arrow ofSelectandNativeSelect, and the chevrons ofAutocomplete, are the field's icon size (theme.icon[size]) in the field's color, so they followcoloranderror; the stacked arrows ofNumberInputare that size too and share the field's height. ACheckbox,Radio,SwitchandTransferListtake atextColor- from the field or their own - for their label text (a newlabelslot on the three), the mark keeping its color. ATextInputis one character wide, as every input is, so it is as wide as the others, not twenty characters. - Inputs follow their form -
TextInput,NumberInputandNativeSelectset their value in their accentcolorwhen they have notextColor, asSelectdoes, so aForm'scolorchanges the text too.Switchtakes itssize,coloranddisabledstate from itsForm, and from itsFormControlthose too, with itsid,requiredand helper text; its neutral label istext.secondary.Selectis named by itsFormLabel, and aFormLabelwith anidof its own is the one a control points at. @soroush.tech/design-system/catalog- every component as data, for tools that place and write components (a design tool's palette, a page's source writer): its category in the docs, the subpath it is imported from, what a freshly placed one is and its named looks (as{ component, props, children }nodes), the props a picker offers with their theme scale or values and default, how its node props are written, its two-way prop and event, and the DOM events worth offering; plusstyleScales, each styled-system prop's theme scale. Pure data: it loads in plain Node with no DOM, React or Emotion, and survives JSON unchanged. Each component's entry lives beside it inComponentName.catalog.ts, typed against its props, and its stories read their args from it. docs- Nine icons -
home,search,add_circle,inventory_2,palette,article,menu_book,light_modeanddark_mode, for a navigation rail and a theme toggle. Sidebaras a navigation rail -layout="stacked"puts each label under its icon, always shown, in a rail ofstackedWidth(5.5rem) that does not animate; the selected fill and the hover tint are on a pill behind the icon (a newpillslot ofSidebarItem).isOpenis now optional, and ignored by a stacked rail. The default layout is the collapsible rail, unchanged. docs- A
Sidebarpanel rendered in place -panelis the second column's content, in the server-rendered markup, wherehasPanelports the selected item's children in on the client.SidebarItemdocuments its use as a link:href, witharia-currentin place ofaria-pressed. docs NavTree,NavGroupandNavLink- a navigation tree for a site's pages: groups that may fold (a nativedetails, so they work without script), and links with a note, an external arrow, andaria-current="page"on the current page. Put it in aSidebarpanelor anav. docs
Changed
MenuItem'svalueis optional outside aSelect(a menu row needs none).Icon'ssizetakes a theme icon size by name -sm,md,lg- as well as a length.size="sm"used to reach the svg as an unknown width, so the icon collapsed to nothing.ButtonandPressablekeep a new tab at arm's length - rendered as an anchor withtarget="_blank"and norel, they carryrel="noopener noreferrer", asLinkhas.- React 19.0 and up - the peer range is
^19.0.0; it had narrowed to^19.2.8through a dependency update, with nothing in the package needing more than 19.0.
Fixed
<Select native>keeps its accessible name. The native path droppedaria-labelandlabelId, so a native select had no name; it now passes them to the<select>, as the custom listbox does on its trigger.
BREAKING CHANGES
- Responsive props switch at the window classes. Without
theme.breakpoints, array props used styled-system's defaults (40em / 52em / 64em). A theme built frombaseThemenow hasxs0,sm600,md840,lg1200 andxl1600px:display={['none', 'none', 'block']}shows from 840px instead of 832px, and a four-value array reacheslg. Migrate: move each call site to named keys ({ xs: 'none', lg: 'block' }) at the class you mean. _is no longer a responsive key - usexs, the base class.- Generated queries are range syntax (
@media (width >= 840px)), through@soroush.tech/styled-system. Draweris an HTML popover. It opens from aDrawerToggleand closes on Escape or a click outside without JavaScript, and its content is always in the page. It needs anid;isOpenbecomes optional (controlled use);onClose's reasons are'escapeKey'and'dismiss';shouldUsePortal,portalContainer,shouldKeepMountedandshouldEnforceFocusare gone - the browser's top layer replaces the portal. Migrate:<Drawer id="nav">with a<DrawerToggle drawer="nav">, or keepisOpen/onCloseand add anid-popovertargetnames its drawer byid, which is what works without JavaScript. In a browser without the Popover API (Safari before 17, Firefox before 125) the drawer stays closed instead of throwing. Unit tests in jsdom, which has no Popover API, installinstallPopover()from the new@soroush.tech/design-system/testingsubpath.Themerequiresbreakpointsandlayout. A theme written out by hand as a fullThemeobject no longer typechecks. Migrate: build it withcreateTheme(baseTheme, ...).
Packaging
- Depends on
@soroush.tech/styled-system^6.0.0(range-syntax queries, helpers, container queries, the layout grid). homepageis the package's docs, https://docs.soroush.tech/design-system/ (was https://soroush.tech/design-system).
v1.3.4
Documentation only. No code and no API changes - the published bundle is identical to 1.3.3.
Added
- Every component now owns its documentation page:
<Name>.mdxsits beside the component'sREADME.mdand drives that component's page on docs.soroush.tech. The page renders the README's intro followed by every story in<Name>.stories.tsxas a live, editable demo. - The page is required - a component without one fails the docs build.
- The README now explains how to point an AI assistant at this library: the hosted MCP server, the same server locally over stdio, and the Claude Code plugin that wraps it.
docs/consumer/carries the consumer-facing setup guide, the layout-kit patterns, and copyabletheme.ts/providers.tsxstarters. The MCP server serves these to AI tools; they are repo files, not part of the npm tarball.
v1.3.3
Maintenance release. One rendered string changes; no API changes.
Changed
TablePagination's default displayed-rows label now reads1-10 of 57with a hyphen, where it used an en dash.labelDisplayedRowsis a prop: pass your own to render it any other way.
No other component's rendered output changed.
v1.3.2
Maintenance release - the package gained a Storybook of its own. No public API, behavior, or emitted-output changes.
Packaging
storybookno longer delegates to the site's Storybook: the package has its own config in.storybook/, serving only this package's stories against the shippedbaseThemeon port 6007 - the component library is now viewable without the site checked out.build:storybookandtest:storybookscripts were added alongside it; the latter runs every story's play function and a11y check.- New dev dependencies:
@storybook/addon-a11y^10.5.8,@storybook/addon-docs^10.5.8,@storybook/addon-vitest^10.5.8. - Dev-dependency bumps:
@storybook/react-viteandstorybook^10.5.7→^10.5.8,@testing-library/jest-dom^7.0.0→^7.0.1. jsdomstays on^29.1.1, held repo-wide inrenovate.json: jsdom 30 absolutises relative lengths ingetComputedStyle, so2remresolves to32pxand-0.025emto-0.4px, and everytoHaveStyleassertion written in rem or em fails.- No runtime, peer, or optional-peer dependency changes. The
files: ["dist"]allowlist is unchanged, so.storybook/is not part of the published tarball.
v1.3.1
Restores the style props on the published component types - they resolved to any for consumers
on 1.2.0 and 1.3.0 - and refreshes dependencies.
(#335)
Fixed
- Style props are back on every component's published types.
TypographyProps,ImageProps,TextInputPropsand the rest inherit their space / layout / typography / flexbox / border / background / position / grid prop groups from@soroush.tech/styled-system, and the components used to reach for those types through this package's own barrel, which passes them on withexport *. The d.ts bundler flattens that barrel into adeclare namespace, which cannot carry anexport *, so the names were referenced but never declared in the emitted types. Under theskipLibCheckconsumers compile with, those dangling references degraded toanyrather than erroring: every style prop disappeared (<Typography mt={2}>,<Image width="100%">), andTextInput'sPick<LayoutProps, 'width' | 'minWidth' | 'maxWidth'>turned those three into required props. Components now import the types straight from@soroush.tech/styled-system, so nothing routes through that namespace. The emitted JavaScript and the runtime API are unchanged - this release moves types only.
Packaging
- Dev-dependency bumps:
@storybook/react-vite^10.5.6→^10.5.7,eslint-plugin-storybook^10.5.6→^10.5.7,storybook^10.5.6→^10.5.7,eslint^10.8.0→^10.8.1,@types/node^26.1.2→^26.2.0. - Repo-wide dependency overrides clear the vulnerable transitives the lockfile resolved for this
package's dev tree (
webpackforced to>=5,eslint-plugin-importto>=2.32.0, plus in-range patches forbrace-expansion,dompurify,js-yaml, andpostcss). - No runtime, peer, or optional-peer dependency changes.
v1.3.0
Code-quality release driven by static analysis (#333 follow-up): no new components, no behavior changes.
Changed
TextInputpicks its input element (input/textarea/ auto-resizingtextarea) through a plain statement instead of a nested ternary. Rendered output is identical.
Breaking
ariaHidden(element, hide)is replaced byhideFromAria(element)andexposeToAria(element)on the Modal subpath. The boolean-selector helper was a leaked internal ofModalManager- no consumer in this repo imports it, and modal behavior is unchanged. Shipped in a minor deliberately, following the precedent of the styled-systemthemeGetroot-export removal: the export only existed by accident. MigrateariaHidden(el, true)→hideFromAria(el)andariaHidden(el, false)→exposeToAria(el).
v1.2.0
Maintenance release - dependency refresh only. No new components, no API or behavior changes. (#280)
Packaging
reactandreact-dompeer floors raised^19.0.0→^19.2.8. Still React 19 only - the range narrows to the patch line this release is built and tested against. Consumers on an earlier React 19 should upgrade before installing.@soroush.tech/styled-systemdependency floor moves to^5.8.1with that package's own maintenance release.@emotion/*dependencies are unchanged.- Dev-dependency bumps:
@storybook/react-vite^10.5.0→^10.5.6,@testing-library/jest-dom^6.9.1→^7.0.0,@types/node^26.1.1→^26.1.2,@types/react^19.2.17→^19.2.18,@types/react-dom^19.2.3→^19.2.4,eslint^10.7.0→^10.8.0,eslint-plugin-storybook^10.5.0→^10.5.6,globals^17.7.0→^17.9.0,playwright^1.61.1→^1.62.1,react/react-dom^19.2.7→^19.2.8,storybook^10.5.0→^10.5.6,tsdown^0.22.7→^0.22.14. jsdomstays on^29.1.1. jsdom 30 absolutizes relative lengths ingetComputedStyle(2remresolves to32px), which breaks thetoHaveStyleassertions this package's tests write inrem/em; the upgrade is held inrenovate.jsonuntil those assertions stop comparing against computed style. Test-only - it does not affect the published package.
v1.1.0
Features
Sidebar+SidebarItem- a collapsible vertical icon rail. Collapsed, it shows icon-only items; open, each item's label appears next to its icon, rendered away from the anchored edge (a right-anchored rail shows labels to the left of the icons), with the icon pinned so it holds its position across the transition. Purely controlled viaisOpen- the menu toggle lives with the consumer (typically the app bar). Children compose freely (items, logos, footers);anchor,expandedWidth,collapsedWidth, and anyFlexprop (bg, ...) shape the rail. Items are built onPressable- real buttons carrying only the rail's own styling, with no uppercase, bold weight, or letter-spacing inherited from the button family. They own their selected fill andaria-pressed, take asizedensity token, accept customchildrenfor their open-state content, and stay accessible collapsed througharia-labels. The item variant (textdefault,outlined,plain) can be set once on the rail -Sidebar'svariantflows to every item through context, with an item's ownvariantwinning. The rail is a labeled<nav>landmark, animates its width honoringprefers-reduced-motion, and registers theSidebarandSidebarItemtheme slots. WithhasPanel, the rail gains a second column and the selected item'schildrenrender there instead of inside its row - an icon rail beside a detail panel, sized bypanelWidth. The column appears only when the selected item has children, so nothing selected leaves no empty gap; it sits on the rail's inner side followinganchor, and is independent ofisOpen, so a collapsed icons-only rail can sit beside an open panel. The panel is a<section>named by the item'slabel, and that item becomes a disclosure carryingaria-expandedandaria-controls. Selection stays the consumer's - the rail only decides where the children go. Off by default, sochildrenkeep rendering inline in the row as before.Pressable- an unstyled clickable surface: button semantics (keyboard activation, focus ring, disabled state) with none of a button's looks. It carries no padding, margin, border, background, or font of its own, so it wraps arbitrary content without shifting it - the primitive to use instead of anonClickon adiv, which no keyboard or screen-reader user can reach. It renders adivby default, since a<button>may not legally contain a link, another button, or block-level markup; that div is givenrole="button", a tab stop,Enter/Spaceactivation (Space suppressed on keydown so the page never scrolls, fired on keyup), andaria-disabled.as="button"opts into a native button when the content is phrasing-only,hrefrenders an anchor, and both skip the shim.feedbackpicks what happens while it is held:none(default) leaves the content alone,opacityfades it toactiveOpacity, andhighlighttints the surface withcolorat 12.5% opacity. Feedback is press-only, so hover styling stays with the consumer. Its text is never selectable, so a drag or double-click presses rather than highlighting, as on a native button. Accepts thespace,layout,border, andtypographyprops, and registers thePressabletheme slot with themeablefeedback/color/activeOpacitydefaults.Icon- three new registry entries:folder,history, andedit_note.
Changes
gap,rowGap,columnGapandaspectRationow come fromViewfor every primitive built on it, resolved by@soroush.tech/styled-system5.8.0's ownspaceandlayoutparsers instead of per-componentsystem()mappings.Flex,Grid,Button,LinkandPaperdropped that hand-wiring - same CSS, same theme scale, same DOM filtering, and the props now accept responsive arrays everywhere.Flex's andGrid'sGapTokenexports are unchanged. (#314)
Breaking
Link'sgapnarrows fromnumber | stringto thetheme.spacetoken union, matching every other component'sgap. Token values (gap={2},gap="auto") are unaffected; raw CSS values (e.g.gap="8px") no longer type-check. Shipped in a minor deliberately: the prop reached raw CSS only becauseLinkhand-wired its owngapparser, which is the inconsistency this release removes, and no consumer in this repo passes one. Replace a raw value with the nearesttheme.spacetoken, or setgapthroughstyle/cssif you need an off-scale length. (#314)
v1.0.0
First stable release (#303) - the soroush.tech design system as a standalone
package: design tokens, consumer-owned theming, and the full component set,
styled through its own engine. The headless markdown editor/renderer ships
separately as its companion package, @soroush.tech/[email protected].
Features
- A token-driven, consumer-extensible
baseThemewith the full component set: layout & surfaces (View, Flex, Grid, Paper, Card, AppBar, Drawer), content & data display (Typography, Link, Image, Icon, Table, Quote, Avatar, Skeleton, Pagination), inputs & forms (Button, ButtonGroup, TextInput, NativeSelect, Select, MenuItem, Checkbox, Radio, Switch, ToggleButton, Form, FormControl, FormLabel, FormHelperText), feedback (CircularProgress, LinearProgress), and overlay primitives (Modal, Popover, Portal, Backdrop, FocusTrap) - each on its own subpath (@soroush.tech/design-system/Button,.../Table/TableCell, ...). - A self-owned styling API:
styled,css, andkeyframesfrom the root barrel against the package's ownThemetype - the styling engine is an internal implementation detail, and nothing engine-specific leaks through the public surface. docs - Three settled entry points:
.../theme(ThemeProvider,createTheme,baseTheme,useTheme,useDefaultProps,withTheme), the custom-CSS layer at.../style(useStyle,withStyles,StylesConsumer), and raw engine escape hatches at.../engine(Global,globalStyles,CacheProvider,styleCachefor SSR critical-CSS extraction - plus the re-exported@soroush.tech/styled-systemprimitives). - Consumer-owned theming:
ThemeProviderprovides one theme (built-in dark by default);createTheme(base, overrides)deep-merges sparse patches; every scale is an open interface, extended viadeclare module '@soroush.tech/design-system/theme'- palettes, tokens, whole scales, andtheme.componentsslots. docs - Overridable component defaults: literal fallbacks (
size = 'md',variant = 'outside', ...) resolve throughtheme.defaults/ the provider'sdefaultsprop. - Per-component customization via
theme.components-defaultProps, per-slotstyleOverrideswith{ theme, ownerState }callbacks, and theme-contributedvariants(Button is the reference implementation; augmentableButtonVariants). Zero cost for themes that don't use it. - Test utilities under
.../utils/test(renderWithTheme, stories helpers) for consumers testing themed components.
Packaging
- Dual ESM/CJS build via tsdown with a subpath export for every component folder, including nested ones.
- Peers are just
react/react-dom^19; everything else ships as a regular dependency - nothing to install alongside. - Supersedes the
0.1.0preview release: the markdown suite (andCodeBlock) now live in@soroush.tech/markdown, and the former.../themes/.../colorssubpaths are folded into.../theme.