@sito/ui
v0.10.2
Published
Low-level React UI primitives for Sito packages and apps.
Readme
@sito/ui
Low-level React UI primitives for Sito packages and apps.
Install Styles
Styles are optional. Import the full stylesheet when you want the default theme and component styles:
import "@sito/ui/styles.css";Import only the theme variables when you want to provide component CSS yourself:
import "@sito/ui/theme.css";Public API
import {
Button,
BUTTON_COLOR_VARIANTS,
BUTTON_SIZES,
BUTTON_VARIANTS,
ContextMenu,
ContextMenuItem,
ContextMenuSeparator,
Dialog,
DialogActions,
DIALOG_INITIAL_FOCUS,
Icon,
IconButton,
ICON_BUTTON_SIZES,
Spinner,
Toast,
TOAST_COLORS,
TOAST_PLACEMENTS,
ToastProvider,
Tooltip,
useContextMenu,
useDialog,
useToast,
} from "@sito/ui";The runtime constants BUTTON_COLOR_VARIANTS, BUTTON_VARIANTS,
BUTTON_SIZES, ICON_BUTTON_SIZES, DIALOG_INITIAL_FOCUS, TOAST_COLORS,
and TOAST_PLACEMENTS expose the
supported values for public component contracts.
Exported types include ButtonProps, ButtonSize, IconButtonProps,
DialogProps, DialogActionsProps, DialogState, IconProps,
IconButtonSize, and
SpinnerProps, UseDialogReturn, ToastOptions, ToastFunction,
ToastProviderProps, plus the corresponding context-menu props and hook return
types.
Spinner
Spinner is the shared indeterminate-progress primitive used by Button
loading states and standalone feedback. Provide label when the spinner owns
the accessible loading announcement; omit it when surrounding content already
provides that context.
<Spinner label="Loading messages" />
<Spinner />Context Menu
ContextMenu owns viewport clamping, focus restoration, outside dismissal and
keyboard navigation. Consumers own the menu's actions and wording:
const menu = useContextMenu<string>();
<button
type="button"
onContextMenu={(event) => {
event.preventDefault();
menu.openAt(event.clientX, event.clientY, "item-id");
}}
>
Item
</button>
<ContextMenu
open={menu.open}
position={menu.position}
onClose={menu.close}
ariaLabel="Item actions"
>
<ContextMenuItem onClick={menu.close}>Open</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem disabled>Delete</ContextMenuItem>
</ContextMenu>;Apps with an existing overlay or hotkey scope can keep dismissal in that layer
with closeOnEscape={false}, closeOnTab={false} and
closeOnPointerDownOutside={false}. The component forwards its menu element ref
for adapters that need compatible positioning or containment checks. Those
adapters may also use clampToViewport={false} when their existing state layer
already owns clamping. Closing menus remain mounted for
--sito-ui-motion-duration-normal, restore focus to the trigger when closing
starts, and ignore pointer and keyboard input during the exit. The built-in
fade and scale motion is disabled when reduced motion is preferred.
Button Sizes
Button supports size="sm" | "md" | "lg" for padding and height. md is
the default size.
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>Icon
Icon renders any glyph (SVG, icon-font element, text) and blurs between
glyphs when it changes. IconButton renders its icon through Icon, so
toggles such as star/unstar animate with no extra code.
<Icon icon={<FontAwesomeIcon icon={starred ? faStar : faStarOutline} />} />
<IconButton aria-label="Star" icon={<StarIcon filled={starred} />} />A swap happens when the glyph's element type, key or props change (compared
deeply, so re-rendering the same icon does not animate). Pass iconKey (on
Icon or IconButton) to set the identity yourself: then only a new iconKey
animates. Icon is decorative (aria-hidden) unless it gets a label.
The old glyph fades out with blur while the new one fades in, over
--sito-ui-icon-swap-duration (200ms), blurring by --sito-ui-icon-swap-blur
(4px) and scaling from --sito-ui-icon-swap-scale (0.8). With
prefers-reduced-motion: reduce, the glyph swaps at once.
Icon Button Sizes
IconButton supports size="sm" | "md" | "lg" for the button container.
md is the default size.
<IconButton aria-label="Add" icon="+" size="sm" />
<IconButton aria-label="Add" icon="+" size="md" />
<IconButton aria-label="Add" icon="+" size="lg" />The container sizes are 28px, 40px, and 48px; their default icon sizes
are 16px, 20px, and 24px. Override a specific token
(--sito-ui-size-icon-button-icon-sm, -md, or -lg) for theme-wide changes,
or use iconSize / --sito-ui-icon-button-icon-size for one button.
Tooltip
Wrap a single trigger to show its visible label on pointer hover or keyboard focus. The tooltip is decorative and hidden from assistive technology, so keep an accessible name on icon-only controls:
<Tooltip label="Next slide" side="top" align="center">
<IconButton aria-label="Next slide" icon={<NextIcon />} />
</Tooltip>hotkey shows the control's keyboard shortcut after the label in a <kbd>
(<Tooltip label="Next slide" hotkey="→">). Like the label, it is hidden from
assistive technology: announce the shortcut with aria-keyshortcuts on the
trigger.
side supports top and bottom; align supports start, center, and
end. Hover is enabled only on devices that support hover. Press Escape to
dismiss it while the trigger is focused. Tooltip surface, text, radius, shadow,
font size, motion, and the hotkey's surface, border, and radius
(--sito-ui-tooltip-hotkey-surface, --sito-ui-tooltip-hotkey-border,
--sito-ui-radius-tooltip-hotkey) can be customized with --sito-ui-tooltip-*
tokens.
The tooltip stacks above context menus using --sito-ui-z-index-tooltip.
Toast
Wrap the app in ToastProvider once, then call useToast() anywhere below it:
<ToastProvider placement="bottom-right">
<App />
</ToastProvider>;
const toast = useToast();
const id = toast({ message: "Saving…", duration: 0, dismissible: false });
toast.update(id, { message: "Saved", color: "success", duration: 3000 });
toast({ message: "Not saved", color: "error" });
toast({
message: "Slide deleted",
action: { label: "Undo", onClick: restore },
});
toast.dismiss(id);color:default,success,error, orwarning.iconadds a leading icon.variantcan beaccent(default) orfilled; filled success, error and warning toasts use the matching--sito-ui-color-*-defaultbackground and--sito-ui-color-*-textforeground tokens. Toasts withcolor: "default"keep their surface appearance with either variant.durationin ms;0orInfinitykeeps the toast until it is dismissed. Defaults to the provider'sduration(5000), orInfinityfor errors.toast.update(id, …)(ortoast({ id, … })with an existing id) replaces the toast in place and restarts its timer.actionrenders a text button; it dismisses the toast afteronClickunlessdismiss: false.dismissible: falsehides the close button.- Provider props:
variant(accentby default orfilled),placement(top-left,top-center,top-right,bottom-left,bottom-center,bottom-right),duration,max(default3, the oldest leaves first),label(region name, default"Notifications"),closeLabel,closeIcon,viewportClassName,toastClassName, andportalContainer.
The viewport is a polite live region. Default, success and warning toasts use
role="status"; errors use role="alert". Hovering or focusing a toast pauses
its timer, and Escape dismisses the focused toast. Toasts animate in and out
and skip the animation with prefers-reduced-motion. Tune them with
--sito-ui-size-toast-width, --sito-ui-radius-toast,
--sito-ui-shadow-toast, --sito-ui-z-index-toast,
--sito-ui-toast-enter-duration, --sito-ui-toast-exit-duration, and
--sito-ui-toast-accent-width (defaults to four thin borders). To remove the
accent stripe with tokens, set its width to
var(--sito-ui-border-width-thin) and
--sito-ui-toast-accent-border: var(--sito-ui-color-border). The accent color
follows the --sito-ui-color-*-default tokens.
Motion
Buttons shrink while pressed and return to their normal size on release. The
press transition uses --sito-ui-motion-duration-press (100ms) and
--sito-ui-motion-easing-press; Button uses
--sito-ui-button-active-scale (0.97), while IconButton uses
--sito-ui-icon-button-active-scale (0.9). Override these tokens to tune the
feedback. With prefers-reduced-motion: reduce, the press scale and its
transition are disabled. The spinner keeps spinning at its reduced-motion
duration.
Pressable
Add sito-ui-pressable to any clickable element (cards, list rows, custom
controls) to get the same press feedback:
<div className="sito-ui-pressable" role="button" tabIndex={0}>
Open
</div>It scales to --sito-ui-pressable-active-scale (0.97) while :active,
skips :disabled and aria-disabled="true" elements, and turns off with
prefers-reduced-motion: reduce. It uses the standalone scale property, so
the element's own transform is kept. It sets transition; if the element
declares its own, add
scale var(--sito-ui-motion-duration-press) var(--sito-ui-motion-easing-press)
to it.
Buttons as Links
Button and IconButton render a native <button> by default. Use as to
render a link with the same classes, sizes, variants and colors:
import { Link } from "react-router";
<Button as="a" href="/report.pdf" download>Download PDF</Button>
<IconButton as="a" href="/docs" target="_blank" rel="noreferrer" aria-label="Docs" icon={<DocsIcon />} />
<IconButton as={Link} to="/" aria-label="Exit" icon={<ExitIcon />} />Props follow the rendered element: anchor attributes with as="a", the
component's props with as={Link}. Use links for navigation and downloads so
users keep new-tab, copy-link and the URL preview; keep <button> for actions.
typedefaults to"button"only for<button>.disabled(andloading) on a link setsaria-disabled="true",tabIndex={-1}and blocks click and middle-click navigation. Router links that skip navigation onevent.defaultPrevented(React Router does) stay put.refpoints to the rendered element.
Development
Run the component test suite with:
pnpm test:runStart Storybook with:
pnpm storybookDialog Layout
Dialogs are min(100%, var(--sito-ui-dialog-max-width)) wide, whatever their
content. The token defaults to 32rem; override it at :root or any theme
scope. mobileFullScreen still takes the full viewport on small screens.
When a dialog is taller than the viewport, it scrolls itself while the header
(title and close button) stays at the top and DialogActions stays at the
bottom. Keep DialogActions as the dialog's last content so it can stick.
Dialog Exit Transitions
Dialog fades in with a small rise and scale, then fades out when closed. It
stays mounted for the exit animation before restoring focus and releasing body
scroll. Set exitDurationMs to override the default 180 ms exit duration; 0
unmounts immediately:
<Dialog open={open} onClose={close} exitDurationMs={220}>
Dialog content
</Dialog>The --sito-ui-dialog-enter-duration, --sito-ui-dialog-exit-duration, and
--sito-ui-dialog-enter-transform theme tokens tune the built-in animation.
Setting both durations to 0ms disables it. Reduced-motion preferences disable
the animation and exit delay automatically.
The built-in animation uses unlayered CSS, so it overrides consumer animation
utilities on the dialog and backdrop. If you provide your own dialog animation,
remove those utilities or set both dialog duration tokens to 0ms. Focus is
restored after the exit animation only when focus remains in the closing dialog
or has fallen back to the document body.
The close button uses the neutral text color by default. Set
closeButtonColor="error" on Dialog when a red close button is preferred.
DialogActions accepts primaryProps and cancelProps to pass button props,
including as and link attributes. Omit cancelText to leave out the cancel
action:
<DialogActions
primaryText="Sign in"
primaryProps={{ as: Link, to: "/sign-in/" }}
/>Scope
This package only exposes generic primitives. It does not include dashboard
workflows, forms, translations, icon packs, routing, storage, app providers,
or data fetching. ToastProvider is the one provider, and it only holds the
toast queue.
