NPM_PACKAGE
@soroush.tech/design-system
Emotion + @soroush.tech/styled-system React component library - token-driven light/dark themes, layout primitives, forms, and overlays.
$ npm i @soroush.tech/design-systemYour design system. Your tokens. Your rules. A fully typed React component library where nothing is hardcoded - every color, scale, and default belongs to you, not the package.
Tired of forking a component library just to change a color? Every token here is designed to be overridden - theming isn't an afterthought, it's the whole point.
Install
npm i @soroush.tech/design-system
react and react-dom are peer dependencies. Emotion is an internal implementation detail - it ships as a regular dependency, not a peer, so you never install it yourself.
Use it with AI tools
An MCP server serves this library's component inventory, every component's props reference, and the live token contract, so an AI assistant writes against the real API instead of guessing:
claude mcp add --transport http soroush https://mcp.soroush.design/mcp
It also runs locally over stdio (npx -y @soroush.design/mcp). Claude Code users can
install the plugin instead, which adds a house-style skill and a UI review agent on top
of the same server:
claude plugin marketplace add soroush-tech/design-system
claude plugin install soroush-design-system@soroush-tech
Any other tool can read docs.soroush.tech/llms.txt.
See @soroush.design/mcp for the full tool list.
Importing
Import components by subpath, styling primitives from the barrel:
import { Typography } from '@soroush.tech/design-system/Typography'
import { Flex } from '@soroush.tech/design-system/Flex'
Styling primitives (the engine) come from the barrel @soroush.tech/design-system - styled, css, keyframes, the Theme type, styled-system functions, and createShouldForwardProp. The barrel (src/index.ts) is the engine abstraction layer: to swap the CSS-in-JS engine, only that file changes.
import { styled, css, type Theme } from '@soroush.tech/design-system'
ThemeProvider and the theme-value hooks (useTheme, useDefaultProps, withTheme) live at their own subpath:
import { ThemeProvider, useTheme } from '@soroush.tech/design-system/theme'
Style-computation helpers (useStyle, withStyles, StylesConsumer) - resolving a StyleInput/StyleFactory into a CSSObject - live at their own subpath:
import { useStyle } from '@soroush.tech/design-system/style'
Raw engine primitives for building your own global styles - Global, globalStyles, and the SSR-only CacheProvider/styleCache pairing - live at their own subpath, not the barrel:
import { Global, globalStyles, CacheProvider, styleCache } from '@soroush.tech/design-system/engine'
Global (retyped against this package's Theme) applies app-wide CSS and should render inside ThemeProvider so it resolves against the active theme. globalStyles(theme) is the base reset this package owns - box-sizing, margin/table resets, and theme-driven body colors - compose it into your own Global styles array alongside your app's own concerns (font-family, webfont loading, and anything else app-specific stay entirely app policy).
The component catalog - every component as data, for tools that place and write components (a design tool's palette, a source writer) - lives at its own subpath and loads in plain Node (docs):
import { catalog } from '@soroush.tech/design-system/catalog'
SSR critical-CSS extraction (e.g. with @emotion/server) is a separate, opt-in concern most apps never need - that's what CacheProvider and its paired styleCache instance are for.
Under a strict Content Security Policy, bring your own cache with the response's nonce - createCache({ key: 'app', nonce }) from @emotion/cache, given to CacheProvider above ThemeProvider - and every style tag the engine inserts carries it. The shipped styleCache has no nonce.
The Emotion packages are regular dependencies of this package, not peers, on purpose: nothing of Emotion is on the public surface, so an app installs nothing extra and has no version to keep in step. An app that brings its own @emotion/react of another major gets two copies - the components keep working, as they style through their own copy and ThemeProvider - at the cost of the duplicate in the bundle and Emotion's one-time development warning.
Component inventory
Layout & surfaces
| Component | Purpose |
|---|---|
ScrollArea | A scrolling area with scrollbars drawn over its content, shown on hover |
View | Styled div primitive - the base building block |
Flex | View with flexbox defaults |
Grid | CSS grid container |
Paper | Elevated surface with background and shadow |
Card | Content container built on Paper |
Quote | Bordered quote / terminal-panel surface on View |
AppBar | Top application bar |
Drawer | Slide-in side panel |
Content & data display
| Component | Purpose |
|---|---|
Typography | Text with variant → element mapping - the reference implementation for all components |
Link | Anchor with theme-aware styling |
Icon | Icon renderer |
Image | Image with styled-system props |
Avatar | User/entity avatar |
Table | Compound data table - exports TableContainer, TableHead, TableBody, TableFooter, TableRow, TableCell, TablePagination, TablePaginationActions, TableSortLabel, and TableControl from @soroush.tech/design-system/Table |
Accordion | A heading that opens and closes a region - exports AccordionSummary, AccordionDetails, AccordionActions and AccordionGroup from @soroush.tech/design-system/Accordion |
Inputs & forms
| Component | Purpose |
|---|---|
Autocomplete | A field whose text finds an option in a list - one, several as chips, or free text |
Fab | A floating action button: the one main action of a page, circular or extended |
Button, ButtonGroup, ToggleButton | Actions and toggles |
TextInput | Text field |
Checkbox, Radio, Switch | Selection controls |
RangeInput | A slider - one value on a range, or two or more thumbs for a range of values |
TransferList | Two lists and the buttons that move the checked items from one to the other |
NumberInput | A field for a number - read in its locale, clamped to a range, stepped by keys or buttons |
Rating | A row of icons the user sets as a rating, or a read-only display of one |
NativeSelect | Low-level platform <select> - not recommended; use Select native |
Select | The select to use - custom listbox, or native for the platform picker |
MenuItem | Option row for Select's listbox, and a menu row in a MenuList |
Form, FormControl, FormLabel, FormHelperText | Form composition and labeling |
Pagination | Page navigation control |
Navigation & menus
| Component | Purpose |
|---|---|
NavTree | A tree of links to the pages of a site - groups of links, which can fold |
Breadcrumbs | The trail of links from a site's root to the current page - a long one collapses behind a button that shows it all |
Menu | A trigger and the list of commands it opens - exports MenuList (the role="menu" list that owns the keyboard), MenuDivider, MenuItems (rows from data) and ContextMenu (the same list, opened at the pointer by a right-click) from @soroush.tech/design-system/Menu |
Tabs | A row of tabs - exports Tab, TabContext (holds the value for a TabList and its panels), TabList (the Tabs of a TabContext), TabPanel (the content a tab shows) and TabScrollButton from @soroush.tech/design-system/Tabs |
Feedback
| Component | Purpose |
|---|---|
FabMenu | A fab that opens a stack of actions from the corner of a page |
Snackbar | A transient message in a corner of the window, with auto-hide and an action |
Alert | An inline message with a severity, an icon, a title and an action - exports AlertTitle from @soroush.tech/design-system/Alert |
Badge | A count or a dot anchored over another element |
Chip | A compact tag: a label, an icon or avatar, a click and a delete |
CircularProgress, LinearProgress | Determinate/indeterminate progress indicators |
Skeleton | Loading placeholder with pulse/wave animations |
Backdrop | Dimmed overlay behind modal surfaces |
Transitions
| Component | Purpose |
|---|---|
Collapse | Opens and closes its content by its height, or its width |
Fade, Grow, Slide, Zoom | Show and hide one child by opacity, scale, an edge or from nothing |
Overlay & behavior primitives
| Component | Purpose |
|---|---|
Tooltip | A short description of one element, shown on hover, keyboard focus and a long touch |
Portal | Renders children into a DOM node outside the parent hierarchy |
FocusTrap | Keeps focus inside a subtree |
Modal | Dialog primitive composing Portal, Backdrop, and FocusTrap |
Popover | Anchored floating surface built on Modal |
ThemeProvider | Supplies the theme and global styles to the app |
Theme tokens
Components never hardcode colors or sizes - props resolve against scales on the Theme object:
| Group | Scales |
|---|---|
| Color | palette (main/light/dark/contrastText per color) · text · background · border · colorScheme |
| Typography | typography (variant map) · fonts · fontSizes · fontWeights · lineHeights · letterSpacings |
| Space & shape | space · sizes · radii · borderWidths · shadows |
| Layout | breakpoints (xs/sm/md/lg/xl window classes) · layout (columns, margins, panels, top bar per class) |
| Component scales | avatar (size steps) · skeleton (wave highlight) |
| Layering & effects | zOrder (appBar/drawer/modal/snackbar/tooltip) · blur · logoFilter · portraitBlend · portraitOpacity |
Prop types are derived from the interface (keyof Theme['text']), so adding a token to themes.ts propagates everywhere automatically. See design-system.md for the full scale table and the palette rules.
Theming
The theme belongs to you - the package only ships defaults. This section is the overview; the full guides are docs/theming.md and docs/customization.md.
One theme, yours
ThemeProvider provides exactly one theme - the built-in dark theme when you pass nothing. Bring one theme, or as many as you like: switching between them is your state, not the provider's.
import { ThemeProvider } from '@soroush.tech/design-system/theme'
import { baseTheme, createTheme } from '@soroush.tech/design-system/theme'
// Zero-config: the built-in `baseTheme`.
<ThemeProvider>{app}</ThemeProvider>
// Your theme - written from scratch or extended from the base.
const brand = createTheme(baseTheme, { palette: { primary: { main: '#00ff88' } } })
<ThemeProvider theme={brand}>{app}</ThemeProvider>
// Mode switching is app policy - own the state and pass the active theme:
const [isDark, setIsDark] = useState(true)
<ThemeProvider theme={isDark ? brandDark : brandLight}>{app}</ThemeProvider>
createTheme(base, overrides) (exported from @soroush.tech/design-system/theme) is the merge primitive: plain objects recurse, arrays (shadows, fontSizes) and functions replace wholesale, undefined values are ignored - keys are added or replaced, never removed.
Breakpoints and layout
theme.breakpoints are Material 3's window classes, each a range from its start until the next:
xs 0-599px (compact, the base), sm 600-839 (medium), md 840-1199 (expanded), lg
1200-1599 (large), xl 1600+ (extra-large). Responsive props use them by name or position -
p={{ xs: 2, md: 4 }}, p={[2, 3, 4]} - and cqSm-style keys write container queries;
theme.breakpoints.up('md') and its siblings write queries for hand-written styles. Everything is
CSS, so pages work with JavaScript off. theme.layout holds the layout tokens per class (columns,
margins, gutters, side panels, top bar); Grid and Flex apply the column grid with
layout / layout={{ lg: { columns: 16 } }}, and apps place AppBar, Sidebar and Drawer
themselves. The full guide is the
Breakpoints and layout section of the theming guide.
Component defaults
Components carry visible literal fallbacks (size = 'md', variant = 'outside', ...) resolved through themeDefault(theme, key, fallback) - so every default, including behavioral variants, is overridable via the optional theme.defaults map or the provider's defaults prop:
| Key | Fallback | Drives |
|---|---|---|
size / compactSize | 'md' / 'sm' | sized components / dense table actions |
color / neutralColor | 'primary' / 'default' | accent controls / toggle controls |
textColor / accentTextColor | 'initial' / 'primary' | body text / icons + input labels |
bg / surfaceBg / inputBg | 'default'/'paper'/'terminal' | switch track / paper surfaces / inputs |
borderRadius / surfaceRadius | 'md' / 'sq' | grouped controls / surfaces |
avatarSize / borderColor / borderWidth | 'md' / 'primary' / 'thin' | avatars and rings |
iconSize | 'lg' | Icon default glyph size (theme.icon) |
inputMinWidth | '12rem' | least width of an input not full width |
buttonVariant / switchVariant | 'contained' / 'outside' | Button / Switch visual variants |
inputVariant | 'default' | TextInput, Select, NativeSelect frames |
avatarVariant / cardVariant | 'circular' / 'paper' | Avatar shape / Card treatment |
linkUnderline | 'always' | Link underline behavior |
paginationVariant / paginationShape | 'text' / 'circular' | Pagination items |
The built-in themes carry no defaults - the literals apply. A theme with entirely different size or palette keys stays valid by pointing the keys at its own tokens:
<ThemeProvider defaults={{ size: 'compact', color: 'brand', switchVariant: 'inside' }}>
Per-component customization (theme.components)
Customize one component for the whole app - default prop values, per-slot CSS, and new variant values - without wrapping or forking:
const brand = createTheme(baseTheme, {
components: {
Button: {
// Buttons default to sm + rounded; explicit props and ButtonGroup still win.
defaultProps: { size: 'sm', shape: 'rounded' },
// Per-slot CSS merged after Button's own styles - the theme wins the cascade,
// but per-instance props (m, p, width, ...) still beat the theme.
styleOverrides: {
root: ({ theme, ownerState }) => ({
letterSpacing: theme.letterSpacings.wide,
...(ownerState.variant === 'contained' && { textTransform: 'none' }),
}),
label: { fontStyle: 'italic' },
},
// A new variant value - register it first so it typechecks:
// declare module '@soroush.tech/design-system/theme' { interface ButtonVariants { dashed: true } }
variants: [
{
props: { variant: 'dashed' },
style: ({ theme }) => ({
backgroundColor: 'transparent',
border: `${theme.borderWidths.thin} dashed ${theme.border.primary}`,
}),
},
],
},
},
})
Style callbacks receive { theme, ownerState }, where ownerState is the styled root's resolved props (after group context, defaultProps, and theme.defaults). defaultProps sits in the standard precedence chain: explicit prop → group context → defaultProps → theme.defaults.* → the component's literal fallback. variants arrays are replaced wholesale by createTheme, never merged.
Your own components can join the same mechanism: create their roots with this package's styled(tag, { name: 'MyWidget', slot: 'root' }) and register the name by augmenting ThemeComponents. Zero-config themes pay nothing - the resolver bails out on the first check when theme.components is absent.
Extending tokens (declaration merging)
Every scale is an open interface declared on @soroush.tech/design-system/theme, so you can add palette colors, tokens, or whole scales - and every component prop union (color, bg, size, ...) widens automatically:
import type { PaletteEntry } from '@soroush.tech/design-system/theme'
declare module '@soroush.tech/design-system/theme' {
interface ThemePalette {
brand: PaletteEntry // new palette color
}
interface ThemeBackground {
tertiary: string // new background token
}
interface Theme {
elevations: Record<'low' | 'mid' | 'high', string> // whole new scale
}
}
Then supply the values with createTheme and use the new keys:
const brandDark = createTheme(baseTheme, {
palette: { brand: { main: '#00ff88', light: '#66ffb2', dark: '#00b25f', contrastText: '#000' } },
background: { tertiary: '#101418' },
elevations: { low: '0 1px 2px', mid: '0 2px 6px', high: '0 6px 18px' },
})
<ThemeProvider theme={brandDark}>
<Button color="brand" />
<View bg="tertiary" />
</ThemeProvider>
Notes: new object-valued keys (like a PaletteEntry) must be supplied complete - components read .main/.contrastText at runtime; and TypeScript cannot verify at runtime that an augmented token was actually supplied, so always pair an augmentation with the matching override.
Adding a component
Scaffold with the skill - it reads the live Typography files and design-system.md so output matches the codebase:
/new_theme_component ComponentName
Then work through the checklist at the bottom of design-system.md.
The library takes inspiration from Material UI - its component vocabulary and prop conventions will feel familiar - but it is written entirely in house. It is not a clone or a fork: every component is built from scratch on our own engine and token system, and the API is free to diverge wherever it serves its consumers better.
Release notes
Per-version notes for every published release live in
release-notes/.