A complete reference for developers, contributors, and teams building with or on top of the Spring Design System. This guide covers how the library is architected, how to use it, how to extend it, and how to evaluate it for production at scale.
Table of Contents
- What Is This Library?
- Entry Points — How to Consume the Library
- How to Use Components
- React and Next.js Guidelines for New Users
- Why the Structure Works This Way
- How to Add Your Own Component
- How Production Builds Work
- The Agentic Workflow
- Building a Full Complex Repo With This Component Set
- What to Do and What Not to Do
- Production Readiness Evaluation
- Scalability Reflection
1. What Is This Library?
The Spring Design Library is a monorepo design system built by the Opportunity Generation team at Spring Financial. It ships:
- A published React component package —
spring-design-library-react(used internally as@spring/ds-react) — containing tokens, components, icons, design rules, and the AppFlow page registry. - A Next.js documentation gallery —
@spring/design-library— running at/(Getting Started), with top-level routes like/components/*,/design-tokens/*, and/page/*. This replaces Storybook as the visual source of truth. - An AppFlow package —
@spring/personal-loan-appflow— containing the 24-step personal loan application flow. - A standalone personal-loan app —
@spring/personal-loan— a production-style Next.js app that consumes the AppFlow package.
Stack: Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS 3.4
Workspace manager: npm workspaces (no Turborepo, no pnpm)
Registry: Gemfury (npm.fury.io/springfinancial/)
spring-design-library/ ← npm workspace root
├── packages/
│ ├── spring-ds-react/ ← published package (@spring/ds-react)
│ └── personal-loan-appflow/ ← AppFlow logic (@spring/personal-loan-appflow)
├── apps/
│ ├── design-library/ ← documentation gallery (@spring/design-library)
│ └── personal-loan/ ← standalone flow app (@spring/personal-loan)
├── scripts/
│ └── sync-tokens.js ← token build + Figma pull
└── docs/ ← documentation hub (this file lives here)
2. Entry Points — How to Consume the Library
The published package spring-design-library-react exposes six distinct subpath exports. Each serves a different consumer need.
2.1 Root export — all components
import { SpringButton, TextField, SpringHeader } from 'spring-design-library-react'
// or from the internal alias:
import { SpringButton, TextField, SpringHeader } from '@spring/ds-react'
This is the primary entry point. Every implemented component is available here.
2.2 /tokens — framework-agnostic design tokens
import {
tokens,
getToken,
getByPath,
figmaNameToCssVar,
AI_REFERENCE_MAP,
} from 'spring-design-library-react/tokens'
Use when you need to read token values in JavaScript (e.g. building a Tailwind theme, a chart palette, or an AI reference map). This is ESM-only.
2.3 /tokens/generated/css-variables.css — 254 CSS custom properties
// In Next.js root layout (SSR-safe):
import 'spring-design-library-react/tokens/generated/css-variables.css'
This single import makes every --spring-* CSS variable available globally. Always import this in your root layout before any component styles.
2.4 /icons — ~1,173 Figma-synced icon components
import { IconArrowRight, IconCheckCircle } from 'spring-design-library-react/icons'
<IconArrowRight className="w-5 h-5 text-spring-color-plum-600" />
Icons are organized into 20 categories (General, Arrows, Charts, Communication, Finance, etc.) and auto-synced from Figma.
2.5 /rules — structured design-system rules
import { rules } from 'spring-design-library-react/rules'
Machine-readable design rules. Primarily consumed by AI agents and linters.
2.6 /appflow — personal loan flow registry
import {
PERSONAL_LOAN_STEPS,
AppFlowShell,
PersonalLoanFlowChart,
} from 'spring-design-library-react/appflow'
Bundles @spring/personal-loan-appflow. Use when integrating the full 24-step personal loan flow into a product app.
2.7 Summary table
| Subpath | What you get | When to use |
|---|---|---|
root / /components | All UI components | Every project |
/tokens | Token values as JS objects | Tailwind config, charts, AI reference |
/tokens/generated/css-variables.css | CSS custom properties | Root layout import |
/icons | 1,173 SVG icon components | Anywhere you need iconography |
/rules | Design system rules | AI tooling, lint rules |
/appflow | Personal loan flow + registry | Product apps integrating the flow |
3. How to Use Components
3.1 Installation (consuming repo)
npm install spring-design-library-react
# or pin to internal registry:
npm install spring-design-library-react --registry https://npm.fury.io/springfinancial/
3.2 Wire up tokens (do this once)
In your Next.js root layout (app/layout.tsx):
import 'spring-design-library-react/tokens/generated/css-variables.css'
import './globals.css' // your Tailwind base
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
In tailwind.config.mjs, extend the theme with Spring tokens:
import { tokens, figmaNameToCssVar } from 'spring-design-library-react/tokens'
export default {
content: ['./src/**/*.{ts,tsx}', './node_modules/spring-design-library-react/src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
// Map semantic token CSS vars to Tailwind color utilities
background: 'hsl(var(--spring-semantic-color-bg-canvas) / <alpha-value>)',
foreground: 'hsl(var(--spring-semantic-color-text-primary) / <alpha-value>)',
// ... (see apps/design-library/tailwind.config.mjs for the full mapping)
},
},
},
}
Note: The design library's own
tailwind.config.mjsis the reference implementation. Copy its pattern when setting up a consuming app.
3.3 Using a component
All components follow the same import pattern:
import { SpringButton } from '@spring/ds-react'
export default function MyPage() {
return (
<SpringButton variant="primary" size="lg" onClick={() => console.log('clicked')}>
Get Started
</SpringButton>
)
}
3.4 Component catalogue
| Category | Components |
|---|---|
| actions | SpringButton |
| navigations | SpringHeader, SpringFooter, SpringFooterLegal, SpringLogo |
| selection-input | SpringSearchBar |
| overlays | ExitModal, InfoModal, ConfirmModal, ActionModal |
| feedback | SpringProgress |
| inputs | TextField, DropdownField, PhoneNumberField, SplitPillInput, OtpField, SearchField, BoxSelector, LoanAmountSlider, TileSelector, ConsentBox, FileUploader |
| modules | AddressFields, FaqAccordion, PlanCard, PlanSummaryCard, SpringLoanCard |
3.5 Props discovery
Every component is documented in the design gallery at /components/<category>/<component-name>. Each page includes:
- Live playground with variant/size controls
- Props table with type, default, and description
- States grid — default, active, error, disabled, loading
- Code block — copy-paste ready snippet
- Figma link to the source design
3.6 Key component patterns
SpringButton — renders as <button> or <a> based on whether href is set:
// Button
<SpringButton variant="primary" size="lg">Apply Now</SpringButton>
// Link
<SpringButton variant="outlined" size="md" href="/learn-more">
Learn More
</SpringButton>
// Loading state
<SpringButton variant="primary" loading>Processing...</SpringButton>
// With icons
<SpringButton variant="primary" leadIcon={<IconArrowRight />}>Next Step</SpringButton>
TextField — floating label, status-aware:
<TextField
label="First Name"
value={value}
onChange={(e) => setValue(e.target.value)}
status="default" // 'default' | 'active' | 'error'
helpMessage="As on your ID"
errorMessage="Required"
/>
AppFlow screens — presentational-only, no data fetching:
// Screens receive only display-layer props; data binding happens outside the gallery
<AppFlowNameScreen
onNext={() => router.push('/loan-amount')}
onBack={() => router.back()}
/>
4. React and Next.js Guidelines for New Users
4.1 Server Components vs. Client Components
This library follows Next.js App Router conventions. Default to Server Components.
// Server Component (default) — no 'use client' needed
// Good for: layouts, wrappers, static content
export default function LoanSummarySection() {
return (
<section>
<PlanSummaryCard title="Personal Loan" amount="$15,000" rate="9.99%" />
</section>
)
}
'use client'
// Client Component — required when using:
// - React hooks (useState, useEffect, useRef)
// - Browser APIs (window, localStorage)
// - Event handlers on interactive elements
// - Context consumers
export default function LoanAmountInput() {
const [amount, setAmount] = React.useState(5000)
return <LoanAmountSlider value={amount} onChange={setAmount} />
}
Rule: Add 'use client' only when the component needs hooks, events, lifecycle, or browser APIs. Spring components that are interactive (inputs, modals, search) are already 'use client' internally — their parent can still be a Server Component.
4.2 Styling: always use tokens
Never write raw CSS values or arbitrary Tailwind values:
// ✅ Correct — semantic token class
<div className="bg-background text-foreground p-space-4">
// ✅ Correct — CSS variable reference
<div style={{ color: 'var(--spring-semantic-color-text-primary)' }}>
// ❌ Wrong — raw hex
<div style={{ color: '#1A1A2E' }}>
// ❌ Wrong — arbitrary Tailwind value
<div className="bg-[#1A1A2E]">
4.3 Route structure
Follow the thin-shell pattern:
apps/your-app/src/
├── app/
│ └── loans/
│ └── page.tsx ← thin shell, one-liner
└── pages/
└── LoansPage.tsx ← all rendering logic here
// apps/your-app/src/app/loans/page.tsx
import LoansPage from '@/pages/LoansPage'
export default LoansPage
This keeps Next.js route files trivially replaceable and all logic testable independently.
4.4 Import paths
Always import components from the package, not from internal implementation paths:
// ✅ Correct — public package import
import { SpringButton, TextField } from '@spring/ds-react'
// ❌ Wrong — reaching into package internals
import SpringButton from '../../packages/spring-ds-react/src/components/actions/SpringButton'
4.5 TypeScript
All components ship TypeScript types. Use the exported types when writing wrappers:
import type { SpringButtonProps } from '@spring/ds-react'
interface MyButtonProps extends SpringButtonProps {
analyticsId: string
}
5. Why the Structure Works This Way
5.1 The core design decision: package first, gallery second
The design system is a package that ships components — not an app that happens to have components. This means:
packages/spring-ds-reactowns all reusable UI. It can be installed in any Next.js app withnpm install.apps/design-librarydocuments and previews those components. It is a consumer, not a definer.apps/personal-loanis a production-style consuming app — it proves the package works in a real flow.
This separation prevents gallery-specific concerns from bleeding into the shipped component API.
5.2 Token pipeline: Figma → CSS → Tailwind
design-tokens.mjs (source of truth — edited by hand or pulled from Figma)
↓ npm run tokens:sync
css-variables.css (254 --spring-* CSS vars — committed to git)
↓ import in layout.tsx
Available as CSS custom properties in all components
design-tokens.mjs
↓ imported by tailwind.config.mjs
Tailwind color/spacing/radius utilities (bg-background, p-space-4, etc.)
Why two outputs? CSS variables work with inline styles and runtime theming. Tailwind classes work with the utility-first authoring pattern and tree-shaking. Both are needed.
5.3 Thin route shells
apps/design-library/src/app/(shell)/**/page.tsx files are intentionally one-liners:
// apps/design-library/src/app/(shell)/components/inputs/text-field/page.tsx
import TextFieldPage from '@/gallery/pages/components/inputs/TextFieldPage'
export default TextFieldPage
Why? Next.js route files are part of the framework's filesystem router — they are infrastructure, not logic. Keeping them thin means:
- All page logic lives in
src/gallery/and is reusable/testable - Route reorganizations (moving URLs) don't touch any business logic
- The gallery can be previewed in isolation without Next.js routing
5.4 AppFlow registry pattern
The 24-step personal loan flow is driven by a single registry array (personalLoanFlow.ts). Each step is a plain object:
{
order: 3,
slug: 'loan-amount',
title: 'How much are you looking to borrow?',
phase: 'A',
status: 'new',
progress: 3,
Body: LoanAmountScreen,
sourcePath: 'screens/AppFlowLoanAmountScreen.tsx',
}
Why? The registry is the single source of truth for step order, routing, sidebar state, progress indicators, flow charts, and gallery previews. Changing the order of steps means editing one array, not updating five files.
5.5 Code Connect split
Figma Code Connect files (*.figma.tsx) are placed either:
- Package-side —
packages/spring-ds-react/src/components/inputs/*.figma.tsx— for all new reusable components and all inputs. These travel with the published package. - Gallery-side —
apps/design-library/src/gallery/pages/components/**/*.figma.tsx— for legacy navigations, overlays, andSpringSearchBaruntil a migration is planned.
Why the split? Code Connect for inputs maps Figma design properties to code props — this information is most useful when it's co-located with the component. Gallery-side Code Connect was added before the convention was established and will be migrated package-side in a future pass.
6. How to Add Your Own Component
This is the end-to-end checklist. Follow every step — skipping any one of them will cause a broken route, missing export, or disconnected navigation entry.
Step 1 — Implement in the package
packages/spring-ds-react/src/components/<category>/<ComponentName>.tsx
Categories (in gallery order):
inputs → navigations → actions → layout-structure → selection-input → image-icons → feedback-indicators → overlays → lists → tables → utilities → modules
Rules:
- Use semantic tokens — no raw hex, no arbitrary spacing
- Prefix with
Springwhere appropriate - Export the component's TypeScript types alongside it
- Default to Server Component; add
'use client'only if needed
// packages/spring-ds-react/src/components/feedback/SpringToast.tsx
import React from 'react'
export interface SpringToastProps {
message: string
variant?: 'success' | 'error' | 'info'
onDismiss?: () => void
}
export function SpringToast({ message, variant = 'info', onDismiss }: SpringToastProps) {
return (
<div className={`rounded-radius-md p-space-4 bg-surface-${variant}`}>
<span className="text-body-md text-foreground">{message}</span>
</div>
)
}
Step 2 — Export from the package barrel
// packages/spring-ds-react/src/index.ts
export { SpringToast } from './components/feedback/SpringToast'
export type { SpringToastProps } from './components/feedback/SpringToast'
Step 3 — Add Figma Code Connect (when applicable)
For any new reusable component, place the Code Connect file beside the component:
packages/spring-ds-react/src/components/feedback/SpringToast.figma.tsx
import figma from '@figma/code-connect'
import { SpringToast } from './SpringToast'
figma.connect(SpringToast, 'https://www.figma.com/design/YOUR_FILE_ID?node-id=XXX', {
props: {
variant: figma.enum('variant', { Success: 'success', Error: 'error', Info: 'info' }),
message: figma.string('message'),
},
example: ({ variant, message }) => <SpringToast variant={variant} message={message} />,
imports: ["import { SpringToast } from '@spring/ds-react'"],
})
Validate: npm run code-connect:parse
Step 4 — Create the gallery documentation page
apps/design-library/src/gallery/pages/components/<category>/<ComponentName>Page.tsx
Use the shared doc primitives:
'use client'
import { SpringToast } from '@spring/ds-react'
import { LibraryDocPageShell, LibraryPageHeader, LibraryPropsTable } from '@/gallery/components/LibraryDocPage'
import { CodeBlock } from '@/gallery/components/CodeBlock'
export default function SpringToastPage() {
return (
<LibraryDocPageShell>
<LibraryPageHeader
title="SpringToast"
description="Transient feedback messages for user actions."
figmaUrl="https://www.figma.com/design/..."
/>
<section>
<h2>Live Example</h2>
<SpringToast variant="success" message="Application submitted!" />
</section>
<CodeBlock language="tsx">{`<SpringToast variant="success" message="Application submitted!" />`}</CodeBlock>
<LibraryPropsTable props={[
{ name: 'message', type: 'string', required: true, description: 'Toast content' },
{ name: 'variant', type: "'success' | 'error' | 'info'", default: "'info'", description: 'Visual tone' },
{ name: 'onDismiss', type: '() => void', description: 'Dismiss callback' },
]} />
</LibraryDocPageShell>
)
}
Step 5 — Add the Next.js route shell
// apps/design-library/src/app/library/components/feedback-indicators/spring-toast/page.tsx
import SpringToastPage from '@/gallery/pages/components/feedback/SpringToastPage'
export default SpringToastPage
Step 6 — Add the sidebar navigation entry
// apps/design-library/src/gallery/navigation/libraryNav.ts
// Add under the correct category:
{
label: 'SpringToast',
to: '/components/feedback-indicators/spring-toast',
}
Only add to navigation when the page is live and complete. Do not add placeholder-only pages.
Step 7 — Verify
npm run dev # check /components/... route renders
npm run build # production build must pass
npm run code-connect:parse # if *.figma.tsx was added
npm run lint # no linter errors
7. How Production Builds Work
7.1 Build commands
| Command | What it does |
|---|---|
npm run build | Builds apps/design-library + apps/personal-loan (Next.js next build) |
npm run build:design-library | Builds only the gallery app |
npm run build:personal-loan | Builds only the standalone personal-loan app |
npm run preview | Serves the built gallery on port 5180 |
npm run preview:personal-loan | Serves the personal-loan app on port 5181 |
7.2 How the package is consumed without a build step
Inside the monorepo, apps/design-library and apps/personal-loan import @spring/ds-react directly from source via next.config.ts aliases:
// apps/design-library/next.config.ts
const nextConfig = {
transpilePackages: ['@spring/ds-react', '@spring/personal-loan-appflow'],
webpack: (config) => {
config.resolve.alias = {
'@spring/ds-react': path.resolve(__dirname, '../../packages/spring-ds-react/src/index.ts'),
// ...
}
return config
},
}
This means there is no separate tsup build step needed for local development. Next.js transpiles the TypeScript package source directly. When publishing externally, tsup.config.ts builds the dist artifacts.
7.3 Token change workflow (required before production build)
Whenever design-tokens.mjs is edited:
# 1. Rebuild CSS variables
npm run tokens:sync
# 2. Restart the dev server (CSS vars are loaded at startup)
# Kill and restart: npm run dev
# 3. Before production build validation, clear the Next.js cache
rm -rf apps/design-library/.next
# 4. Build
npm run build
Skipping the .next cache clear after a token change can result in stale CSS variables in the production build.
7.4 Deployment
The gallery app is configured for Vercel (vercel.json at repo root). The personal-loan app can be deployed independently.
Environment variables needed for Figma sync (not for production app runtime):
FIGMA_TOKEN— Figma personal access tokenFIGMA_FILE_ID— Foundation file for token pull
7.5 What ships in the published npm package
When packages/spring-ds-react is published to Gemfury, tsup builds:
dist/index.js— all componentsdist/icons/index.js— icon treedist/rules/index.js— design rulesdist/appflow/index.js— AppFlow registry (bundled from workspace package)dist/tokens/— token artifacts (copied verbatim fromsrc/tokens/)
The token CSS file (generated/css-variables.css) is published as a static asset and imported directly by consumers.
8. The Agentic Workflow
8.1 What it is
The agentic workflow is a structured, outcome-first protocol for AI-assisted design-to-code work. It defines how AI agents (and human contributors) classify requests, choose file targets, build outputs, and validate results before handoff.
It is documented in docs/ai/ and codified as executable .cursor/rules/ and .cursor/skills/.
8.2 The seven outcome types
Every request must be classified before any implementation begins:
| Type | When to use | Primary target |
|---|---|---|
| A — Reusable component | Result should be imported from @spring/ds-react | packages/spring-ds-react/src/components/ |
| B — Gallery doc page | Documenting or demonstrating existing UI | apps/design-library/src/gallery/pages/ |
| C — AppFlow screen | Personal loan flow step | apps/design-library/src/gallery/page/appflow/screens/ |
| D — Static library page | Informational, foundation, or guide content | apps/design-library/src/gallery/pages/ |
| E — Token/foundation change | Visual system change | packages/spring-ds-react/src/tokens/design-tokens.mjs |
| F — Icon change | Adding or regenerating icons | packages/spring-ds-react/src/icons/ |
| G — Documentation/rule change | Operating guidance update | docs/ or .cursor/rules/ |
8.3 The five-agent model
A single AI instance can play multiple roles, but the responsibilities must remain distinct:
| Role | Responsibility | Cannot |
|---|---|---|
| Workflow Conductor | Intake, classification, approval gates, handoff | Skip user validation; hide failures |
| Design & Token Analyst | Read Figma context; map values to tokens; report gaps | Write component syntax; add raw values |
| Structure & Reuse Architect | Determine reuse vs. create; audit exports and registries | Create new components when existing ones suffice |
| Implementation Builder | Build only the approved outcome and injection points | Put logic in route shells; call production APIs in gallery |
| Guardrail Critic | Run validation; classify failures; limit correction loops | Mark work complete without named validation coverage |
8.4 The deterministic workflow loop
Inbound request
→ Classify outcome type
→ Check for missing context (ask if critical context is absent)
→ Investigate existing structure (audit exports, routes, registry)
→ Token and design normalization (map to tokens; report gaps)
→ Build plan + file map (stop for user approval when uncertain)
→ [User approval gate]
→ Build outcome files
→ Inject routes, registries, exports, nav
→ Guardrail validation (max 3 correction loops)
→ Developer handoff with WorkflowRunState
8.5 Core guardrails
- File boundary — components stay in the package; gallery docs stay in
apps/design-library/src/gallery; routes stay underapps/design-library/src/app/(shell) - Server Components by default — add
'use client'only when necessary - Registry completeness — every new component/screen must be wired into all required injection points
- Placeholder policy — do not add incomplete pages to navigation
- Three-loop limit — stop and escalate after three failed validation attempts
8.6 Reading order for agents
docs/ai/project-context.md ← classify and route the request
docs/ai/operational-manifesto.md ← workflow, roles, guardrails
docs/ai/workflow-contracts.md ← file targets and injection contracts
docs/ai/review-checklist.md ← quality gate before handoff
8.7 Handoff artifact
A completed workflow must state:
- Route/import path: /components/...
- Known risks: [any dirty-tree caveats or skipped validations]
---
## 9. Building a Full Complex Repo With This Component Set
This section walks through how to build a production-grade Next.js application using Spring DS as the component foundation.
### 9.1 Project setup
```bash
# Create your app
npx create-next-app@latest my-loan-app --typescript --tailwind --app
# Install Spring DS
cd my-loan-app
npm install spring-design-library-react
9.2 Configure Next.js to transpile the package
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
transpilePackages: ['spring-design-library-react'],
}
export default nextConfig
9.3 Wire tokens into Tailwind
// tailwind.config.mjs
import { tokens } from 'spring-design-library-react/tokens'
export default {
content: [
'./src/**/*.{ts,tsx}',
'./node_modules/spring-design-library-react/src/**/*.{ts,tsx}',
],
theme: {
extend: {
colors: {
background: `hsl(var(--spring-semantic-color-bg-canvas) / <alpha-value>)`,
foreground: `hsl(var(--spring-semantic-color-text-primary) / <alpha-value>)`,
primary: `hsl(var(--spring-semantic-color-bg-brand) / <alpha-value>)`,
// extend with your own brand aliases as needed
},
},
},
}
9.4 Import the CSS variables in your root layout
// src/app/layout.tsx
import 'spring-design-library-react/tokens/generated/css-variables.css'
import './globals.css'
9.5 Recommended folder structure for a complex app
my-loan-app/
├── src/
│ ├── app/ ← Next.js App Router routes (thin shells)
│ │ ├── layout.tsx ← token CSS + font + providers
│ │ ├── (marketing)/ ← route group for public pages
│ │ │ ├── page.tsx
│ │ │ └── about/page.tsx
│ │ └── (application)/ ← route group for authenticated flow
│ │ ├── layout.tsx ← auth guard, app shell
│ │ └── apply/
│ │ ├── page.tsx ← start screen
│ │ └── [step]/page.tsx ← dynamic flow steps
│ ├── features/ ← domain-specific feature modules
│ │ ├── application/ ← loan application feature
│ │ │ ├── components/ ← feature-level components (extend Spring DS)
│ │ │ ├── hooks/ ← useApplicationState, useLoanCalculator
│ │ │ ├── services/ ← API calls, data fetching
│ │ │ └── screens/ ← full screen compositions
│ │ └── auth/
│ │ ├── components/
│ │ └── hooks/
│ ├── components/ ← app-wide shared components
│ │ ├── AppHeader.tsx ← wraps SpringHeader with app-specific data
│ │ ├── AppFooter.tsx
│ │ └── FormField.tsx ← wraps TextField with validation
│ ├── lib/ ← utilities, constants, types
│ │ ├── api.ts
│ │ ├── validation.ts ← zod schemas
│ │ └── types.ts
│ └── styles/
│ └── globals.css
9.6 Building a multi-step flow
// src/features/application/screens/LoanAmountScreen.tsx
'use client'
import { LoanAmountSlider, SpringButton } from 'spring-design-library-react'
import { useState } from 'react'
interface LoanAmountScreenProps {
defaultAmount?: number
onNext: (amount: number) => void
onBack: () => void
}
export function LoanAmountScreen({ defaultAmount = 5000, onNext, onBack }: LoanAmountScreenProps) {
const [amount, setAmount] = useState(defaultAmount)
return (
<div className="flex flex-col gap-space-8 max-w-stage mx-auto px-space-4">
<h1 className="text-heading-xl text-foreground">
How much would you like to borrow?
</h1>
<LoanAmountSlider
value={amount}
min={1000}
max={35000}
onChange={setAmount}
/>
<div className="flex gap-space-4">
<SpringButton variant="outlined" size="lg" onClick={onBack}>Back</SpringButton>
<SpringButton variant="primary" size="lg" block onClick={() => onNext(amount)}>
Continue
</SpringButton>
</div>
</div>
)
}
// src/app/(application)/apply/[step]/page.tsx
import { notFound } from 'next/navigation'
import { LoanAmountScreen } from '@/features/application/screens/LoanAmountScreen'
const STEP_COMPONENTS: Record<string, React.ComponentType<any>> = {
'loan-amount': LoanAmountScreen,
// ... other steps
}
export default function StepPage({ params }: { params: { step: string } }) {
const Screen = STEP_COMPONENTS[params.step]
if (!Screen) notFound()
return <Screen />
}
9.7 Separating presentation from data
The Spring DS components and AppFlow screens are purely presentational. Keep data concerns out of components:
// ✅ Correct — data fetching in a Server Component above the screen
async function ApplicationPage({ params }: { params: { step: string } }) {
const application = await fetchApplication(params.applicationId) // server-side
return <LoanAmountScreen defaultAmount={application.requestedAmount} onNext={...} />
}
// ❌ Wrong — data fetching inside a presentation screen
function LoanAmountScreen() {
const { data } = useSWR('/api/application') // this couples UI to data layer
// ...
}
9.8 Extending components without forking
Wrap Spring DS components to add app-specific behavior:
// src/components/FormField.tsx
'use client'
import { TextField, type TextFieldProps } from 'spring-design-library-react'
import { useFormContext } from 'react-hook-form'
interface FormFieldProps extends Omit<TextFieldProps, 'value' | 'onChange' | 'status' | 'errorMessage'> {
name: string
}
export function FormField({ name, ...props }: FormFieldProps) {
const { register, formState: { errors } } = useFormContext()
const { ref, ...rest } = register(name)
return (
<TextField
{...props}
{...rest}
inputRef={ref}
status={errors[name] ? 'error' : 'default'}
errorMessage={errors[name]?.message as string}
/>
)
}
10. What to Do and What Not to Do
Do
| Practice | Why |
|---|---|
Import from @spring/ds-react (or the published package root) | Ensures you use the public API, not internal implementation details |
Use semantic token classes (bg-background, text-foreground) | Token classes are versioned; raw hex breaks on theme updates |
Import css-variables.css in your root layout | All Spring components depend on these CSS custom properties |
Default to Server Components; add 'use client' only when needed | Improves performance; avoids unnecessary client bundles |
Keep route files thin (app/**/page.tsx as one-liners) | Separates routing infrastructure from UI logic |
Run npm run tokens:sync after editing design-tokens.mjs | Keeps generated CSS in sync with the source |
Clear .next/ before production build validation after token changes | Prevents stale CSS in the cache |
Run npm run code-connect:parse after editing *.figma.tsx | Validates Code Connect mappings before publishing to Figma |
| Export component types alongside the component | Enables consumers to write strongly-typed wrappers |
Register AppFlow steps in personalLoanFlow.ts | Keeps sidebar, routing, flow charts, and progress indicators in sync |
| Wrap Spring DS components when adding app-specific behavior | Avoids forking the package; updates compose cleanly |
Do Not
| Anti-pattern | Why it breaks things |
|---|---|
Import from package internals (../../packages/spring-ds-react/src/...) | Bypasses the public API; breaks on refactoring |
Use raw hex colors or arbitrary Tailwind values (bg-[#1A1A2E]) | Disconnects from the token system; visual inconsistency at scale |
Skip the css-variables.css import | Components will render without colors, spacing, or typography |
| Put production API calls, analytics, or database access inside gallery pages or AppFlow screens | Gallery is a documentation layer; it cannot call production services |
| Add placeholder-only pages to sidebar navigation | Users encounter dead-end routes; erodes trust in the design system |
| Modify multiple unrelated files during a single feature addition | Produces unclear diffs, harder rollbacks, and obscured responsibility |
| Revert unrelated user changes during automated agentic workflows | Destroys concurrent work |
| Define component API inside gallery pages (e.g. local type definitions that mirror the component) | Creates drift between package and gallery; types go stale |
Hand-edit generated/css-variables.css directly | Overwritten on next tokens:sync |
Hand-edit the icons/ directory | Overwritten on next icons:sync |
Add 'use client' to route shells | Route shells should be Server Components; client boundaries belong in the gallery implementation |
Skip npm run build before raising a PR | Production builds surface errors that dev mode silently ignores |
11. Production Readiness Evaluation
This section honestly evaluates the library's current state for complex production workflows.
11.1 Strengths
| Area | Assessment |
|---|---|
| Token system | Strong. 254 CSS variables, Zod-validated, two-output pipeline (CSS + JS), full Figma sync. Covers primitives, semantics, and component-level tokens. |
| Component API | Consistent patterns across all categories. forwardRef on interactive components, TypeScript types exported, Figma property tables documented in component headers. |
| Gallery documentation | Comprehensive. Every component has a live playground, states grid, props table, and code block. Replaces Storybook effectively for this team's scale. |
| AppFlow registry | Elegant. Single registry drives routing, sidebar, progress, flow charts, and previews. Adding a step is a one-file change. |
| Agentic workflow | Mature. Five-role model, outcome types, three-loop guardrail, handoff artifacts — this is a production-grade AI workflow, not an ad-hoc prompt. |
| Monorepo structure | Clean. Clear package/app boundary. Thin shells, gallery implementations, package-first components. |
| Code Connect | 20 files covering all inputs and major components. Figma config validated via CLI. Enables designers to inspect real component usage. |
11.2 Current gaps (known)
| Gap | Severity | Notes |
|---|---|---|
| No package build step for external publish | Medium | Apps consume source via Next.js aliases. Publishing externally requires tsup dist build. This is documented but not automated. |
| Code Connect split | Medium | Inputs are package-side; navigations, overlays, and SpringSearchBar are legacy gallery-side. A migration pass is needed for consistency. |
| Empty component categories | Low | layout-structure, lists, tables, image-icons, utilities exist as stubs. No components yet. Reserved for future Figma targets. |
| No Turborepo | Low | Acceptable for current monorepo size (2 apps, 2 packages). Parallel builds and caching become important with 4+ workspaces. |
| No automated component tests | Medium | No unit or visual regression tests. Production confidence relies on manual QA (npm run build + gallery review). |
| No versioning strategy | Medium | spring-design-library-react is at 0.0.1. No semantic versioning policy, no changelog, no deprecation process. |
| No dark mode | Info | Token structure supports a .dark class (defined in Tailwind config) but no dark-mode token set is populated. |
| Placeholder gallery routes | Low | ~12 routes exist but are hidden from navigation. Direct URL access returns a content-free page. |
11.3 Verdict by use case
| Use case | Readiness |
|---|---|
| Internal team building loan flow screens | Production-ready — the AppFlow system and component set are purpose-built for this |
| Cross-team design system consumption (same org) | Ready with setup — publish to Gemfury, document token setup, establish versioning |
| Open-source or external npm distribution | Not yet — needs dist build, changelog, semver, deprecation policy |
| Multi-brand theming (partner DPP) | Architecture supports it — Tailwind config is token-driven; partner token overrides are planned but not implemented |
| Design-to-code AI generation | Ahead of the curve — Code Connect + agentic workflow + token AI reference map is a strong foundation |
| Team with no prior React/Next.js experience | Needs companion guide — this document addresses the gap, but onboarding time is real |
12. Scalability Reflection
These are the architectural improvements to consider as the system grows.
12.1 What works well and will continue to scale
- Token pipeline — the
design-tokens.mjs → tokens:sync → css-variables.csspipeline is deterministic and composable. It can grow to 500+ tokens without structural change. - Registry pattern —
personalLoanFlow.tsas single source of truth is excellent. Extend to more flows by creating additional*Flow.tsregistries following the same shape. - Thin route shells — the shell/implementation split means URL restructuring is always a one-file change. This is scalable to 100+ routes.
- Outcome-typed agentic workflow — classifying requests before building prevents scope creep. This scales with team size.
12.2 Where friction will grow
Tailwind content scanning
As the monorepo gains more packages, the content array in tailwind.config.mjs must be updated manually:
content: [
'./src/**/*.{ts,tsx}',
'../../packages/spring-ds-react/src/**/*.{ts,tsx}',
'../../packages/personal-loan-appflow/src/**/*.{ts,tsx}',
// each new package must be added here
]
Recommendation: Introduce a shared Tailwind preset package (@spring/tailwind-config) that apps extend. This moves the content paths into one maintainable location.
Package consumption without a build step
The current next.config.ts alias approach works inside the monorepo but is incompatible with external consumers. As the team adds more consuming apps (even internally), each needs the same manual transpilePackages + alias setup.
Recommendation: Add a proper tsup dist build that runs as part of npm run build. Publish dist/ artifacts. Remove the need for consuming apps to alias the package source.
No versioning or changelog
At 0.0.1, breaking changes to component APIs are silently absorbed by all apps in the monorepo. As soon as a second team consumes the package, an untracked API change will break their build.
Recommendation: Adopt semantic versioning, generate changelogs from conventional commits, and establish a deprecation notice period (minimum one minor version).
Component test coverage
The lack of automated tests means every component change requires manual gallery review. With 20+ components and 5 reserved categories still to implement, this is a growing QA burden.
Recommendation (in order of effort):
- Snapshot tests for token output (
tokens:syncproduces identical output) - TypeScript strict mode on all component files
@testing-library/reactunit tests for interactive components (inputs, modals)- Visual regression testing (Chromatic or Percy) once the component set stabilizes
CSS variable naming collision risk
All 254 CSS variables use the --spring- prefix. As partner DPP tokens are introduced, a convention for namespacing brand-specific overrides is needed to avoid collisions.
Recommendation: Reserve --spring-brand-* for partner token overrides. Document the override surface in the token system.
AppFlow registry coupling
personalLoanFlow.ts is the single registry for step order, routing, and previews. If two different products need different flows (e.g. auto loan, home equity), duplicating the registry pattern will work but creates maintenance overhead.
Recommendation: Generalize personalLoanFlow.ts into a typed FlowRegistry<T> factory. The existing structure is already close — extracting the type from PERSONAL_LOAN_STEPS and creating createFlowRegistry(steps) would allow any product flow to use the same shell infrastructure.
Monorepo tooling
npm workspaces has no build caching, no parallel execution, and no dependency graph. npm run build rebuilds everything serially.
Recommendation: Introduce Turborepo when the monorepo reaches 4+ packages. The migration is low-risk — add turbo.json, replace root build/dev scripts. The existing structure maps cleanly onto Turborepo's pipeline model.
12.3 Priority order for scalability improvements
| Priority | Improvement | Effort |
|---|---|---|
| 1 | Package dist build + versioning | Medium |
| 2 | Shared Tailwind preset package | Low |
| 3 | Interactive component unit tests | Medium |
| 4 | FlowRegistry<T> generalization | Low |
| 5 | Turborepo | Low |
| 6 | Visual regression testing | High |
| 7 | Multi-brand token namespacing | Medium |
Quick Reference
Essential commands
npm install # install workspace dependencies
npm run dev # start design gallery at localhost:3000
npm run dev:personal-loan # start standalone personal-loan app
npm run build # production build (both apps)
npm run tokens:sync # regenerate CSS variables from design-tokens.mjs
npm run tokens:figma # pull Figma Variables into figma-variables.json
npm run icons:sync # sync icon components from Figma
npm run code-connect:parse # validate Figma Code Connect files
npm run code-connect:publish # publish Code Connect to Figma
npm run lint # ESLint across the workspace
Key file locations
| What you need | Where to find it |
|---|---|
| Token source of truth | packages/spring-ds-react/src/tokens/design-tokens.mjs |
| CSS variables (generated) | packages/spring-ds-react/src/tokens/generated/css-variables.css |
| Component source | packages/spring-ds-react/src/components/<category>/ |
| Public component exports | packages/spring-ds-react/src/index.ts |
| Gallery doc pages | apps/design-library/src/gallery/pages/ |
| Route shells | apps/design-library/src/app/(shell)/ |
| Sidebar navigation | apps/design-library/src/gallery/navigation/libraryNav.ts |
| AppFlow step registry | packages/personal-loan-appflow/src/personalLoanFlow.ts |
| AppFlow screens | apps/design-library/src/gallery/page/appflow/screens/ |
| Tailwind config | apps/design-library/tailwind.config.mjs |
| AI workflow docs | docs/ai/ |
| Component addition checklist | docs/COMPONENT_ADDITION.md |
Related documentation
| Document | Audience | Purpose |
|---|---|---|
docs/REPOSITORY_STRUCTURE.md | All developers | Canonical repo map |
docs/CONTRIBUTING.md | Contributors | Setup + PR workflow |
docs/COMPONENT_ADDITION.md | Component authors | Step-by-step checklist |
docs/PLACEHOLDER_POLICY.md | Gallery maintainers | Route placeholder conventions |
docs/ai/project-context.md | AI agents | Intake protocol + outcome routing |
docs/ai/operational-manifesto.md | AI agents / workflow | Roles, guardrails, workflow loop |
docs/ai/workflow-contracts.md | AI agents / maintainers | File targets + injection contracts |
docs/ai/review-checklist.md | AI agents | Pre-handoff quality gate |
packages/spring-ds-react/README.md | DS authors | Package entry points + Figma rules |
apps/design-library/README.md | App developers | App routing + integration |