@umriss-ui/core
v0.29.0
Published
React component library for data-dense dashboards and tools - forms, date pickers, overlays, a tree view and a command palette. Precise, quiet, professional.
Downloads
6,034
Maintainers
Readme
@umriss-ui/core
A React component library for data-dense dashboards, monitoring and tools: forms, date pickers, overlays, a tree view and a command palette. Guiding idea: precise and quiet, with palpable quality – depth comes from soft shadows and fine light edges, not from hard outlines or effects.
The design language — "Ink & Paper", the dark theme, motion — is written down
once for all five packages in ../../docs/design-language.md.
Use
pnpm add @umriss-ui/coreThe demo is the documentation: https://romanhaendler.github.io/umriss-ui/core/.
Every prop of a props table is linked to the examples that show it, 27 pages
open with a configurator, an API index lists every export, one search finds
across all five packages, and the header switches the examples to German.
For a coding agent the same material stands as one Markdown file inside the
installed package, docs/llms-full.md, pinned to that version — every page
with its examples' source, its props tables and why it is built as it is, and
an index of every export with its comment and its declaration; each page also
has a Markdown twin; online, for the latest version,
https://romanhaendler.github.io/umriss-ui/core/llms.txt.
In an application:
import { Button, Card } from "@umriss-ui/core";Nothing else is required.
Styles
They load themselves. dist/core.js imports its stylesheet, and the
manifest marks CSS as a side effect, so a bundler keeps it. The stylesheet stays
exported as @umriss-ui/core/styles.css for setups that link stylesheets by
hand; nobody has to import it.
They touch nothing else (ADR-0021). No rule selects html, body, * or a
native element of the page, and the only rules on :root declare the tokens.
Each component carries what it needs itself: its type, box-sizing on its own
elements, a focus ring on what it makes focusable. A caller's content inside
Stack or Grid keeps the caller's type.
Everything can be overridden. The library's rules lie in three cascade
layers, umriss.tokens, umriss.base and umriss.components, and CSS written
outside a layer wins over all of them, whatever the order of loading:
:root {
--u-font-sans: "Inter", system-ui, sans-serif;
--u-color-accent: #0f766e;
}Light and dark follow the application's color-scheme. Every token with
two values is written light-dark(<light>, <dark>). An application that
switches its mode with a theme library usually sets color-scheme already;
one with a switch of its own writes one line, which its native controls need
anyway:
html.dark { color-scheme: dark; }
:root { color-scheme: light dark; } /* or: follow the system */An application that sets nothing stays light. A part of a page can be dark on
its own (<aside style="color-scheme: dark">).
Fonts are the application's. The tokens name Geist first and end in the
system fonts; nothing is loaded. Geist is recommended — @fontsource/geist-sans
(400, 500, 600) and @fontsource/geist-mono (400) — and any other face is one
token away.
Browsers: Chrome 123, Firefox 120, Safari 17.5 or newer. light-dark() is
older nowhere; an older browser discards the tokens and shows the interface
unstyled.
Wording
English is the default wording. German ships as well, as the subpath export
@umriss-ui/core/wording/de, and an application takes it on purpose:
import { UmrissProvider } from "@umriss-ui/core";
import { GERMAN_WORDING } from "@umriss-ui/core/wording/de";
<UmrissProvider language={{ wording: GERMAN_WORDING }}>…</UmrissProvider>;Both objects are typed Wording, so a missing entry is a compile error rather
than a gap that shows up in the interface (ADR-0018, ADR-0019). An
UmrissProvider without language, nested for a toast or a density, keeps the
language around it.
The formats — dates, numbers, durations — are a seam of their own. The default
is English notation on a 24-hour clock (17/03/2026, 09:05, 1,234.5), and
the same subpath ships GERMAN_FORMATS beside the German wording, so a German
application takes both halves of its language in one import (ADR-0024):
import { GERMAN_FORMATS, GERMAN_WORDING } from "@umriss-ui/core/wording/de";
<UmrissProvider language={{ wording: GERMAN_WORDING, formats: GERMAN_FORMATS }}>…</UmrissProvider>;Components
The table and the alarm list are not part of this package. They live in
@umriss-ui/table:
useTable with columns as elements, Toolbar, Search, ColumnMenu, Export,
Pagination, VerdictColumn, AlarmList and alarmModel. That package takes
@umriss-ui/core as a peer and reads its provider, formats and wording.
| Component | Purpose |
| --- | --- |
| Button | variants primary/secondary/ghost/plain/danger, sizes, loading state |
| IconButton | a square button that shows only an icon; its required aria-label is the name and the tooltip |
| FormField / FormFieldBoundary | label, help text, error; wires id/aria-* automatically through context. FormFieldBoundary resets the context inside panels so that their contents do not inherit the trigger's field id |
| Textarea | multi-line input; grows with its content on request (autoGrow, maxRows), character counter (showCount) |
| RadioGroup | one out of a few, each option with an optional explanatory line; one tab stop, arrow keys select |
| SegmentedControl | one out of a few short options, drawn inset - a sunken track, the choice lifted out of it - or as a field; fill shares its place equally, an option takes an icon; a radio group to the keys and the screen reader |
| Alert | a message that stays; five tones, actions, closable on request – the role follows the tone |
| Tag / TagGroup | a removable label; one tab stop and arrow-key navigation within the group |
| Divider | a dividing line beside Stack and Grid: horizontal/vertical, two weights, optionally with a label |
| VisuallyHidden | text for the screen reader only; focusable turns it into a skip link |
| ButtonGroup / SplitButton | connected buttons; a main action plus variants through the existing menu |
| Text / Heading / Link | typography on the token scale; for Heading, level and size are independent |
| Input / Select / Checkbox | form elements; Input numeric in Geist Mono, Input clearable with a softly fading-in × button (clears without losing focus); Select a native <select> that opens the Combobox's list under a mouse and the keys and the system's picker under a finger (ADR-0043); Checkbox indeterminate; every field fills its place or is its natural width, never what it shows, and takes chars for a width in characters (ADR-0041) |
| Modal / ModalHeader / ModalBody / ModalFooter | dialogs built on <dialog>; free space is distributed in the golden ratio (38 : 62) above and below the surface – small modals sit in the upper third, long ones use the full height, and only the body scrolls while head and foot stay put |
| ConfirmDialog | a compact confirmation dialog, tone="danger" for destructive actions, loading state |
| ToastProvider / useToast | passing messages as a deck at one of six places: four tones and loading, one action, update in place, a close reason, a limit, Alt+T, aria-live |
| Menu / MenuItem / MenuSeparator | a dropdown menu with a portal panel, arrow-key navigation, tone="danger" |
| ContextMenu | the same menu opened at a point in the viewport - for a right-click on a surface that is not a button; controlled, focus returns to where it was |
| Tooltip | help text on hover and keyboard focus, inverted (ink surface), portal |
| Tabs / TabList / Tab / TabPanel | controlled tabs with an ink underline and arrow-key control |
| Skeleton | a loading placeholder (bar or circle) with a discreet pulse |
| EmptyState | an empty state with title, description, action and an optional symbol |
| Stack / Grid | layout primitives with token spacing (a 4 px step), Grid with a fixed count, a width per column (columns={[140, "fill"]}) or responsive via minItemWidth |
| DatePicker | calendar selection: weeks from Monday, arrow keys across the month, today/clear, the date in Geist Mono |
| DateTimePicker | date plus time: two-digit fields that advance automatically, visible up/down steppers and arrow-key counting; after the day is chosen the focus jumps into the time, withSeconds, "now"; the clock change is handled honestly – a missing hour (the start of summer time) is detected and corrected forwards with a note, a duplicated hour (the start of winter time) offers the choice between both occurrences including the UTC offset |
| Combobox | a searchable select field (listbox panel, arrow keys, aria-activedescendant), optionally clearable; Input's two sizes |
| MultiSelect | multiple selection: the field always stays on one line and measures the space (as many chips as fit, the rest as "+N"); chips are remove buttons (hover shows ×, a click deletes, backspace deletes the last, roving tabindex with arrow keys), and the "+N" counter opens the selected view directly; a panel with a search field (autofocus, Enter toggles the first hit), a view switch "all | selected (N)" (the list always in natural order, deselected rows stay visible in the selected view until the view changes), and all/none/invert acting on the filtered set of the active view; optionally clearable, a × that empties what "none" would, its room kept in the field's width; Input's two sizes |
| Card / CardHeader / CardBody | panels with an eyebrow, actions, optionally collapsible; flush for borderless tables; a dividing line under the head only through divider (for borderless data) – otherwise white space carries the hierarchy |
| Badge | a status label with a dot, five tones; slightly rounded by default, pill for counters |
| NumberInput | numeric input in the notation the formats seam provides (group separators on leaving the field; it reads back what it writes, whatever the notation), decimals (0 = whole numbers), min/max, arrow keys with Shift ×10, an integrated spinner column at the right inner edge, prefix/suffix adornments (€, %) |
| Meter | a narrow fill bar (0–1) with a mono percentage label, five tones – for utilisation in cells; label names what is being measured |
| ControlSizeProvider | one size for the controls in a place - a toolbar, a dense form: every control with size inside takes it unless it says its own. A popover, dialog or tooltip opened from there starts without it |
| UmrissProvider | one place to set things: density, portal target, toasts, language. Optional – without it everything behaves as it does unconfigured. It holds no theme (light and dark are the application's color-scheme) and writes nothing onto the document |
| LanguageProvider / useFormats / useWording | formatting and wording as an overridable seam; the wording is a directory of entries, not a translation call. English is the default, German ships as @umriss-ui/core/wording/de |
| Dock | a tool strip over one surface: four named resting places at the edges of the host, moved by the grip (dragging snaps, the four arrow keys are the four places), orientation follows the edge; translucent material like the palette; a place without room refuses visibly; mode marks at most one tool and belongs to the caller |
| DateRangePicker | a period in two clicks across two chained months: backwards is allowed (the ends swap silently), a preview band on hovering, quick choices from the wording; on a phone the months stand one below the other |
| DateTimeRangePicker | a period with a time at both ends: "all day" as the default, time fields that advance, Enter accepts; the clock change is handled per end |
| Popover | the one closable, anchored surface underneath Menu, Combobox and the pickers: portal (nearest <dialog>, then the portal target, then the body), position with flipping above and below and across its trigger's edges, a panel that fits nowhere cut to the larger side instead of laid over its trigger, clamping as the last resort, never larger than the window (it scrolls in itself beyond that), outside click, Escape with focus returned |
| TreeView / TreeSearch / useTree | a tree with exactly one active node and optional ticking (cascade, indeterminate, locked, unloaded); arrow keys, type to jump, range selection, virtualisation. The search keeps its term in the tree |
| Stat | a metric: one value, read against its limits – the verdict as a word and a colour, the deviation from the target value, history and freshness |
| CommandPalette / useCommandPaletteShortcut | a command palette: subsequence search with rank and highlighted finds, groups, keywords a candidate is found by, at most maxFinds finds with a line for the rest, a populated resting state; opens on Ctrl/⌘+K and / |
| CrossGlyph / PlusGlyph / MinusGlyph / AngleGlyph / CalendarGlyph / ClockGlyph / GripGlyph / GridGlyph / MeasureGlyph | the shared character set, one stroke width at one nominal size; specification in docs/glyphs.md |
| Sparkline | a miniature history line with an area gradient and an accent end point – for trends in cells |
| Spinner | a functional loading indicator |
| Switch | on or off, taking effect at once: a native checkbox under role="switch", the label beside it, two sizes, invalid through FormField; the thumb travels on the path transition |
| Slider | one value between two bounds on the native range input, drawn with tokens: min/max/step, marks with optional words, format for the mono readout and aria-valuetext; arrows, PageUp/PageDown by a tenth, Home/End - the same on every engine |
| Drawer | the Modal's dialog entering from an edge (side="right" \| "left"): focus trap, Escape and focus return from the browser, ModalHeader/ModalBody/ModalFooter inside, its width the token --u-drawer-width; modal only |
| ProgressBar | how far a task has come (role="progressbar"), determinate from 0 to 1 or indeterminate without a value; valueText for a count; no tone, because progress is no verdict - that is the Meter |
| Accordion / AccordionItem | sections behind headers that are buttons with aria-expanded: type="single" \| "multiple", controlled or uncontrolled, arrow keys between the headers; the height animates as the card's collapse does |
| Breadcrumb | where a page stands: <nav> with an ordered list, the last item aria-current="page", items as links or buttons (routing is the caller's); when narrow the middle levels fold into a Menu, measured rather than guessed |
| Splitter | two panes and the line between them (the APG window splitter): side by side or stacked, the first pane's share in per cent, controlled or uncontrolled, min/max; drag, the arrows of its axis, Home/End, Enter collapses and restores |
| Stepper | where a procedure stands: an ordered list of steps, done, current (aria-current="step"), upcoming or failed - each said as a word beside its label and drawn as a number, tick or cross; in a row or a column; moving on is the caller's |
| FileInput | the native file input behind a key, inside a zone that takes a drop: accept and multiple hold for the drop as for the dialog, the chosen files listed with their size and a cross each, controlled or uncontrolled, invalid through FormField; no upload |
Principles for new components
- Pass
forwardRef,classNameand...restthrough to the root element – every component that renders a root element. Excepted are the wrappers that have none:Popoverrenders into a portal,Tooltiparound the caller's child,Menu,ContextMenuandToastProviderare composed of other parts, and the providers render no element at all. The ref goes to the element a caller lays out: the<dialog>ofModal,Drawer,ConfirmDialogandCommandPalette, the field's wrapper of the pickers and the combobox family, therole="tree"list ofTreeView. The native fields that wear a wrapper (Checkbox,Switch,Slider,NumberInput,Select,FileInput,Input,Textarea) put the class and the style on the wrapper - what a caller dresses and sizes - and ref and rest on the control (ADR-0041). The pickers and the combobox family take a caller'saria-label,aria-labelledbyandaria-describedbyout of the rest and put them on the field that has the focus. The component's ownrole, thearia-*it computes and its handlers are not replaced byrest: a caller's handler runs first and canpreventDefault. Held bytests-unit/passthrough.test.tsx, which renders every export. - Support controlled and uncontrolled use (
value/defaultValue). Deliberately controlled only:ComboboxandMultiSelect– the field keeps no second state beside the caller's – as well asModal,DrawerandCommandPalette, because opening is the caller's decision. - Keyboard operation and
ariaattributes are part of the definition of done. - No business logic: a mapping such as "status X is green" is the application's to make.
- Tokens only, no raw values. A new value is created as a token first. No
selector reaches beyond the component's own elements, and every rule lies in
a layer (ADR-0021; the guards in
tests-unit/stylesheets.test.tshold it). - Everything is English – identifiers, props and prose (ADR-0018, which
reversed ADR-0015). What still stands from ADR-0015 is the spelling of the
accessible name: it is
aria-labelwhere it names the root element, andariaLabelonly where it does not. - A field's root composes
extentfrom#own-styles: it fills the place that gives it a width, is its natural width where the place asks, and is never wider than its place; what it shows never moves it (ADR-0041). A field that shows text takescharsand sets its chrome; every control with two heights takessizeand reads it throughuseControlSize, so aControlSizeProviderreaches it, and a surface of its own resets it. Held bytests-unit/controlSize.test.tsxandtests-visual/features-sizes.spec.ts. Two exceptions, by design: theSegmentedControlstands among fields but shows no value, so it is as wide as its words (fit-content) and fills a place only when told to (fill); and theCheckboxhas one look and nosize- in a field it reads the place's size only for the height of the row it stands in (field-row-alignment).
Roadmap
The core scope is complete, and so are the command palette, Dock, the
danger text tone, the pass-through of rules 1 and 2 above, the six basic
components and the layout extras, forced colours, the listbox
announcements, and the fields' widths and sizes (ADR-0041). What is still open is one check a person makes: the listbox
announcements heard through VoiceOver (.scratch/listbox-announcements/,
ticket 02).
Collapsing the dock onto its grip stays out of scope, because it doubles the state space. What core will not build at all stands in ADR-0032.
