dibk-design
v15.0.1
Published
Shared React component library for DIBK applications — 43 components, design tokens, and themes, published as the [`dibk-design`](https://www.npmjs.com/package/dibk-design) npm package.
Keywords
Readme
DIBK design
Shared React component library for DIBK applications — 43 components, design tokens, and themes, published as the dibk-design npm package.
Component documentation (Storybook): https://arkitektum.github.io/dibk-design/
Using the package
Install
npm install dibk-designPeer dependencies (must be installed in your app):
react18 or 19react-dom18 or 19react-router7 or 8
react-routeris needed becauseButton,StepandDropdownButtonrender a router link when given atoprop. Import it fromreact-router, notreact-router-dom: that package was removed in react-router 8 and is frozen at 7.18.2. On react-router 7 the two are interchangeable forLink, soreact-routeris the specifier that spans both majors.react-router 6 is not supported from 13.0.0 onwards, because
Linklives inreact-router-domthere. Use 12.x if you are still on 6.All three peers are required rather than optional,
react-routerincluded. The router import is static and top-level, so your bundler has to resolve it even if you never render a component that links anywhere.Your app and this library must resolve to one
react-routerinstance. Two instances break links in two different ways, and only one of them is loud:StepandDropdownButtonthrowuseHref() may be used only in the context of a <Router> component, because router context is per instance.Buttonfails silently. It identifies a<Link>child by comparing module identity, so aLinkfrom a second instance is not recognised and the button renders as a plain<button>with no navigation and no error.
The usual cause is a stray
react-router-domleft in your dependencies alongsidereact-router8. Check withnpm ls react-router(orpnpm why react-router) and expect a single version.Import the component styles
All compiled component styles ship in a single CSS file that your app must import once (e.g. in your global CSS or app entry):
@import "dibk-design/dibk-design.css";This pulls in
dibk-design/fonts.css(the PP Mori@font-facerules) for you. Import it directly only if you load the component styles some other way, or omit it if your app supplies its own copy of the font.Use the components
import { Button } from "dibk-design"; const Example = () => ( <Button onClick={() => {}} size="small" color="primary"> I'm a button </Button> );
Styles & tokens
In addition to dibk-design/dibk-design.css (component styles), the package ships:
dibk-design/theme.css— Tailwind-friendly theme tokens (via@theme)dibk-design/tokens.css— plain CSS variables for non-Tailwind appsdibk-design/fonts.css— the PP Mori@font-facerules, referencing the font files indibk-design/dist/fonts/. Already imported bydibk-design.css.
Use with Tailwind (recommended)
Import the theme tokens in your global CSS:
@import "tailwindcss";
@import "dibk-design/theme.css";
@import "dibk-design/dibk-design.css";Use without Tailwind
Import the plain tokens instead:
@import "dibk-design/tokens.css";
@import "dibk-design/dibk-design.css";Migrating
Two releases changed Select in ways that break working consumer code, and both shipped as
minor releases without a release note. Items 1 to 5 below cover those and the props since
restored. Item 6 is the one breaking change in 12.0.0, item 7 the one in 13.0.0, items 8 and 9
the two in 14.0.0, and items 10 to 12 what 15.0.0 changes. The greps are the fastest way to find
what needs changing. Full details in CHANGELOG.md.
1. Select onChange receives a value, not an event (9.1.0)
Before 9.1.0 the handler got the raw DOM event. It now gets the selected value.
// Before (≤ 9.0.2)
<Select onChange={(event) => setValue(event.target.value)} />
// After (≥ 9.1.0)
<Select onChange={(value) => setValue(value)} />Symptom: TypeError: Cannot read properties of undefined (reading 'value') as soon as a
selection is made.
# Every Select usage, to review its onChange
grep -rn "<Select" src
# Handlers still reading the event off a change callback
grep -rn "target\.value" src2. Select multiple onChange receives the whole selection (9.1.0)
This is the one to check most carefully — nothing crashes. Before 9.1.0, each click fired
onChange(option.value) with the single value that had just been toggled, and the consumer
merged it into their own state. It now computes the next selection itself and fires
onChange(nextValues) with the full array.
Consumer toggle logic therefore runs a second time on top of an already-correct array, and quietly corrupts state — a re-selected value gets removed, a deselected one gets added back.
// Before (≤ 9.0.2) — the consumer did the toggling
<Select
multiple
value={selected}
onChange={(toggled) =>
setSelected(
selected.includes(toggled)
? selected.filter((v) => v !== toggled)
: [...selected, toggled]
)
}
/>
// After (≥ 9.1.0) — just store what you are given
<Select multiple value={selected} onChange={setSelected} /># Every multiple Select, then read each onChange: if it inspects the argument
# with includes/filter/spread, it is still doing the toggling and is now wrong
grep -rn -A 8 "<Select" src | grep -B 8 "multiple"3. contentOnly was removed, and is back (10.3.2, 11.5.0, 12.0.0)
10.3.2 rewrote Select on top of react-select and deleted contentOnly from eight
components, so read-only views silently started rendering as editable controls.
All eight have it back, with the same behaviour as 10.1.1: the label, then a plain <span>
holding the content, with no form control in the DOM. Prefer it over disabled for view modes
— disabled greys the text out, implies "temporarily unavailable", and still renders a
focusable widget.
Select and InputField were restored in 11.5.0. Textarea, CheckBoxInput,
CheckBoxListItem, RadioButtonInput, RadioButtonListItem and DragAndDropFileInput
followed in 12.0.0.
Two things worth knowing if you are putting contentOnly back into use:
Textareakeeps the line breaks in a multi-line value, where the pre-10.3.2 markup collapsed them.CheckBoxInputshows a bare checkmark, whileRadioButtonInputshows no indicator at all. That is deliberate rather than an oversight: a read-only checkbox list renders every option and has to mark which are ticked, whereas a radio group has one answer, so a read-only view renders only the selected option and a dot beside it would be noise.
grep -rn "contentOnly" src4. keyAsContent and placeholderValue were silent no-ops (10.3.2–11.3.x)
Both stayed in SelectProps after the rewrite stopped reading them, so they type-checked and
did nothing. Both work again as of 11.5.0:
keyAsContent—contentOnlyonly. Shows the matching option'skeyinstead of the rawvalue. It has no meaning outsidecontentOnly.placeholderValue— the sentinel meaning "nothing selected". Avalueequal to it shows theplaceholder, and clearing the selection reports it throughonChange. Clearing a single select requires the newisClearable.
If you were relying on either between 10.3.2 and 11.3.x, your read-only views were rendering as editable controls and your sentinel was rendering as visible option text.
5. Select value types no longer have to match exactly (11.5.0)
Matching was options.find(o => o.value === value), so a string "5" against a numeric option
value 5 matched nothing and rendered the raw id as the visible option label. Matching now
falls back to comparing string forms, and warns in development when a non-empty value matches
no option at all. If you see that warning, the value and the option values have different
types.
6. DropdownButton's menu is no longer a list (12.0.0)
The menu was a <ul role="menu"> whose items each sat in an <li role="none">. It is now a
<div role="menu"> with the items as direct children. role="menu" replaces a list's
semantics outright, which is exactly why every item needed role="none" to suppress them
again, so the list was doing no work.
This only affects you if you style or query the dropdown's internals. Anything selecting by
role is unchanged, since role="menu" and role="menuitem" are both still there.
# CSS or queries reaching into the dropdown's list markup
grep -rn "menu li\|menu ul" src7. react-router replaces react-router-dom (13.0.0)
Link is now imported from react-router, and the peer dependency is react-router 7 or 8
instead of react-router-dom 6.4+ or 7.
On react-router 7 this is a one-line change to your package.json at most: react-router is
what react-router-dom depends on, so it is already installed. On react-router 6 it is a
blocker, because Link lives in react-router-dom there. Stay on 12.x until you can move to 7.
react-router-dom was removed in react-router 8 and is frozen at 7.18.2, so importing from it
capped every consumer at react-router 7. It was also worse than a cap: an app that installed
react-router 8 and kept react-router-dom around for this library ended up with two
react-router instances, and since router context is per instance, the library's Link threw
useHref() may be used only in the context of a <Router> component.
# Anywhere your app still imports from the removed package
grep -rn "react-router-dom" src8. NavigationBar no longer takes color (14.0.0)
The prop took "secondary" or "neutral". secondary painted the bar's links in the bar's own
background colour, which made the navigation unreadable, and no app used it. neutral was the
default and only restated the values already on the container.
// Before (≤ 13.1.0)
<NavigationBar themeId="dibk" color="neutral" links={links} />
// After (≥ 14.0.0)
<NavigationBar themeId="dibk" links={links} />Delete the prop. Nothing replaces it and nothing changes on screen — the default is what
neutral produced. To recolour the bar, set the --navigation-overlay, --navigation-text and
--navigation-surface custom properties in your own stylesheet.
Symptom: a TypeScript error on the prop. It is deliberate — the alternative was accepting it and ignoring it.
grep -rn "<NavigationBar" src9. ErrorMessage emits different markup (14.0.0)
The icon is an inline <svg> instead of an <img>, so it can take its colour from CSS, and the
message now sits inside an always-present aria-live region so screen readers actually announce
it. Both are fixes; both change the DOM.
<!-- Before (≤ 13.1.0) -->
<span aria-live="polite" class="…errorMessage"><img class="…errorSign">Message</span>
<!-- After (≥ 14.0.0) -->
<span class="…errorMessageRegion" aria-live="polite">
<span class="…errorMessage"><svg class="…errorSign"></svg>Message</span>
</span>errorMessage and errorSign still name the same things, so most CSS keeps working. Check two
shapes: selectors treating the icon as an image, and selectors assuming .errorMessage is a
direct or last child of the field. The region is rendered even when there is no error, so
:last-child on a field's children now matches it instead of the input.
This affects every component that shows a validation error — InputField, Textarea, Select
and DragAndDropFileInput — not just direct uses of ErrorMessage.
InfoBox's variant icon (.iconImage) and Step's finished-step checkmark (.checkmarkSymbol)
changed from <img> to inline <svg> for the same reason. Their class names and sizing rules
are unchanged; only selectors that assume an image break.
grep -rn "errorSign\|errorMessage\|iconImage\|checkmarkSymbol" src10. Button disabled now really disables the button (15.0.0)
This is the one to check most carefully — nothing crashes. disabled used to add a CSS class
and nothing else, so the button looked disabled while staying focusable and clickable, and
onClick still fired. It now sets the disabled attribute on the rendered element.
// Before (≤ 14.0.0): looked disabled, still ran the handler
<Button disabled onClick={submit} content="Send inn" />
// After (≥ 15.0.0): the handler does not run
<Button disabled onClick={submit} content="Send inn" />Symptom: a click that used to do something now does nothing. That is the fix working, but if any
of your code depended on it — a button disabled for appearance that still needed to be clickable
— drive it with your own state instead of disabled.
The same applies to DropdownButton, which passes disabled through to Button.
grep -rn "disabled" src | grep -i button11. inputValue is gone from the radio components (15.0.0)
Deprecated in 13.1.0 in favour of value, removed now. It only ever affected RadioButtonInput
and RadioButtonListItem; the two checkbox components always took value.
// Before (≤ 14.0.0)
<RadioButtonInput id="x" inputValue="value 1" onChange={onChange}>Ja</RadioButtonInput>
// After (≥ 15.0.0)
<RadioButtonInput id="x" value="value 1" onChange={onChange}>Ja</RadioButtonInput>Symptom: a TypeScript error on the prop. value is required on all four components now.
grep -rn "inputValue" src12. Two colour tokens changed value (15.0.0)
Both failed WCAG AA contrast. Nothing about the API changes, so this only matters if you hardcoded the old values somewhere to match the library.
| Token | Was | Now | Why |
| --- | --- | --- | --- |
| --color-error | #db0000 | #ad0000 | 3.83:1 as text on a checked list item |
| optional field marker | #828282 | #5c4d4a | 3.45:1 on the page background |
--color-error drives every error affordance, so error text, icons, the required asterisk,
error borders and the InfoBox and Badge error variants all shift together. Read the token
rather than the hex and this stays in step by itself.
grep -rni "db0000\|828282" srcUse with Next.js
- Import the CSS entrypoints in your
globals.css
@import "tailwindcss";
@import "dibk-design/theme.css";
@import "dibk-design/dibk-design.css";- Import components from the library
"use client";
import React from "react";
import { Button } from "dibk-design";
const Home = () => {
return (
<main>
<Button onClick={() => {}} size="small" color="primary">
I'm a button
</Button>
</main>
);
};
export default Home;Developing the library
This project uses pnpm as the package manager.
Install dependencies
pnpm installRun Storybook for development
To view and work on components in isolation, run the Storybook development server:
pnpm run storybookThis will open Storybook in your browser, usually at
http://localhost:6006.
Building for production
Build library
Type-checks, bundles the library (ES/CJS + a bundled index.d.ts), and copies the token files. The output is saved to the /dist folder.
pnpm run build:libBuild Storybook
Builds the Storybook as a static web application. The output is saved to the /storybook-static folder.
pnpm run build:storybookBuild everything
Builds both the library and the Storybook site:
pnpm run buildTesting local changes in a consuming app
Use yalc to test the built package in another app without publishing:
# In dibk-design — after making changes:
pnpm run build:lib && npx yalc push
# In the consuming app — first time only:
npx yalc add dibk-design