Skip to content
Cynosurev0.5.0
SearchStorybookGitHubnpmColour mode: System. Switch to Light

Project

Changelog

Every release of Cynosure, newest first.

0.5.0

Latest0.5.0 on npm

Minor ChangesLink to "Minor Changes"

  • Badge: leave out the text for a status dot alone in the tone's own colour: with a label it is an image named by it, without one it is decoration hidden from assistive technology.
  • Button: --cy-button-hover-bg and --cy-button-hover-ink tokens set the fill and text colour on hover, for one button whose hover differs from its variant.
  • Button: --cy-button-square-width sets the width of a square (icon-only) button such as an IconButton; it defaults to the height. TitleBar widens its window controls with it.
  • Card: title and description put a header above the content (headingLevel, default 3), sharing its row with actions. New token --cy-card-title-size.
  • Every palette has seriesColors, eight chart colours from the accent that read 3:1 on the page (--cy-color-series-1 to -8), and resolveColors and swiftChartTheme give canvas engines and swift-chart concrete colours, without a chart dependency.
  • ChatMessage is one message in an assistant chat: the person's at the end, the assistant's after an avatar, with a streaming caret, a danger tone, wide content and actions shown on hover or focus.
  • CodeBlock: actions puts your own buttons in the header before the copy button, such as "Insert into editor".
  • Combobox and CommandPalette mark the typed text in their option labels with HighlightedText (in CommandPalette, each typed word). Options are still named by their whole label.
  • CommandPalette: a filter(command, query) prop replaces the built-in search with your own (a fuzzy matcher's { score, positions } ranks the results and marks exactly those letters). Search results are now one flat list in rank order with each command's group shown as a small tag, as recent commands are; browsing without a query still lists commands under their group headings.
  • ContextMenu: one menu can serve many targets (the rows of a list, the tabs of a tab list) through the render prop's targetFor(key); label and items may be functions of the key, and onSelect and onOpenChange receive it. Tabs and Tree use it instead of their own copies.
  • DataGrid is a read-only grid for large results: rows and columns render only where they are in view (a hundred thousand rows by fifty columns stay quick), under a sticky header, with the APG grid keyboard.
  • Disclosure: edge draws a coloured inline-start edge in a tone or any colour, as Card's does, and trailing puts a Badge or a Button at the end of the summary row, outside the trigger button. New token --cy-disclosure-edge-width.
  • ErrorBoundary: a new component that catches errors thrown while its children render and shows a fallback in their place (a danger Notice with "Try again" by default, or your own fallback(error, retry)), with resetKey, onError and label.
  • New Field tokens --cy-field-control-border, --cy-field-control-bg, --cy-field-control-radius and --cy-field-control-sunken-bg restyle the box of every field at once: TextField (and the fields built on it) and HotkeyField read them under their own tokens.
  • Field and every control built on it (TextField, Select, Combobox, TagInput, PathField, NumberField, DateField, TimeField, ColorField, TextArea, HotkeyField; the FieldLayoutProps type): layout="split" puts the label and description at the start and the control at the end of one row (a settings row), moving the control under the text when narrow, and width (sm, md, lg, full) lines the controls of a column up. New tokens --cy-field-control-width-sm, -md, -lg and --cy-field-split-text-min.
  • FileDrop: onPick replaces the browser's file dialog with an app's own picker, as PathField's onPick (the button is busy until its promise settles; chosen files are checked and arrive in onFiles, chosen paths in onPaths), and a controlled value lists the chosen files or paths in the zone, each with a remove button that calls onRemove(item, index). New token --cy-file-drop-file-background.
  • PanZoomCanvas takes a handle prop, as ScrollArea and VirtualList do, and its handle has centerOn (American spelling, as the rest of the API). ref still gives the handle and centreOn still works; both are kept for existing code. VirtualList's scrollToIndex(index, { align }) takes an options object, as ScrollArea's scrollIntoView does; the earlier scrollToIndex(index, "center") form still works.
  • New HighlightedText: text with the parts that matched a search marked, from a fuzzy matcher's positions or every occurrence of a query (one term or several), ignoring case. It reads as one piece of text to screen readers and marks matches with the system colours in forced-colours mode. textMatches(text, query) filters by the same rule, and Combobox, TagInput, TextArea's mentions, Tree and CommandPalette use it, so what a list keeps and what it marks always agree.
  • HotkeyField records a keyboard shortcut for a keybindings page, written as useHotkey reads it; hotkeyFromEvent turns a key press into that string, and useHotkey now also accepts space as a key name.
  • Icons: a new entry, @arshad-shah/cynosure/icons/by-name, exports iconByName(name) and the icons record of every Facet icon by its name, for names that arrive as strings (plugin manifests). It brings every icon with it, so the main entry and @arshad-shah/cynosure/icons never import it and stay tree-shaken.
  • Facet icons: 24 new icons, each with search words and a motion: file-question, file-search, file-plus, window-maximize, window-restore, panel-right, scissors, text-select, clipboard-paste, function (exported as functionIcon), rows, square-plus, cable, boxes, shield-question, gauge, check-check, power-off, loader, party-popper, rocket, hourglass, languages and monitor-down.
  • Stack, Inline and Grid take grow, shrink, fill, minSize="0" and overflow, for app shells whose middle takes the free space and scrolls; GridItem takes overflow.
  • New ListRow and ListRows: interactive list rows (notifications, activity, saved queries, connections) with a leading slot, a title and two meta lines, a trailing slot, actions shown on hover or focus (always on touch), and active, unread, dimmed and tone looks. The rows are buttons or links with one Tab stop and arrow keys, and Right moves into a row's actions.
  • New LiveRegion foundation and useLiveMessage hook: the one hidden live region every component announces through, so the same message twice is spoken twice. FileDrop, TagInput, Tabs, VirtualList and SortableList now announce a repeated message again (a second "1 file added"), and every component's hidden region carries aria-live and aria-atomic.
  • LoadingDots: variant="spinner" draws the loader icon turning, icon-sized and in the current colour, with the same size, tone, color, speed and label; reduced motion leaves it still.
  • Menu: open, defaultOpen and onOpenChange, so an app can open a menu from elsewhere, such as a keyboard shortcut.
  • Menu: --cy-menu-offset sets the gap between a menu and its trigger (default --cy-space-2), and Menubar's menus read it too instead of a fixed 4px.
  • Menu, ContextMenu and Menubar: an item with items opens a submenu beside it, nested to any depth, following the APG menu keys (Right/Left, RTL aware), opening on hover after a short delay that tolerates diagonal travel, and closing the whole tree on a choice; onSelect receives values from any level.
  • Menu, ContextMenu and Menubar: a section's label is optional; a section without one is set apart by separator lines instead of a named group.
  • Menubar: a new APG menubar for a desktop app's application menu, a row of titles each opening a Menu (label, menus: { label, items }[], onSelect, size). One menu is open at a time; Left and Right, hovering and tapping move between menus.
  • Named themes: createTheme({ mode }) fixes a theme to light or dark (the ThemeMode type) so the colour-mode switch does not flip it, and defineTheme and createThemeRegistry check plain-JSON theme definitions (plugin themes) at runtime, rejecting bad input or unreadable text with CynosureThemeError. useCynosureTheme() returns the theme, palette and resolved colours where it is called, for canvas and chart code.
  • Notice: variant="solid" paints the tone's solid colour with the text colour made for it, and size="sm" tightens the padding and keeps the title and message on one line when they fit. New token --cy-notice-padding-sm.
  • OptionList: options take trailing content (a shortcut or a count at the row's end) and dimmed, and the list draws a footer action pinned under the options ({ action, id, active, onActive, onRun }). Select and Combobox options take trailing and dimmed, and both fields take a footer action ({ label, icon?, onSelect }) that the arrow keys reach after the last option.
  • New OverlayFrame: a frame whose content fills it, with an overlay pinned at the bottom centre that stays put while the content scrolls (tokens --cy-overlay-frame-offset and --cy-overlay-frame-layer). ScrollArea and VirtualList draw their overlay with it.
  • PanZoomCanvas takes claimPress: asked about each press with its point in canvas units, it can claim the press to drag a node (children or painted content) instead of panning, with the drag reported in canvas units.
  • PanZoomCanvas's renderScreen gets the screen's dpr, canvasProps to size a <canvas> in device pixels, and a transform to draw in canvas units; it draws again when the window moves to a screen of another density.
  • PanZoomCanvas takes min and max for its zoom limits in percent (25 to 800 unless given), and ZoomControls takes the same min and max, offering only the presets between them.
  • PanZoomCanvas takes overlays: controls in its four corners (start and end follow the page direction) that stay put while the content pans and zooms, keep their own pointer, wheel and keys, and never start a pan. The minimap now sits in the bottom-end corner under that corner's overlay.
  • PathField: a new component for a file or folder path, a text field with a Browse button that asks your app's picker (onPick(): Promise<string | null>) and a clear button, with value/onChange, name and form for forms and labels. A picker that fails leaves the path as it was.
  • Popover: initialFocus (first, panel or none, which leaves focus where it is so a hint can open beside a field being typed in) and width (content, trigger to be at least as wide as the trigger or anchor, or fixed for exactly the size preset). An anchor element now counts as inside, so pressing it no longer closes the panel.
  • useRovingFocus takes current: true to treat the item marked aria-current as the selection, so the Tab stop starts on (and follows) the open entry of a list of links or buttons.
  • RowActions: the buttons at the end of a row, hidden with a mouse until the row is hovered or holds focus or a menu of theirs is open, always shown on touch screens, and in the Tab order only on the row that holds the Tab stop. ListRow, Tree and ChatMessage render their actions with it, so they share one reveal rule (Tree's actions now fade by opacity like the others').
  • New ScrollArea: a box that fills its parent and scrolls, with direction, follow, onScroll, onAtBottomChange, an overlay that stays put (its root is an OverlayFrame), and a handle with scrollToEnd and scrollIntoView. With a label it is a named region and a tab stop while there is something to scroll.
  • SearchField: trailing shows status text inside the field's end, such as a match count ("3 of 12"), announced politely when it changes. New tokens --cy-search-field-trailing-color and --cy-search-field-trailing-font-size.
  • One vocabulary and one look across lists and fields:

    • Select and TextArea now read the field skin (--cy-field-control-border, --cy-field-control-bg, --cy-field-control-radius, and for a read-only TextArea --cy-field-control-sunken-bg), so restyling fields once restyles every field's box.
    • VirtualList takes selected / defaultSelected, like Tree, Table and DataGrid. selectedKeys / defaultSelectedKeys still work but are deprecated.
    • Tabs reveals the selected tab with the shared alignedScroll. A padded tab wider than the strip now shows its start, as scrollIntoView does.
  • SideNav works as an activity bar: footer pins entries (Settings, a notifications action) to the bottom, placement="end" puts the current indicator and the rail's tooltips on the side of a right-hand rail, and in the collapsed rail an item's badge sits on its icon's corner.
  • Button, IconButton, Select, TextField, ProgressBar and SegmentedControl take size="xs" (the new DenseSize type): about 20px tall (the new global --cy-control-height-xs, control.height.xs in a theme), with smaller text, icons and padding, for dense desktop chrome. Button and TextField also take --cy-button-height-xs, --cy-button-padding-inline-xs, --cy-text-field-height-xs and --cy-text-field-padding-inline-xs. Only the look shrinks: the hit area stays at least 24px, and 44px on coarse pointers.
  • New SortButton: the button in a sortable column's header, with the sort's icon. Table and DataGrid render it, so their sort headers share one look (Table's cy-table-sort and DataGrid's cy-data-grid-sort classes are gone).
  • SplitPane: collapsed, defaultCollapsed and onCollapsedChange collapse a pane from a menu or button and restore its remembered size, onResizeEnd fires once when a drag or key resize ends, and anchor="end" gives the size to the end pane (a right sidebar or bottom dock that keeps its pixels). Collapsing no longer reports a size of 0 through onSizeChange: the size is kept.
  • StatusBar and StatusBarSegment: the bar along the bottom of a desktop app, with start and end clusters of segments that show a tone, an icon or a status dot and monospace text, and as buttons open a Popover above them; arrow keys move between the clickable ones. --cy-status-bar-soft-mix sets the tint of soft segments, static or clickable.
  • Tab: a new closeLabel prop names a closable tab's close button. Without it, a tab whose label is not a plain string (a badge, bold text) is named by the text it shows instead of its raw value.
  • Tabs: contextMenu and onCommand on a Tab give it its own commands (Close others, Close to the right…) in a ContextMenu opened by right-click, a long press, Shift+F10 or the context menu key. A chosen command goes to onCommand(value), the name Tree uses for its commands.
  • Tabs: dirty on a Tab marks unsaved changes with a dot that turns into the close button on hover or focus; the close button's name and the tab's description say the changes are unsaved.
  • Tabs: trailing on a TabList places content, such as a "New tab" button, right after the last tab and outside the tab list.
  • Tabs: a label longer than --cy-tabs-label-max-width (default 20em) is cut short with an ellipsis, and the whole text shows in a tooltip on hover or focus.
  • Tabs: a middle-click on a closable tab closes it, without starting the browser's autoscroll.
  • Tabs: scrollable on a TabList adds chevrons at each end, shown only while there are more tabs that way, and scrolls the list sideways with a vertical mouse wheel.
  • TagInput: Backspace in an empty field now takes the last tag back into the field to edit (announced through the new labels.editing) instead of only removing it. renderTag(tag) draws custom tag content, and iconStart puts an icon in the field.
  • TextArea: mentions. trigger (such as "@") and suggestions (a list, or a function of the query returning items or a promise) open the shared drop-down list at the start of a word; arrows move, Enter or Tab inserts the value and a space, Escape closes it, and onMention reports the choice. The list is named by the field's label; new labels.loading.
  • TextField: boxRef gives the field's box (the input with its icons, addons and buttons), so a list or popover can drop from the whole field. Combobox and TagInput anchor their lists with it.
  • Text: wrap="pre" keeps spaces and line breaks as written, and wrap="break" breaks long ids and URLs anywhere instead of overflowing.
  • TitleBar: the title bar of an installed app that draws its own (frameless Electron windows, Window Controls Overlay). The whole bar drags the window while the controls inside stay clickable, and it takes leading, trailing, leadingInset, an overlay mode, onHeightChange and window controls (minimize, maximize or restore, close).
  • Toast: toast({ id }) with the id of a showing toast updates it in place instead of adding another, loading shows a spinner and holds the toast until it is updated, and onDismiss(id, reason) says whether it timed out, the user closed it or dismiss() did.
  • Tooltip: disabled turns the tooltip off without remounting its trigger, and describe={false} shows a tip that repeats the trigger's name without reading it to screen readers again; whenTruncated turns it on only while text is cut short (measured with one ResizeObserver) and implies describe={false}. Tabs, StatusBar and DataGrid use it, so StatusBar and DataGrid no longer read a cut-short text twice and DataGrid's tip shows only when the cell is cut.
  • Tree, for schema explorers: items take iconTone/iconColor, trailing, a typed data payload, actions (buttons on hovered, focused or selected rows, in the Tab order after the focused row) and a contextMenu (one ContextMenu beside the tree), all reporting through onCommand; kind: "group" heading rows that count their children and are never selected; filter (a string or a function returning match positions) shows matches with their ancestors and marks them; loadingIds, errorIds, onRetry, renderError and renderEmpty for loading driven by a store; and a handle whose reload(id) runs loadChildren again.
  • VirtualList takes onAtBottomChange and an overlay pinned over the bottom of the list (a "3 new" pill), and follows its end through the same helper as ScrollArea. The list now always sits in an OverlayFrame.

Patch ChangesLink to "Patch Changes"

  • Accordion: a controlled, collapsible single Accordion now closes its open section when value becomes undefined, instead of falling back to its own state and staying open.
  • Select, Combobox, TagInput, TextArea's mentions and CommandPalette keep their active option the same way: kept while it is still in the list, else the first usable one, and always scrolled into view (TagInput's suggestions did not scroll before).
  • Popovers, menus and drop-down lists flip to the other side when their content grows after opening and no longer fits.
  • AppShell: the navigation drawer closes when the shell widens, so it no longer pops open by itself when the shell narrows again.
  • Slider and NumberField write aria-valuemin and aria-valuemax without exponent notation (0.0000005, not 5e-7), as they already did aria-valuenow.
  • Calendar no longer risks a hydration mismatch: the device's date, language and weekend settle in the browser before the first paint, and a server draws no day as today and uses locale (or English) and Monday-first weeks until then.
  • Calendar builds its date formatters and reads each locale's week once instead of on every render (about 40 Intl objects per render before), so DateField stays light while typing.
  • A Checkbox with indeterminate stays mixed after a click or Space while the prop is still true, instead of the browser clearing it.
  • A controlled ColorField keeps its saturation and hue at black and through 8-bit rounding: bringing the brightness back up returns the colour you had, not grey. A value the parent changes itself is still read afresh.
  • ColorSwatches no longer chooses a swatch when Tab lands on it (so a custom colour in ColorField survives tabbing through), and a click calls onChange once instead of twice.
  • Combobox with allowCustom keeps the chosen option's value when you leave the field (it used to commit the label), and options that load after the field mounts now show the chosen label instead of blank text that cleared the value on blur.
  • CompareSlider calls onChange only when the divider actually moves, not for Home at the start, arrows past an end or a pointer resting on one spot.
  • <CynosureTheme contrast="normal"> without a theme now keeps the normal palettes on an OS set to more contrast, instead of inheriting the high-contrast tokens from the root.
  • DateField submits its date, not the text being typed: a form will not submit while the text is not an available date, and a form reset brings back defaultValue.
  • DateField's calendar now opens on the chosen day inside a shadow root too; it used to find the calendar by id in the document, which missed it there.
  • The FloatingToolbar and HoverCard offset tokens accept any length (rem, calc()), and the offset is read once when they open instead of on every render.
  • Escape in a Menu or Popover opened from a FloatingToolbar or DockedToolbar now closes that menu first; the next Escape leaves the bar.
  • useHotkey now matches Option shortcuts on macOS (alt+k) and shifted digits (shift+1) by the physical key, and accepts + as the key (mod++).
  • TextField, Select and SegmentedControl at size="lg" show large (20px) icons, like a large Button, instead of medium ones, so a row of large controls shows one icon size.
  • NumberField reads back its own text in every locale: other digits (Arabic-Indic in ar-EG, Devanagari…), currency text with dots such as "1.234,50 kr.", unit text and accounting brackets. Edits there are no longer undone on blur.
  • ProgressBar: documents the slim bar in a row of text (labelHidden without showValue), with a story that checks it stays named, takes the space between its neighbours and fills from the reading start.
  • Reduced motion no longer stops the animations of host elements whose class merely contains "cy-" (such as .privacy-banner); only Cynosure's own elements stop.
  • A Menu or Popover inside a Toolbar keeps its own arrow keys, and the toolbar keeps its Tab stop while one is open.
  • Escape in a SearchField while an input method is composing (Chinese, Japanese, Korean…) now only cancels the composition; it no longer clears the query or leaves the field.
  • In an open Select, PageDown and PageUp stop on the last or first usable option when the end option is disabled, instead of losing the active option.
  • Selected rows in Tree, VirtualList and DataGrid use the global --cy-color-accent-soft, the same tint as Table's selected rows (it was a slightly lighter mix of the accent written into each stylesheet).
  • Inside a shadow root, closing a Popover, Menu or other layer returns focus to its trigger, a modal Popover keeps Tab inside it, and dismissing a focused Toast moves focus to the next toast instead of leaving the stack paused.
  • SignaturePad draws again at the new density when the window moves to a screen of another pixel density (or the browser zooms), so the signature stays sharp; it measures through the same helper as PanZoomCanvas.
  • SignaturePad draws faster: finished strokes keep their outline, the canvas is reallocated only when its size or pixel density changes, and the stroke in progress is drawn once per frame without re-rendering. What it draws and reports is unchanged.
  • A controlled Slider value outside min/max now shows at the nearest end: the thumb, aria-valuenow and the arrow keys stay in range.
  • Slow trackpad pinches now zoom DocumentViewport and PanZoomCanvas: Ctrl/Cmd+wheel steps under a whole percent add up instead of being rounded away.
  • SortableList: a keyboard move is cancelled when focus leaves the handle, instead of staying picked up. Dropping applies the move to the items as they are now, so items the parent added during the move are kept.
  • Tooltip, Combobox, TagInput and the components that float beside an anchor no longer log "useLayoutEffect does nothing on the server" when rendered on the server with React 18.
  • Slider and NumberField round to the decimals of both step and min, so min={0.5} step={1} reaches 0.5, 1.5 and so on, and steps written as 1e-7 no longer round to 0. Their ARIA and form values are written without exponent notation.
  • Table: selecting rows no longer drops selected ids that are not in the current rows, so a selection survives paging and filtering. The select-all acts on the rows shown.
  • Tabs: tab values that differ only in punctuation, such as "a b" and "a_b", no longer share an element id, so each tab controls its own panel.
  • TagInput's arrow keys between the input and the tags follow the reading direction, so in right-to-left text Arrow Right moves into the tags.
  • TextArea fits its height again when its width changes (a resized window, a collapsing sidebar), so rewrapped text neither scrolls nor leaves empty lines.
  • CynosureTheme no longer logs React 18's useLayoutEffect warning when rendered on the server.
  • TimeField submits its 24-hour value, not the text being typed: a form will not submit while the text is not an allowed time, and a form reset brings back defaultValue.
  • TimeField's arrow keys now keep the minutes on the step grid: with a step of 15, Up from 09:32 goes to 09:45 (not 09:47) and Down to 09:30.
  • Swiping a Toast away now runs through the shared drag handling: Escape mid-swipe puts it back and taps on its buttons stay taps. A leaving toast (and a closing Dialog) waits for its longest exit transition, delays included.
  • Tree: ArrowRight and ArrowLeft no longer move the tab stop onto a disabled row (nor does a selected disabled row take it), so the tree always stays in the Tab order. Expanded and selected rows are looked up through Sets, so large trees render faster.
  • Type-ahead in Select, Menu, Tree and VirtualList now finds the first item when nothing is active yet, instead of skipping it.
  • "use client" now marks only modules that run React, so themeInitScript(), qrToSvg() and the zoom and layout helpers can be called from React Server Components.
  • VirtualList: moving rows with Alt+arrows or a drag is linear in the list's length (it was quadratic, so a large selection in a long list froze the page), and scrolling no longer re-keys every item or re-measures every wrapped row.

0.4.0

0.4.0 on npm

Minor ChangesLink to "Minor Changes"

  • New @arshad-shah/cynosure/astro adapter, an Astro integration that themes the page on the server: cynosure({ theme }) adds the product theme as CSS on :root and Cynosure's stylesheet to every page, and the saved-mode script inline in <head> (initScript, default on); htmlAttributes({ mode, contrast }) gives <html> its mode and contrast attributes. See docs/adapters.md.
  • New Choice: the row a choice control sits in (the control, its label and description, the whole row clickable), with strikeWhenChecked and the tokens --cy-choice-gap, --cy-choice-done-color and --cy-choice-strike-width. Checkbox, Switch and RadioGroup's options are now Choice rows, and each owns its control's look in its own stylesheet: styles/checkbox.css holds only the box, the new styles/switch.css the switch, styles/radio-group.css the radio, and styles/choice.css the shared row (it used to be ChoiceBase's stylesheet, now styles/choice-base.css). Import Choice's stylesheet with Checkbox's, Switch's or RadioGroup's when you load stylesheets one by one (styles/manifest.json lists them). Props, tokens and the look are unchanged; --cy-checkbox-done-color and --cy-checkbox-strike-width now set the row's done tokens. ChoiceControlProps and ChoiceInput are exported for building new choice controls.
  • New DeviceFrame: a phone, tablet or browser window drawn in CSS around a screenshot, a video or a live demo. The screen keeps its ratio at any width and clips its content to its corners, orientation turns a phone or tablet, and the browser bar shows an optional url. It is decoration by default and adds nothing to the accessibility tree; with label it is a named figure.
  • TextField: new addonStart prop (controls inside the start of the field, before the input) and --cy-text-field-numeric token (the typed text's font-variant-numeric). With type="search" TextField hides the browser's own search decorations itself. DateField, NumberField and TimeField set --cy-text-field-numeric: tabular-nums instead of restyling the input, and SearchField no longer has a stylesheet of its own: dist/styles/search-field.css is gone (its rule is in text-field.css). ColorField is now a TextField with the swatch button in addonStart, and draws its hue and opacity strips through Slider's trackContent instead of restyling the slider; it looks and behaves as before.
  • The root entry exports the types components' props are written in: IconData, IconNode, IconMotion, IconCategory and BrandInfo (no need to reach into /icons for a prop's type), and the layout types Space, PaddingProps, Breakpoint, BREAKPOINTS, LayoutAttributes, VisibilityProps and GridResponsive.
  • New IconTile: a Facet icon on a tinted square or circle, for features, settings sections, stats and list items. It has three sizes (the control heights, so it scales with the theme), the six tones, soft, solid and outline variants, and is decorative unless it has a label, which makes it an image with that name.
  • Ten new Facet icons (250 in all) and a Google Play brand mark (98 marks). calendarDays, checkSquare (a checkbox with a large tick, Lucide's square-check-big), bookMarked, listTree, refreshCw and qrCode join their categories. A new faith category holds mosque, kaaba, crescent (a crescent with a star) and prayerBeads (tasbih). googleplay comes from Simple Icons, unchanged. Each has search words in ICON_KEYWORDS, and grid is now also found by "layout grid".
  • Layout primitives take what page layouts asked for. Stack, Inline, Grid, GridItem and Container accept id, role and aria-* in their types (they already passed them on) and hideBelow / hideAbove (a breakpoint of the window: hidden with CSS, no script). Grid columns takes a track list for unequal columns ("2fr 1fr"). GridItem takes span and order per breakpoint ({ base: "full", md: 2 }), measured on the grid's width like its columns, for unequal columns that change with the width and for reordering. GridItem and Stack take sticky and stickyOffset. The breakpoint scale is now shared as BREAKPOINTS; GRID_BREAKPOINTS is the same object.
  • New List: a real ul or ol with drawn markers (disc, number, check, icon, tile or none), each item a label with an optional description. Numbered lists are ordered lists that count from start and suit how-to instructions; gap, divided, size and tone adjust the look, and markers follow the reading direction.
  • New OptionList (with OptionListItem, OptionListNote and OptionListGroup): the drop-down list of options under a field, which Select, Combobox and TagInput now all draw with, so their lists are one component instead of three copies. Restyle it with --cy-option-list-{bg,shadow,max-height,max-width,active-bg}; Select's --cy-select-list-* and --cy-select-option-active-bg and Combobox's --cy-combobox-list-* and --cy-combobox-option-active-bg keep working and set them. TagInput's suggestion list follows --cy-option-list-* (it used to read Combobox's tokens). The lists look as before; Select's list now also drops its fade under reduced motion and outlines the active option in forced-colors mode, as Combobox's did. The list classes changed (cy-select-list, cy-select-option…, cy-combobox-list, cy-combobox-option… are now cy-option-list…).
  • Button takes square (as wide as it is tall, no padding, for one icon) and, with href, id and linkRef (the link element). IconButton is a square Button and takes flipRtl (the icon is mirrored in right-to-left text), iconColor and wrapperClassName (layout of the element around it). Tooltip takes className for its anchor and Icon takes flipRtl. IconButton no longer has a stylesheet: dist/styles/iconbutton.css is gone (its rules are now Button's square).
  • Printing: closed Accordion and Disclosure sections print open, and every TabPanel prints, in order (unselected panels render their content while the page is printed). The shared usePrinting() hook is in src/foundations/printing.ts.
  • New QRCode: a QR code drawn as one crisp SVG path from Cynosure's own encoder (versions 1 to 40, levels L, M, Q and H, numeric, alphanumeric or UTF-8 text). size takes sm, md, lg or a number of pixels, quietZone keeps the light margin scanners need, and icon puts a logo in the middle and raises the level to H. It stays dark on light in dark mode, because many phones cannot scan an inverted code. qrMatrix, qrPath and qrToSvg export the encoder for pages without React.
  • New Section: a full-width band of a page with a background (variant: plain, surface, soft, accent), padding above and below from a space token, and its content in a Container (size, or contained={false}). as, id, aria-labelledby, aria-label and role are set on the root, so a Section can be a named region and an in-page link target.
  • New SiteFooter: the bottom of a website. A brand and tagline, link columns as data ({ title, links }, in one navigation landmark, each list named by its heading), and a bottom row with legal and social slots. Columns wrap and the bottom row stacks on narrow screens, and every link has a 44px hit area on touch screens.
  • New SiteHeader: the top bar of a website that scrolls as a document, where AppShell (an app frame) does not fit. It has a brand linked home, the site navigation as data ({ label, href, current? }, the current one marked aria-current="page"), an actions slot, a SkipLink before it, and it sticks to the top over a blurred view of the page (sticky, default true). Below collapseBelow (the header's own width, CSS only) the links move into a menu Sheet opened from a button; Escape closes it and focus returns to the button.
  • SkipLink is shown as an accent outline Button link, hidden by the shared .cy-visually-hidden until it has focus (still CSS only, so it works without JavaScript), and takes contained to show at the corner of the nearest positioned ancestor (AppShell sets it). Restyle the shown link through the --cy-button-* tokens; SkipLink's own tokens are offset and radius.
  • New SkipLink: "Skip to content", hidden until focused and then shown at the window's top-start corner. It is a plain link that works without JavaScript; with it, focus moves to the target (given tabindex="-1" when it needs one) without changing the URL, also inside shadow roots. AppShell and SiteHeader now both use it, so the library has one skip link.
  • Smaller additions: Button color (any CSS colour, overriding tone; the text on a solid fill is white or black, whichever reads); Link underline (always, hover or none) and aria-current; ThemeToggle storageKey; PageHeader titleSize and Heading size="3xl" (1.4 times 2xl, between PageHeader's md and lg); EmptyState headingLevel={1} for a page that is the empty state; Card aria-current, shown with the accent border (--cy-card-current-border).
  • Stack takes direction ("column", the default, or "row"), and its align accepts "baseline". Inline is now a Stack in a row: it renders Stack with direction="row" and keeps its own centring and wrap in the new styles/inline.css (Inline's rules used to live in styles/stack.css). Load both stylesheets for Inline when you import them one by one. Inline's props and look are unchanged.
  • New Stat: a KPI tile on a Card with a label, a value in tabular figures, an optional unit, description and icon, and a delta that draws an arrow and the change in a tone and is spoken as "up 12%" (the direction comes from the sign or is given, and the words translate through labels). The label and figures are a description list, so they are read together. href makes the whole tile one link; as="li" keeps a linked stat inside its list item.
  • Steps works as a numbered how-to: current is optional. Without it every step is numbered alike, nothing is marked current, no status is read and the list is named "Steps" (labels.list still overrides it). A step's own status still shows.
  • Every component now says which stylesheets it needs. @arshad-shah/cynosure/styles/manifest.json maps each component to its files, base first, worked out from the source imports at build time (Inline → stack.css, Sheet → dialog.css, Accordion → disclosure.css, IconButton → tooltip.css, Button → loading-dots.css). The Vite and Astro adapters take styles: "imports" to add exactly those stylesheets for the components each module imports. .cy-visually-hidden moved from link.css to base.css, so Steps, Tabs, Calendar, Stat and the others no longer need link.css. ColorField now lists text-field.css and TagInput combobox.css, whose classes they use.
  • New VisuallyHidden: content for screen readers only (as span, div, p or a heading), with a focusable variant that is shown while something inside it has focus. The shared .cy-visually-hidden rule it uses moved from link.css to base.css, so it no longer depends on loading Link's stylesheet.

Patch ChangesLink to "Patch Changes"

  • Button no longer cuts off the tails of g, y and p in fonts with deep descenders (Plus Jakarta Sans): the label's box holds the font's whole height. The button's height is unchanged.
  • ChoiceBase's stylesheet is now styles/choice-base.css (it was styles/choice.css, which is now the Choice row's), and its class is cy-choice-base (was cy-choice-input). Update a one-by-one stylesheet import of choice.css for ChoiceBase to choice-base.css.
  • DeviceFrame fixes from a product page. On a dark page the phone and tablet bezel no longer disappears: it is a dark grey lifted off the page with a lighter rim (3:1 against the page), and still the ink on a light page. Live content on a phone now starts clear of the camera island (below it upright, beside it turned) instead of running under it; a screenshot, video or canvas still fills the screen. The new safeArea prop decides for any content.
  • KeyValueEditor and Checklist no longer reach into the Checkbox and TextField they render: KeyValueEditor lays out its checkbox through a class of its own and hides its file picker with the shared cy-visually-hidden class, Checklist finds its focus targets through its own classes, and TagInput's announcer uses cy-visually-hidden.
  • HiddenValue hides its input with the shared .cy-visually-hidden class from styles/base.css, so styles/hidden-value.css is gone; drop it from one-by-one stylesheet imports. Behaviour is unchanged.
  • Avatar and Image show their picture in server-rendered HTML that is never hydrated (an Astro page without a client directive). Before any script runs the picture shows as the browser has it; on mount an unfinished picture switches to the fallback or placeholder before the first paint, as before. Image's data-state has a new first value, pending.
  • Each component styles only its own classes, with no change in look: Pagination, Calendar, CopyButton, QRCode, Dialog, Notice, SideNav, SortableList, Toast and FloatingToolbar place, mirror and colour the Buttons, IconButtons, Icons, Tooltips and Toolbars they render through those components' props and tokens and their own layout classes; Sheet draws its docked panel on its own classes (.cy-sheet, .cy-sheet-panel, .cy-sheet-actions) over the Dialog's frame. Announcer, CopyButton, Field, FileDrop, SideNav's collapsed rail, SortableList, Steps and Table hide text with the shared .cy-visually-hidden. SiteHeader's brand and navigation links are Links, drawn as before by the span inside each.
  • SegmentedControl's internal classes follow its name: cy-segmented is cy-segmented-control, cy-segment is cy-segmented-control-segment and cy-segment-label is cy-segmented-control-label. Restyle through the --cy-segmented-control-* tokens; the look is unchanged.
  • A product palette now reaches everything worked out from it. createTheme({ palettes }) applies each palette's colours first and derives from them: accentSoft is mixed with the palette's own surface (it used Cynosure's), an accent given for one palette re-derives that palette's hover, active, soft, text and on-accent colours, and the high-contrast palettes (prefers-contrast: more, contrast="more") start from the product's light and dark palettes instead of falling back to Cynosure's. Colours a patch sets explicitly still win.

0.3.0

0.3.0 on npm

Minor ChangesLink to "Minor Changes"

  • AppShell now shares its narrow state, so products no longer repeat the breakpoint. useAppShell() returns { narrow } to anything rendered inside the shell (it is false on the server, on the first client render and outside a shell), and the new onNarrowChange prop tells code outside the shell once per change.
  • New AspectRatio component: a box that keeps a width-to-height ratio at any width, for a video, a map, a chart or an embed. ratio takes a number or a CSS ratio such as "4/3" (default 16 / 9) and the height comes from CSS, so nothing is measured. Its children are stretched to the box, images and videos cover it, and radius rounds and clips the corners from the radius tokens.
  • New Avatar: a person, team or thing as a picture, falling back to initials worked out from the name (emoji and non-Latin names included) and then to an icon. It has five sizes, circle and square shapes, tones (with tone="auto" picking one from the name), a status dot that is spoken with the name, and decorative for when the name is written beside it.

    New AvatarGroup: overlaps several avatars along the reading direction, gives them one size and sums up the rest as a "+3" avatar that names the hidden people.

  • New Breadcrumbs component: the trail from the top of a site or app to the current page, as a "Breadcrumb" navigation landmark with an ordered list. Pass items (label, href, onClick, icon); every item but the last is a link or a button, and the last is the current page (aria-current="page"). maxItems collapses the middle of a long trail into a menu, long names are cut with an ellipsis and the trail wraps in narrow containers.
  • Button and IconButton now set aria-current on the element in both forms, so a link-form button (href) can say it is the current page; a link-form button also takes style now. Pagination with getHref renders the current page as a link with aria-current="page" instead of a button. Pagination's onChange gets the click event as a second argument, (page, event), so a client-side router can call event.preventDefault() on a link press and navigate itself.
  • Calendar gains markDay, which marks a day without drawing it: a tone tints the cell, dots puts up to three coloured dots under the number, and label is added to the day's accessible name. The day state passed to renderDay and markDay has a weekend flag that follows the locale. With renderDay, a day's height is its own token, --cy-calendar-content-size, and defaults to 66px on touch screens (it was 110px), so a six-week month fits a phone while every day stays a 44px target.
  • Calendar can have its month set from outside: month (controlled, YYYY-MM or any date of the month) and defaultMonth, with onMonthChange reporting the month asked for. monthPicker turns the month heading into a button that opens a panel to jump to any month and year within min and max. The focused day now always stays inside the month shown.
  • Card takes edge: a coloured edge on its inline-start side for a category or status. A tone name uses that tone's colour and any other string is a CSS colour. It follows the reading direction and the card's radius, works with every variant and on link and button cards, and its thickness is the new token --cy-card-edge-width. Pair it with text or a Badge, since colour alone carries no meaning for many people.
  • Checkbox has a checklist style: with strikeWhenChecked, a checked box strikes through and mutes its label and mutes the description, so a finished task reads as done. The muted label keeps 4.5:1 contrast in light and dark, the strike stays in forced colours, and the new tokens --cy-checkbox-done-color and --cy-checkbox-strike-width adjust it. Switch and RadioGroup are unchanged.
  • New Checklist: a named list of rows people add, tick, rename in place and remove, such as a task's subtasks. It works controlled (items) or uncontrolled (defaultItems), moves focus to a sensible control after every change, and has maxItems, reorderable (drag or keyboard, through SortableList), disabled, readOnly, two sizes and overridable labels.
  • New Container: the page column. It centres its content, caps its width (size: sm 640px, md 768px, lg 1024px by default, xl 1280px, or full for no cap) and adds side gutters from a space token that never get narrower than a phone's safe area. paddingBlock, center and as (div, main, section, article) cover the rest, and the widths are tokens.
  • The default changed: Dialog and Sheet no longer keep their content mounted while closed; pass the new keepMounted prop to keep the old behaviour. The content now mounts when the dialog opens and unmounts once its closing transition has ended (at once under reduced motion), so every opening starts with fresh state, a closed dialog renders an empty <dialog> element (on the server too), and tests can no longer find the content of a closed dialog. Use keepMounted for content whose state must survive closing or that is expensive to rebuild. This also applies to what is built on them: ProgressOverlay, and the navigation drawer of AppShell.
  • New Divider: a line that separates content. It is horizontal by default and fills its container; orientation="vertical" stretches it to the height of a row such as an Inline or a footer of links. It can carry a short label in the line (labelAlign places it), be dashed, take a strong tone and add its own spacing. It is decorative by default; decorative={false} makes it a role="separator" named by its label.
  • Grid columns also takes a count per breakpoint, columns={{ base: 1, sm: 2, lg: 4 }}. The breakpoints (sm 30rem, md 45rem, lg 60rem, xl 75rem, exported as GRID_BREAKPOINTS) are widths of the grid itself, not the window, so it adapts inside panels, popups and shadow roots. It is CSS only, so server-rendered markup is right on the first paint, and GridItem spans are clamped to the columns in effect at each width.
  • New Heading component: a section or page heading whose level (the h1 to h6 element) is separate from its size (xs to 2xl, plus display for a hero title), so the page outline stays correct whatever the heading looks like. It has tone, weight, align and truncate (one line or a number of lines), uses the display font with a tight line height and balanced wrapping, and has no margins. Each size has a token (--cy-heading-size-*) so a product can retune its heading scale.
  • Icons: ICON_KEYWORDS (from @arshad-shah/cynosure/icons) maps each Facet icon's name to other words it is found by (trash: delete, remove, bin), for galleries and icon pickers. The words are a separate export, so importing an icon does not bring them along.
  • New Image component: a picture that never shifts the layout. ratio reserves its space with AspectRatio (or width and height do), a Skeleton shows while it loads, and a quiet box with the image icon and the alt text takes its place when it fails; placeholder and fallback replace either. It also has fit, position, radius, lazy loading by default, a caption that makes a named figure, and it notices a picture that had already loaded before React attached.
  • Stack, Inline and Grid take padding, paddingInline and paddingBlock: space tokens that pad the layout's own box, so a wrapper element is no longer needed for it. The specific prop wins over padding, and a nested layout never inherits its parent's padding. Layouts that do not use the new props are unchanged.
  • Icons in options can be coloured. Select and Combobox options and Menu and ContextMenu items take iconTone and iconColor, with the meaning of Icon's tone and color. Select keeps the colour in its trigger once the option is chosen. Disabled options show the icon muted, and icons without the new fields look as before.
  • New PageHeader component: the top of a page or a major section, with breadcrumbs, an eyebrow, the title (the header's one heading, at headingLevel 1 to 3), a lead description, a meta row, actions and content below. It is built from Heading and Text. The actions sit at the inline end and move below the text when the header's own width is under 36rem; size="lg" with align="center" makes a landing page hero, and divider draws a bottom border.
  • New component: Pagination, for moving between the pages of a list or table. It is a navigation landmark of Buttons with previous and next, numbered pages with ellipses, optional first and last controls, a compact "Page 3 of 10" variant for phones, and real links through getHref.

    The helpers pageCount(total, pageSize) and pageRange(page, count, siblings, boundaries) are exported with it.

  • SideNav takes one primary action among its items: { kind: "action", id, label, icon, onClick } (type NavAction). In layout="bar" it is a round accent button raised above the tab bar, placed where it sits in items; in the column it is a full-width accent button and in the rail an icon button. It is a button, never the current page, and does not call onNavigate. New tokens: --cy-side-nav-action-size and --cy-side-nav-action-lift.
  • New Text component: one piece of text in the type scale, for labels, captions, lead paragraphs, helper lines and values. It has presets (body, lead, caption, eyebrow, code), size, weight, tone and align props that map to the font and colour tokens, one-line or multi-line truncate, and numeric for tabular figures. It renders a span by default or the element given in as, adds no margins, and is not changed by host CSS.
  • TimeField can have a picker, and shows the time the way the person's region writes it.

    • picker adds a clock button at the end of the field that opens columns rolling under one lens, with a strip of the whole day to jump along (dayTrack={false} hides it) and the hours outside min and max hatched. It is off by default: fields that do not ask for it look as they did.
    • hourCycle is "auto" by default: 9:30 PM where AM and PM are used, 21:30 elsewhere, from the browser or from locale. hourCycle={24} keeps the form every TimeField had before; hourCycle={12} fixes the 12-hour form and adds an AM/PM switch to the picker. The value, the limits and what a form receives stay 24-hour HH:MM.
    • New labels (open, hours, minutes, seconds, am, pm, period, dayTrack, now) and tokens (--cy-time-field-row-height, -column-width, -lens-background, -night, -day).
  • Toaster takes offset, to keep toasts clear of fixed parts of an app such as a bottom tab bar on a phone. One value (a space step like "8" or a CSS length like "56px") moves them in from the top or bottom edge they sit on; { block, inline } also sets the distance from the side. The same distances are the new tokens --cy-toast-offset-block and --cy-toast-offset-inline, so an app can set them in CSS, for example only below a breakpoint. The toast region now also stays inside the device's safe area (env(safe-area-inset-*)) on every side.

Patch ChangesLink to "Patch Changes"

  • Theming: createTheme({ components }) overrides for components with multi-word names (sideNav, timeField, segmentedControl…) are now written as the custom properties their styles read (--cy-side-nav-…). Before, they were written with the camelCase name (--cy-sideNav-…) and had no effect.

0.2.0

0.2.0 on npm

Minor ChangesLink to "Minor Changes"

  • SegmentedControl: two controls given the same name no longer share one selection. The segments are grouped under a name of the control's own (with ids of its own), and name submits the value through a hidden field, so choosing in one control never clears another and a label never points at another control's segment. A form's reset still restores the default. The small size is now exactly as tall as other small controls (26px, was 28px); its segments keep a 24px hit area. In dark mode the active segment is now a visible pill: it used the raised surface, which on dark is as dark as the track, so only the text colour showed which segment was active.

Patch ChangesLink to "Patch Changes"

  • AppShell: text hidden for screen readers (a Table's hidden caption, Kbd and Calendar labels) inside the content or the navigation now scrolls with that area. Before, it escaped the scrolling area and stretched the shell's container, so a full-height shell showed a second, page-level scrollbar beside the content's own.

0.1.0

0.1.0 on npm

Minor ChangesLink to "Minor Changes"

  • AppShell: tabs shows a bottom tab bar on narrow screens. SideNav: layout="bar" draws its top-level items as that bar.
  • New: Calendar (the APG date grid, with renderDay for a month of plans), DateField (typed, stepped or picked) and TimeField (24-hour, read loosely). Dates are YYYY-MM-DD strings; the date and time helpers are exported.
  • Tabs: document tabs. Tab takes onClose (a close button, and Delete on the focused tab); TabList takes onReorder (drag, or Ctrl/Cmd+Shift with the arrows). Tabs takes labels.
  • Code colours as tokens (--cy-color-syntax-*, keywords from the accent, ≥ 4.5:1 everywhere and 7:1 in high contrast), an opt-in styles/syntax.css for CodeMirror 6, Prism, highlight.js and Shiki's css-variables theme, and monacoTheme, textMateTheme and editorColors for editors that take a theme object. No editor dependency.
  • FileDrop: target="window" takes files dropped anywhere in the window, with a full-page overlay while they are dragged in.
  • First release of @arshad-shah/cynosure: one small, themeable, accessible React UI package (tokens, foundations, controls, structure, overlays and feedback components, Facet icons and brand marks), replacing the old @arshad-shah/cynosure-* packages.
  • Icons and tokens for every product: 56 new Facet icons (with a new maths category), 97 official brand marks in @arshad-shah/cynosure/brands (drawn exactly as their owners publish them, color="brand" for their own colour), iconToSvg for pages without React, and a React-free @arshad-shah/cynosure/tokens entry (createTheme, themeToCss, themeInitScript).
  • KeyValueEditor: allowFiles lets a row's value be a file, for multipart form fields.
  • PanZoomCanvas: renderScreen draws a layer in screen pixels under the content, for engines that paint into a <canvas>. CanvasScreen is exported.
  • What the product migrations asked for, as props on existing components: Badge color and dot (steady or "live"), ProgressBar segments, offset and marker, Button href, confirm and keepFocus, TextField revealable, ThemeToggle variant="cycle", Card actions, CodeBlock activeLines, Select option groups, Tree onAction and revealing an outside selection, and the useDrag and useControllableState hooks.
  • SelectionFrame: draw draws a new box by dragging on the layer (region select), and box may be null before one exists. Enter places a box for keyboard users.
  • Slider: any number of thumbs (value may hold more than two numbers), and trackContent for a custom-drawn track. SliderValue widens to number | number[].
  • SplitPane unit="px" for fixed-size panes, Popover anchor (a point or any element, with trigger optional), and NumberField scrub (drag the label to change the value).
  • VirtualList: wrap (measured rows of any height), follow (stays at the end as rows arrive), isHeader (pinned section headings) and handle (scrollToIndex, scrollToEnd). VirtualListHandle is exported, and moveItems now lives in a shared foundation.
Getting started
Theming
Icons
Accessibility
Code editors and highlighters
Using Cynosure without React
Build-tool adapters
Components
Accordion
AppShell
AspectRatio
Breadcrumbs
Calendar
Card
Container
Disclosure
Divider
DocumentViewport
Grid
Inline
PageHeader
PanZoomCanvas
ScrollArea
SideNav
SortableList
SplitPane
Stack
StatusBar
Tabs
TitleBar
Toolbar
Tree
VirtualList
ZoomControls
Announcer
Avatar
Badge
CodeBlock
DescriptionList
EmptyState
ErrorBoundary
Field
Heading
IconTile
Image
Kbd
List
LoadingDots
Notice
ProgressBar
Prose
Skeleton
Stat
Steps
Table
Text
Button
Checkbox
Checklist
ColorField
ColorSwatches
Combobox
CompareSlider
CopyButton
DateField
FileDrop
IconButton
KeyValueEditor
Link
NumberField
Pagination
PathField
RadioGroup
SearchField
SegmentedControl
Select
SelectionFrame
SignaturePad
Slider
Switch
TagInput
TextArea
TextField
ThemeToggle
TimeField
ButtonBase
ChoiceBase
DialogBase
FileInputBase
HiddenValue
Icon
InputBase
LinkBase
LiveRegion
TextAreaBase
ChatMessage
Choice
DataGrid
DeviceFrame
HighlightedText
HotkeyField
ListRow
OptionList
OverlayFrame
QRCode
RowActions
Section
SiteFooter
SiteHeader
SkipLink
SortButton
VisuallyHidden
CommandPalette
ContextMenu
Dialog
FloatingToolbar
HoverCard
Menu
Menubar
Popover
ProgressOverlay
Sheet
Toast
Tooltip
Changelog