@deriv-com/dds-product
v0.3.1
Published
Deriv Design System — components and tokens for product apps (trading, banking, dashboards).
Maintainers
Readme
@deriv-com/dds-product
Components and design tokens from the Deriv Design System, for product apps — trading, banking, dashboards.
Working with an AI agent? Point it at AGENTS.md in this package. It is
the short version of everything below plus one line per component, and it says which of the
.llm/ documents to open and when.
Install
npm install @deriv-com/dds-productReact 19 is a peer dependency, so install it alongside if your app does not already have it:
npm install react react-domThe Quill icon set comes with the package — there is nothing else to install.
Use
Import the stylesheet once, at the root of your app. It defines every token this package's components bind to — without it they render unstyled.
import '@deriv-com/dds-product/product.css'Then import components by name:
import { Button, TextInput, InlineAlert } from '@deriv-com/dds-product'
export function SignIn() {
return (
<form>
<TextInput label="Email" type="email" />
<Button variant="primary">Sign in</Button>
</form>
)
}Each component brings its own stylesheet with it, so nothing else needs importing.
Icons
Every Quill icon is re-exported
from @deriv-com/dds-product/icons, under the name Quill gives it:
import { Icon } from '@deriv-com/dds-product'
import { CircleCheckSolidIcon } from '@deriv-com/dds-product/icons'
import { UsdIcon } from '@deriv-com/dds-product/icons/currency'
<Icon label="Verified"><CircleCheckSolidIcon /></Icon>Each Quill group has a subpath of its own — accounts, app-stores, currency, flag,
illustrative, markets, social, system and trade. Production builds tree-shake either
form to the icons you use; a dev server does not, so the group subpaths keep it from loading
all ~1,850 at once. Raw SVG files are not re-exported; for those, install
@deriv-com/quill-icons-web and use its svg/ folder.
An app that already imports from @deriv-com/quill-icons-web keeps working. It is the same
module, so the two can be mixed while you migrate — but drop the direct dependency when you
are done, or pin it to the range this package uses, so the app does not bundle two copies.
Fonts
The system is designed for Inter (all text) and
Red Hat Mono (code and tabular figures).
product.css names them but cannot carry them — if nothing loads the fonts, every role
silently falls back to the system typeface, and any weight the page is missing gets faked
by the browser. Pick one of the two paths:
Let the package supply them. One import next to product.css:
import '@deriv-com/dds-product/fonts.css'
import '@deriv-com/dds-product/product.css'This self-hosts both faces from the package (variable-weight WOFF2, subset per script,
~260 KB total across all scripts — a page only downloads the subsets its text uses). Both
fonts are under the SIL Open Font License; the licenses ship in dist/fonts/.
Load them yourself. If your app already hosts the fonts — next/font, Fontsource, or
your own @font-face — skip fonts.css and point the two family variables at what you
loaded:
:root {
--font-family-sans: var(--your-inter-variable), system-ui, sans-serif;
--font-family-mono: var(--your-mono-variable), ui-monospace, monospace;
}Every type role reads its family from these two variables, so this redirects the whole system at once. Load weights 400–700 for Inter (a variable font covers this in one file); a missing weight does not fall back visibly — the browser synthesises a faux bold instead.
Light and dark
product.css defines both themes. Dark is selected by data-theme="dark" on any
ancestor — usually <html>:
<html data-theme="dark">
<body>
<div data-dds-context="product"> <!-- dark, from here down -->The theme attribute and the context attribute are independent, and the theme may sit above the scope, on the same element, or below it. All three are covered by the stylesheet.
There is no provider and no second import.
Components that hold other components
Some components are a set and its members, and both halves are imported:
import { Select, Option, DropdownMenu, MenuItem } from '@deriv-com/dds-product'The same shape applies to RadioGroup/Radio, CheckboxGroup/Checkbox,
TabList/Tab, Accordion/AccordionItem, List/ListItem, Stepper/StepperItem,
FileUpload/FileItem and the SideNav family. Each parent's document says which child
it takes and what the child needs.
DocsNav is the same shape with a borrowed half: it takes DocsNavGroup for the levels
that fold, and SideNavItem and SideNavSection unchanged for the pages and the headings.
It is the page list for a documentation site — a SideNav is the product's own areas, and
the two differ at every point below that, starting with this one never collapsing to a rail.
Components that need mounting
Two do not work from the import alone.
Toast is raised from a queue rather than rendered in place. Mount the viewport once near
the root, then raise toasts from anywhere below it:
import { ToastViewport, useToast } from '@deriv-com/dds-product'Tooltip neither reveals nor places itself. TooltipAnchor wraps a trigger and its chip;
useTooltip is for a trigger a wrapper cannot hold:
import { TooltipAnchor, useTooltip } from '@deriv-com/dds-product'Both are covered in full in .llm/components/toast.md and .llm/components/tooltip.md.
Server Components
Every module that needs the browser carries "use client", so this package imports cleanly
from a React Server Component tree — including ToastViewport in a root layout.tsx, which
is where it belongs. Components that hold no state — Badge, Divider, Card — carry no
directive and render on the server.
Components that a client component distributes props to carry the directive as well, even
where they hold nothing themselves. List clones props onto its ListItem children, and a
ListItem rendered on the server would arrive as a payload with nowhere for those props to
land: the list would draw no dividers and leak divided to the DOM, with nothing said in
production. Both halves of every such pair sit on the same side of the boundary.
What remains yours: React does not let a Server Component pass a function across the
boundary, so a component you hand onChange or onClick to goes in a file of your own
carrying "use client". The error, when you forget, names your file.
Part classes
Every component exports the classes its parts carry, so a test selector or a wrapper's layout can name one without a string that goes stale:
import { buttonParts } from '@deriv-com/dds-product'
document.querySelector(`.${buttonParts.label}`)Use these rather than writing .dds-button__label by hand. Restyling a part is not what
they are for — the tokens are where a look is decided.
Accessibility
Roles, names, aria-* attributes and keyboard behaviour are the components' own, and the
ids they point at are generated per instance. Setting one from outside either says the same
thing twice or points at nothing, so pass the props that name a thing — label,
accessibleName, removeLabel — and leave the attributes alone. Each component's document
lists what it guarantees.
Documentation
AGENTS.md— the front door: setup, the rules that span components, and one line per component..llm/rules.md— the constraints no single component can state..llm/components/<name>.md— per component: every prop with the case it is for, its anatomy, what it guarantees, and snippets that compile..llm/tokens.md— the token vocabulary, for writing CSS of your own..llm/index.md— every component's full guidance and what to use instead of it.
Props also carry their guidance in the types, so an editor shows the case for a prop and each of its values without opening a file.
Versioning
Pre-1.0. Minor versions may contain breaking changes while the component surface settles; pin an exact version if that matters to you.
