DBC React Components
Reusable React components for DBC projects.
This library provides a shared, themeable component system used across DBC’s internal (and selected external) applications. It is designed to promote consistency, speed up development, and improve overall quality and accessibility.
Purpose of the library
The goals of this component library are:
-
Consistency
Provide a unified look and feel across DBC’s digital products, especially internal tools. -
Development speed
Reduce development and maintenance time by reusing well-tested components instead of rebuilding UI from scratch. -
Quality & accessibility
Components are reviewed and built according to best practices, with accessibility and robustness in mind, ensuring a strong baseline quality. -
Reduced third-party dependency
Increase digital independence by building and sharing our own components across the organisation, reducing reliance on external NPM packages.
Getting started
1) Install the package
npm install @dbcdk/react-components
2) Import global styles
Important: The component library requires global styles to be imported once in your application.
import '@dbcdk/react-components/themes/dbc.css'
3) Add ThemeScript in your root layout (Next.js example)
The library's shared CSS foundation is still imported normally, but
product-theme switching is handled by ThemeScript + useTheme().
ThemeScript restores the persisted color scheme and product theme before
paint, so the app avoids a flash of the wrong theme on first load.
import { ReactNode } from 'react'
import { ThemeScript } from '@dbcdk/react-components'
import { TooltipProvider } from '@dbcdk/react-components/client'
import '@dbcdk/react-components/themes/dbc.css'
export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
return (
<html lang="da" suppressHydrationWarning>
<head>
<ThemeScript defaultTheme="dbc" />
</head>
<body>
<TooltipProvider>{children}</TooltipProvider>
</body>
</html>
)
}
4) Switching theme in your application
Example using useTheme() when you want to control the theme:
'use client'
import { AppHeader, Button, useTheme } from '@dbcdk/react-components/client'
import { Moon, Sun } from 'lucide-react'
export default function Header() {
const { theme, switchTheme } = useTheme({ defaultTheme: 'dbc' })
return (
<AppHeader>
<Button
variant="outlined"
onClick={() => switchTheme(theme === 'dbc' ? 'filmstriben' : 'dbc')}
>
{theme === 'dbc' ? <Moon /> : <Sun />}
</Button>
</AppHeader>
)
}
Light is the default color scheme, including before hydration. ThemeScript,
useColorScheme(), and the color scheme controls preserve saved Dark or System
preferences. To default to the OS preference explicitly, use
<ThemeScript defaultColorScheme="system" /> and useColorScheme('system').
The hook updates data-product-theme, applies the selected product theme's
token overrides, and persists the selection.
If you only need to read the active theme state, use useGetActiveTheme()
instead of useTheme():
'use client'
import { useGetActiveTheme } from '@dbcdk/react-components/client'
export function ThemeBadge() {
const { theme, colorScheme, themeKey } = useGetActiveTheme()
return <span>{themeKey ?? `${theme} / ${colorScheme}`}</span>
}
useGetActiveTheme() is intentionally read-only. It reports the current
active theme state as established by ThemeScript, persisted storage, or
another mounted theme control. themeKey is only set when both theme and
colorScheme are resolved, and is otherwise null. If your app passes a
custom storageKey/colorSchemeStorageKey (see below), pass the same values
here so it reads the right persisted state.
5) Running multiple apps on the same origin
useTheme, useColorScheme, useGetActiveTheme, ThemeScript,
ThemeButton/ThemeMenuSection, and ColorSchemeButton/ColorSchemeMenuSection
all persist to a cookie + localStorage under a shared default key. If two
different apps built on this library run on the same origin (e.g. two
products under one domain), they'll read and overwrite each other's theme
selection by default.
To keep them independent, give each app its own key via storageKey (product
theme) and/or colorSchemeStorageKey (color scheme, ThemeScript only —
elsewhere it's the storageKey option passed to useColorScheme):
<ThemeScript
defaultTheme="dbc"
storageKey="my-app-theme"
colorSchemeStorageKey="my-app-color-scheme"
/>
const { theme, switchTheme } = useTheme({ defaultTheme: 'dbc', storageKey: 'my-app-theme' })
const { colorScheme, switchColorScheme } = useColorScheme(undefined, {
storageKey: 'my-app-color-scheme',
})
Use the same keys everywhere in a given app (ThemeScript, the hooks, and
any switcher components), or the values will resolve inconsistently.
Package integration
The package targets bundlers (Next.js, Vite, webpack) and test runners with a CSS transform
(Vitest, Jest with a CSS Modules mapper). Every entry point - the main entry, /client,
/tanstack, /tanstack-virtual and /markdown - imports CSS Modules and ships unbundled files
with extensionless relative imports, so Node's own import/require can't load it directly.
Optional table integration
Table and useTableData are available from @dbcdk/react-components/client
without TanStack. Pass the same ColumnItem<T>[] to the table and the hook's
sorting.columns; the hook uses each column's accessor and optional
sortFunction. Use the column id as sortBy.
Existing accessor-shaped columns (accessorKey / accessorFn), including typed
TanStack columns, also work with the hook without introducing a TanStack dependency
into core declarations. Explicit columnComparators override column-derived sorting.
useTableSettings, TableSettings, and TableSettingsMenuSection are also
available from /client, together with their types and
localStorageTableSettingsStorage. They accept the normal Table's columns and
honor hidden and canHide. Existing TanStack columns with accessorKey,
meta.hidden, and enableHiding remain compatible.
Install @tanstack/react-table for TanstackTable from /tanstack. Existing
settings exports from /tanstack remain available as compatibility aliases.
Optional Markdown integration
MarkdownEditor and MarkdownViewer are available from
@dbcdk/react-components/markdown, not from the main entry or /client. They
need markdown-it, which is an optional peer dependency: install it
(npm i markdown-it) only if you use them. MarkdownViewer also works in
server components; MarkdownEditor is a client component.
Next.js configuration
Add transpilePackages: ['@dbcdk/react-components'] to your Next.js configuration
so it processes the library's CSS Modules. Import interactive components and
providers from /client. The packed-package integration check covers Next.js
16.2.6 with webpack and React 19.2.4.
Using Storybook
Storybook is the primary documentation and exploration tool for this library.
Local Storybook
npm run storybook
Storybook runs on http://localhost:6006.
In Storybook you can:
- Browse components in the left-hand navigation
- Open a story to see variants and states
- Adjust props via Controls
- Read guidelines and usage notes in the Docs tab
Themes
Use the 🎨 theme selector in the Storybook toolbar to switch between available themes (e.g. light and dark).
All components are styled using CSS variables defined in the theme files.
Accessibility (a11y)
Accessibility is a first-class concern in this library.
We aim to ensure that components:
- Are usable with keyboard navigation
- Have visible and consistent focus states
- Work with screen readers
- Follow common ARIA and semantic HTML best practices
Storybook includes the a11y addon to help identify issues during development.
MIME type helpers
These helpers work on both the server and client and are exported from the root and /client entrypoints:
import { formatMimeType, getMimeTypeExtension } from '@dbcdk/react-components'
formatMimeType('application/pdf') // 'PDF'
formatMimeType('text/plain; charset=utf-8') // 'Tekst'
getMimeTypeExtension('image/jpeg') // 'jpg'
formatMimeType provides Danish labels for common MIME types, preserves unknown values unchanged, and returns - for null or undefined. getMimeTypeExtension returns an extension without a dot for common image types and PDF, or undefined for unsupported types. Both ignore MIME parameters, casing, and surrounding whitespace when looking up known types.
Native selection controls
Use NativeSelect when you want the browser's native <select> interaction,
including the platform picker on phones. Select is the custom popover control.
Import interactive form controls from the client entrypoint:
'use client'
import { useState } from 'react'
import { NativeSelect } from '@dbcdk/react-components/client'
export function FormatFilter() {
const [format, setFormat] = useState('')
return (
<NativeSelect
label="Format"
name="format"
options={[
{ label: 'Alle formater', value: '' },
{ label: 'PDF', value: 'pdf' },
]}
selectedValue={format}
onChange={setFormat}
/>
)
}
NativeSelect includes InputContainer: pass labelAction, helpText, error,
orientation, or fullWidth directly rather than adding another label wrapper.
It is controlled and single-select: onChange receives the original option value
(string, number, or object with datakey), not a DOM event. A provided option with
value: '' is a real choice and replaces the synthetic placeholder. Otherwise,
selecting the placeholder invokes onClear, when provided.
Native select attributes and the element ref are forwarded, including name,
form, required, disabled, and onBlur. Values participate in FormData,
and required uses browser validation. allowClearWhenRequired allows clearing
but does not bypass that validation. Use MultiSelect for multiple selections.
The default medium text uses the 16px theme token. Coarse pointers get at least the 44px control-height token, while small/medium text uses the 16px token. This keeps mobile controls readable without replacing native interaction. Check the published Storybook on actual iOS Safari and Android Chrome when validating platform picker behavior; desktop emulation does not verify those pickers.
Contributing
See CONTRIBUTING.md for detailed guidelines on:
- Folder structure
- Styling and theming rules
- Storybook requirements
- TypeScript conventions
- Versioning and changesets
License
ISC
Group labels, heading sizes, and print content
These options are additive; existing defaults remain unchanged.
import {
CollapsibleHeadline,
Headline,
RadioButtonGroup,
} from '@dbcdk/react-components/client'
// The fieldset's legend stays above the controls. The existing div-based
// orientation/layout remains the default when `as` is omitted.
<RadioButtonGroup
as="fieldset"
label="Format"
direction="row"
options={[
{ label: 'PDF', value: 'pdf' },
{ label: 'Tekst', value: 'text' },
]}
value={format}
onChange={setFormat}
helpText="Vælg ét format."
/>
// An h3 with the theme's 24px/xl type token, without changing heading semantics.
<Headline size={3} textSize="xl">Dokumentoplysninger</Headline>
// Closed on screen initially; includes its content when printing.
<CollapsibleHeadline header="Flere oplysninger" defaultExpanded={false} printExpanded>
<p>Supplerende indhold.</p>
</CollapsibleHeadline>
Headline.textSize accepts xs, sm, md, lg, xl, and 2xl; omit it to
retain the existing responsive heading scale. It also works on
CollapsibleHeadline.
printExpanded keeps collapsed children mounted but hidden on screen, including
from keyboard navigation and the accessibility tree. Their state and effects
remain active. The default still unmounts collapsed children. Printing does not
change the expansion state or its persisted value. Any application print flow
must still include the component and its stylesheet.
RadioButtonGroup associates its label, error, and help text with the group in
both layouts. InputContainer.labelId is also available for custom controls
that need to reference their visible label with aria-labelledby.
For FAQ questions that must remain fully readable on narrow screens, use
<Accordion wrapHeaders items={items} mode="single" defaultOpenIndex={null} />.
Header wrapping is opt-in; existing truncation is unchanged. Closed panels are
inert and hidden from assistive technology, including after they have been opened.
Their existing animation and retained child state are preserved.
Use Accordion variant="plain" for a lightweight question list with separators
between rows and no enclosing border or background. headerWeight optionally
sets question weight (400, 500, 600, or 700); the existing default remains 400.
Accordion flushContent removes top and bottom padding from each item's content,
including the filled variant's open-panel top spacing. Horizontal content padding
and header padding are preserved for every size and variant. Like Tabs, this
option defaults to false, keeping existing spacing. Nested Accordions keep
their own content spacing, even inside a flush Accordion.
For reusable state policies or persistence, import useAccordionState from the
client entrypoint and let it own Accordion's controlled state:
'use client'
import { Accordion, useAccordionState } from '@dbcdk/react-components/client'
const items = [
{ id: 'general', header: 'Generelt', children: <p>Generelle oplysninger</p> },
{ id: 'details', header: 'Detaljer', children: <p>Flere oplysninger</p> },
]
export function Details() {
const { accordionProps, openAll, closeAll } = useAccordionState({
itemIds: items.map(item => item.id),
mode: 'multiple',
initialOpen: 'all',
storageKey: 'my-app-details', // Omit for in-memory state only.
})
return (
<>
<button onClick={openAll}>Åbn alle</button>
<button onClick={closeAll}>Luk alle</button>
<Accordion items={items} {...accordionProps} />
</>
)
}
itemIds must contain unique, non-empty, stable strings in the same order as
items. The hook rejects missing or duplicate IDs rather than falling back to
indexes. Accordion itself still supports items without IDs.
initialOpen is a mount-time default: 'none' (the default), 'all', or an array
of IDs. In single mode, only the first valid ID is opened; 'all' and openAll()
open the first item. closeAll() collapses everything. The result also exposes
openIds, setOpenIds(ids), and hydrated.
With storageKey, the hook uses the library's localStorage persistence and
restores after mount. Server rendering and the first client render use the same
initial state; hydrated signals when restoration is complete. Saved empty
arrays intentionally stay collapsed, malformed data falls back to the default,
and unavailable storage leaves state usable in memory. Only IDs are saved, so
reordering keeps the same sections open; IDs removed from the list are pruned.
Use a distinct key for each independent accordion.
When switching keys on a mounted hook, the new key is restored before writing
or pruning its saved IDs.
Callers can instead supply openIds and onOpenIdsChange for external control;
this disables all storage reads and writes. For persistence, let the hook own
state and pass its accordionProps to Accordion without overriding them with
independent controlled state. Accordion has no storageKey prop, and its existing
controlled/uncontrolled props and defaultOpenIndexes="all" remain unchanged.
In MarkdownEditor editOn="click", the Luk button beside Ren tekst
ends editing, keeps the current value, calls onEditEnd, and restores focus to
the edit button. Its text can be customized with labels.doneButton. The action
is also available from preview and plain-text mode; Enter in the textarea keeps
its normal newline behavior. On narrow screens, the switch and button occupy
their own row.