@danieljoffe/shared-ui
v0.6.0
Published
React component library and design system — Tailwind CSS 4, React 19, server-component-friendly.
Maintainers
Readme
@danieljoffe/shared-ui
A React component library built with Tailwind CSS 4. Provides 40+ accessible, theme-aware UI primitives designed for the danieljoffe.com ecosystem.
Live component catalog: ui.danieljoffe.com (Storybook)
Installation
npm install @danieljoffe/shared-uiPeer dependencies
| Package | Version | Required |
| -------------- | ----------- | --------------------------------- |
| react | ^19.0.0 | Yes |
| react-dom | ^19.0.0 | Yes |
| tailwindcss | ^4.0.0 | Yes |
| lucide-react | >=0.400.0 | Yes (icons in various components) |
Bundled dependencies
These are included in the package and do not need to be installed separately:
clsx— conditional class constructiontailwind-merge— Tailwind class deduplication
npm install react react-dom tailwindcss lucide-reactQuick start
import { Button } from '@danieljoffe/shared-ui';
// Or use deep imports for tree-shaking:
import { Button } from '@danieljoffe/shared-ui/Button';
export default function App() {
return (
<Button variant='primary' size='md'>
Get Started
</Button>
);
}Theme setup
Import the theme CSS to get design tokens (colors, spacing, typography, shadows, animations):
/* In your global CSS or Tailwind entry point */
@import '@danieljoffe/shared-ui/styles/theme.css';The theme file is self-contained with no external imports. It uses Tailwind CSS 4's @theme directive so every token is available as both a CSS custom property and a Tailwind utility class (e.g., bg-brand-500, text-text-secondary, shadow-md).
oklch color space -- Brand colors use
oklch()for perceptually uniform lightness across the scale. Semantic surface/text/status colors use hex orrgbafor maximum browser compatibility.
Consumer setup (Tailwind CSS 4)
/* app/globals.css */
@import 'tailwindcss';
@import '@danieljoffe/shared-ui/styles/theme.css';That single import gives you every token below. No tailwind.config changes are needed in Tailwind CSS 4 -- the @theme directive registers tokens automatically.
Integration paths
| Approach | When to use | How |
| ---------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Full adoption | New apps built on shared-ui | Import theme.css as shown above. All tokens, base styles, dark mode, and keyframes are active. |
| Selective adoption | Existing apps with their own theme | Copy individual @theme token groups into your own CSS. Or import theme.css and override specific variables in a subsequent @theme block. |
To override tokens selectively:
@import '@danieljoffe/shared-ui/styles/theme.css';
@theme {
--color-brand-500: oklch(0.6 0.2 280); /* shift brand toward purple */
--font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
}Full token reference
Radius
| Token | Value | Tailwind class |
| --------------- | ---------- | -------------- |
| --radius-sm | 0.375rem | rounded-sm |
| --radius-md | 0.5rem | rounded-md |
| --radius-lg | 0.75rem | rounded-lg |
| --radius-xl | 1rem | rounded-xl |
| --radius-full | 9999px | rounded-full |
Typography
| Token | Value | Tailwind class |
| ------------- | ----------------------------------------------- | -------------- |
| --font-sans | 'Inter', ui-sans-serif, system-ui, sans-serif | font-sans |
| --font-mono | 'JetBrains Mono', ui-monospace, monospace | font-mono |
Shadows
| Token | Light mode | Dark mode | Tailwind class |
| ------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------- |
| --shadow-xs | 0 1px 2px rgba(0,0,0,0.04) | 0 1px 2px rgba(0,0,0,0.2) | shadow-xs |
| --shadow-sm | 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04) | 0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2) | shadow-sm |
| --shadow-md | 0 4px 6px -1px rgba(0,0,0,0.07), 0 2px 4px -2px rgba(0,0,0,0.05) | 0 4px 6px -1px rgba(0,0,0,0.3), 0 2px 4px -2px rgba(0,0,0,0.2) | shadow-md |
| --shadow-lg | 0 10px 15px -3px rgba(0,0,0,0.08), 0 4px 6px -4px rgba(0,0,0,0.04) | 0 10px 15px -3px rgba(0,0,0,0.4), 0 4px 6px -4px rgba(0,0,0,0.2) | shadow-lg |
| --shadow-xl | 0 20px 25px -5px rgba(0,0,0,0.08), 0 8px 10px -6px rgba(0,0,0,0.04) | 0 20px 25px -5px rgba(0,0,0,0.4), 0 8px 10px -6px rgba(0,0,0,0.2) | shadow-xl |
Animations
| Token | Value | Tailwind class |
| -------------------------- | --------------------------------------- | ------------------------ |
| --animate-fade-in | fade-in 0.2s ease-out | animate-fade-in |
| --animate-slide-up | slide-up 0.2s ease-out | animate-slide-up |
| --animate-slide-down | slide-down 0.15s ease-out | animate-slide-down |
| --animate-slide-out-down | slide-out-down 0.15s ease-in forwards | animate-slide-out-down |
| --animate-scale-in | scale-in 0.15s ease-out | animate-scale-in |
Additional keyframes defined (usable in custom animations): bounceSubtle, pulseSlow.
Colors -- Brand (blue-indigo, oklch hue 250)
| Token | Value | Tailwind class |
| ------------------- | ---------------------- | ------------------------------------ |
| --color-brand-50 | oklch(0.97 0.01 250) | bg-brand-50, text-brand-50, etc. |
| --color-brand-100 | oklch(0.93 0.03 250) | bg-brand-100 |
| --color-brand-200 | oklch(0.87 0.06 250) | bg-brand-200 |
| --color-brand-300 | oklch(0.78 0.1 250) | bg-brand-300 |
| --color-brand-400 | oklch(0.68 0.15 250) | bg-brand-400 |
| --color-brand-500 | oklch(0.54 0.19 250) | bg-brand-500 |
| --color-brand-600 | oklch(0.5 0.19 250) | bg-brand-600 |
| --color-brand-700 | oklch(0.43 0.17 250) | bg-brand-700 |
| --color-brand-800 | oklch(0.37 0.14 250) | bg-brand-800 |
| --color-brand-900 | oklch(0.3 0.1 250) | bg-brand-900 |
| --color-brand-950 | oklch(0.22 0.08 250) | bg-brand-950 |
Colors -- Semantic surface
| Token | Light | Dark | Tailwind class |
| --------------------------- | ----------------- | ----------------- | ---------------------- |
| --color-surface | #ffffff | #0f1117 | bg-surface |
| --color-surface-secondary | #f9fafb | #161922 | bg-surface-secondary |
| --color-surface-tertiary | #f3f4f6 | #1e2130 | bg-surface-tertiary |
| --color-surface-elevated | #ffffff | #1a1d2b | bg-surface-elevated |
| --color-surface-overlay | rgba(0,0,0,0.5) | rgba(0,0,0,0.7) | bg-surface-overlay |
Colors -- Semantic border
| Token | Light | Dark | Tailwind class |
| -------------------------- | ---------------------- | --------- | ------------------------- |
| --color-border | #e5e7eb | #2a2d3a | border-border |
| --color-border-secondary | #d1d5db | #3a3d4a | border-border-secondary |
| --color-border-focus | oklch(0.54 0.19 250) | (same) | outline-border-focus |
Colors -- Semantic text
| Token | Light | Dark | Tailwind class |
| ------------------------ | --------------------- | --------- | --------------------- |
| --color-text-primary | #111827 | #f1f5f9 | text-text-primary |
| --color-text-secondary | #6b7280 | #94a3b8 | text-text-secondary |
| --color-text-tertiary | #9ca3af | #64748b | text-text-tertiary |
| --color-text-inverse | #ffffff | #0f1117 | text-text-inverse |
| --color-text-brand | oklch(0.5 0.19 250) | (same) | text-text-brand |
Colors -- Status
| Token | Light | Dark | Tailwind class |
| ----------------------- | --------- | --------- | ---------------------------- |
| --color-success | #10b981 | (same) | text-success, bg-success |
| --color-success-light | #ecfdf5 | #052e16 | bg-success-light |
| --color-warning | #f59e0b | (same) | text-warning, bg-warning |
| --color-warning-light | #fffbeb | #451a03 | bg-warning-light |
| --color-error | #ef4444 | (same) | text-error, bg-error |
| --color-error-light | #fef2f2 | #450a0a | bg-error-light |
| --color-info | #2563eb | (same) | text-info, bg-info |
| --color-info-light | #eff6ff | #172554 | bg-info-light |
Dark mode
Dark mode is handled via .dark class overrides in the same theme file. The ThemeProvider component toggles the .dark class on <html>. The theme file also registers a custom variant:
@custom-variant dark (&:is(.dark *));This means you can use dark: prefix in Tailwind classes (e.g., dark:bg-surface-secondary) and they will activate when .dark is present on an ancestor element.
Base styles
The theme file includes an @layer base block that sets:
- Default border and outline colors on all elements
- Body background, text color, font-family, and line-height
- Heading typography (h1-h6) with balanced text wrapping and tight letter-spacing
- Code element styling using
font-monowith brand-colored background - Table and list resets
- Scrollbar styling (WebKit)
prefers-reduced-motionsupport (disables animations automatically)
Component catalog
The tables below are a high-level index. For authoritative, always-current props — types, defaults, and descriptions generated from the source — open a component in the Storybook catalog; its autodocs can't drift. See also the Guides section there: Reach for a Primitive, Extend, Don't Reimplement, and Accessibility.
Typography
| Component | Description | Key props |
| ----------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Heading | Semantic headings with predefined variants | variant: hero, detail, subtitle, section, cardTitle, component, mdxH1-mdxH4. as: override element (h1-h6, p) |
| Text | Body text with semantic variants | variant: body, bodyLg, subtitle, cardDescription, detail, label, meta, caption, helper, error. as: override element (p, span, div, label) |
Layout
| Component | Description | Key props |
| ----------------------- | ------------------------------------------ | ----------------------------------------------------------------- |
| Container | Max-width centered container | size: sm, md, lg, xl, full |
| Grid / GridItem | CSS Grid wrapper | cols, gap, responsive |
| PageContainer | Page-level wrapper with consistent padding | as, size, wrapperClassName |
| PageLayout | Full page layout with sections | children (a <main> shell; Section owns spacing via contain) |
| Section | Semantic section with spacing | id, className |
| SectionLabel | Section header with icon + label | icon, label |
| Spacer | Vertical/horizontal spacing | size: xs-xl |
| Stack | Flex stack (vertical/horizontal) | direction, gap, align, justify |
| Sidebar | Navigation sidebar | items, activeId, onSelect |
Data display
| Component | Description | Key props |
| ---------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------- |
| Avatar | User avatar with fallback | src, alt, size |
| Badge | Status/category label | variant: default, brand, brand-solid, success, warning, error, info |
| Breadcrumb | Navigation breadcrumb trail | items: BreadcrumbItem[] |
| Card / CardHeader / CardTitle / CardContent / CardFooter | Composable card | Standard card sections |
| CTACard | Call-to-action card with heading and action | title, description, action |
| Divider | Horizontal rule with optional label | label |
| Kbd | Keyboard shortcut display | children (key text) |
| Pagination | Page navigation controls | currentPage, totalPages, onPageChange |
| ProgressBar | Progress indicator | value, max, variant |
| Skeleton | Loading placeholder | width, height, variant: text, circular, rectangular |
| StatsCard | Metric display card | label, value, subtitle, trend |
| StructuredData | JSON-LD script injection | data |
| Table | Semantic data table with keyboard-navigable rows | columns: Column<T>[], data, onRowClick, striped, caption |
| Tabs | Tab navigation | tabs: Tab[], activeTab, onTabChange |
| Tooltip | Hover tooltip | content, position |
Forms
| Component | Description | Key props |
| ------------------ | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Button | Button with variants and sizes | variant emphasis (bare/primary/secondary/outline) + status (error/warning/success/info). size, loading, as/href |
| Checkbox | Checkbox input | checked, onChange, label |
| FormFieldError | Validation error message | message, id (for aria-describedby) |
| Input | Text input with label/error | label, error, helper |
| Select | Select dropdown | options: SelectOption[], label, error |
| Switch | Toggle switch | checked, onChange, label |
| Textarea | Multi-line text input | label, error, helper |
Feedback
| Component | Description | Key props |
| -------------------------------- | ------------------------- | --------------------------------------------------------------------- |
| Alert | Contextual alert banner | variant: info, success, warning, error. title, children |
| Dropdown | Action menu | items: DropdownItem[], trigger |
| Loading | Full-area loading spinner | message |
| Modal | Dialog overlay | open, onClose, title |
| Spinner | Inline loading indicator | size: sm, md, lg |
| ToastProvider / useToast | Toast notification system | toast({ variant, title, description }) |
Theming
| Component | Description | Key props |
| -------------------------------- | --------------------------------------- | ------------------------------------------ |
| ThemeProvider / useTheme | Theme context provider | Wraps app, provides theme, toggleTheme |
| ThemeToggle | Light / dark / system segmented control | Uses useTheme internally |
Decorative
| Component | Description | Key props |
| --------------- | ---------------------------- | ----------- |
| AspectRatio | Fixed aspect ratio container | ratio |
| GridBg | Decorative grid background | className |
Utilities
| Export | Description |
| ----------------------------- | -------------------------------------------------------- |
| cn(...classes) | Tailwind-aware class merging (clsx + tailwind-merge) |
| validateProps(props, rules) | Runtime prop validation helper |
| ErrorBoundary | React error boundary component |
| ModalErrorBoundary | Error boundary scoped to modals |
TypeScript
All components export their prop types (e.g., ButtonProps, AlertProps). The library is built with exactOptionalPropertyTypes compatibility.
import type { ButtonProps, ButtonVariant } from '@danieljoffe/shared-ui';Tree-shaking
Use deep imports for optimal bundle size:
// Only bundles Button code:
import { Button } from '@danieljoffe/shared-ui/Button';
// Bundles everything (barrel import):
import { Button } from '@danieljoffe/shared-ui';React 19 ref pattern
Components accept ref as a regular prop — forwardRef is not used. This is the React 19 pattern:
import { useRef } from 'react';
import { Input } from '@danieljoffe/shared-ui/Input';
function MyForm() {
const inputRef = useRef<HTMLInputElement>(null);
return <Input ref={inputRef} label='Name' />;
}Constraints
- No Next.js APIs: This library depends only on React and Tailwind CSS. Components requiring
next/link,next/image,useRouter, etc. belong in the consuming app'scomponents/kit/directory. - No framework-specific code: Components work in any React 19+ environment with Tailwind CSS 4.
Linked development
By default, import { Button } from '@danieljoffe/shared-ui' resolves to the compiled dist/. That's right for production, but iterating on the library from a consumer would otherwise mean a build-version-publish-bump cycle just to see a change render. This package ships a source export condition so you can skip that loop: import the TypeScript source directly, edit a component, see it live in your app, and publish once at the end.
Every export declares a @danieljoffe.com/source condition pointing at src/:
// libs/shared/ui/package.json
"exports": {
".": {
"@danieljoffe.com/source": "./src/index.ts", // ← linked dev: raw source
"types": "./dist/index.d.ts",
"import": "./dist/index.js" // ← published default: built dist
}
}Inside this monorepo it is already wired — apps opt into the condition and resolve shared-ui straight from libs/shared/ui/src with no build step, so editing a component hot-reloads the app.
From a separate repo, put the library's source on disk beside your app (a workspace entry, pnpm link, or a plain checkout), then opt your toolchain into the condition:
TypeScript — resolve types from source:
// tsconfig.json { "compilerOptions": { "customConditions": ["@danieljoffe.com/source"] } }Next.js (webpack) — prepend the condition so imports resolve to
src/:// next.config.mjs webpack: (config) => { config.resolve.conditionNames = [ '@danieljoffe.com/source', ...(config.resolve.conditionNames ?? ['import', 'require', 'default']), ]; return config; },Other bundlers — add
@danieljoffe.com/sourceto the resolver's condition list (e.g. Vite'sresolve.conditions), ahead ofimport/default.
With the condition active, @danieljoffe/shared-ui imports resolve to the library's TypeScript source; edits show up live. Leave the condition unset in production builds so they always resolve the compiled dist/.
Development
# Run unit tests
pnpm nx test @danieljoffe/shared-ui
# Start Storybook
pnpm nx storybook @danieljoffe/shared-ui
# Build library
pnpm nx build @danieljoffe/shared-uiContributing
This library lives in the danieljoffe.com monorepo at libs/shared/ui/. See the root CLAUDE.md for conventions around the Rule of Three, component patterns, and the shared-ui boundary.
License
FSL-1.1-MIT — Functional Source License 1.1 with MIT Future License.
The Functional Source License grants permission to use, copy, modify, and redistribute the software for any purpose except a Competing Use (a commercial product or service that substitutes for this library or anything we offer using it). Two years after each release, that release automatically re-licenses under the MIT License. Read the LICENSE for the full terms.
Practical translation: you can use, fork, and self-host this library freely for personal, internal, educational, or non-competing-commercial work. If you'd like to use it inside a competing commercial offering, open an issue — there's almost certainly a path that works for both of us.