jcicl
v1.0.76
Published
Component library for the websites of Johnson County Iowa
Readme
Johnson County Component Library (jcicl)
A React component library for Johnson County, Iowa web applications. Built with TypeScript, Vite, MUI, and Emotion. Includes reusable components, form state management, data loading patterns, and page templates.
Quick Start (for consumers)
Runtime Environment
- Download and install NVM for Windows
nvm install 22.11.0nvm use 22
Install
npm install jcicl@latestUsage
import Button from 'jcicl/Button';
import DataPage from 'jcicl/DataPage';
import { BaseApiClient } from 'jcicl/api';
import { createFormContext } from 'jcicl/FormContext';Required CSS & Fonts
In your project entry point (main.tsx), add:
import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';
import '@fontsource/material-icons';
import 'jcicl/assets/tailwind.css';
import 'overlayscrollbars/overlayscrollbars.css';Key Features
BaseApiClient (jcicl/api)
Reusable API client base class with timeout, JSON parsing, and standardized error handling. Extend it in your app and add endpoint methods.
DataPage (jcicl/DataPage)
Generic component that manages async data fetching with loading, error, empty, and happy-path states. Includes setData for optimistic CRUD, withWarnings for partial success, and guards for data-dependent routing.
FormContext (jcicl/FormContext)
Factory function that creates typed React Context + Provider pairs for form state management. Handles text, checkbox, dropdown, phone, zip, and multi-select inputs automatically.
Development
Getting started
- Ensure your React development environment is set up
npm installfrom the root project directorynpm startornpm run storybookto launch Storybook
Storybook
We use Storybook for component documentation and visual testing.
npm run storybook # Start Storybook dev server
npm start # Same as aboveComponent directory structure
src/components/
base/ # Foundational building blocks (Button, Input, Flex, etc.)
composite/ # Reusable patterns built from base components (Table, Toast, WithLoading, etc.)
supercomposite/ # Higher complexity composites (FieldGroup, FormFields, etc.)
templates/ # Full-page layouts (DefaultTemplate, AppContainer, DataPage)Each component folder contains:
[Component].tsx— the component implementation[Component].stories.tsx— Storybook documentationindex.ts— barrel export
Dependencies
- Material UI — base UI components (legacy, being phased out)
- Emotion — CSS-in-JS styling (legacy, being phased out)
- Tailwind CSS v4 — utility-first CSS (new standard)
Styling Migration: MUI/Emotion to Tailwind
Current state
The library is actively migrating from Material UI + Emotion to Tailwind CSS + fully custom components. During the transition, both approaches coexist:
- Legacy components use MUI base components (
MuiButton,MuiDrawer, etc.) with Emotionstyled()for visual customization - New and migrated components use Tailwind utility classes directly in JSX, with CSS custom properties for runtime-dynamic values
Why Tailwind
Broader ecosystem adoption. Tailwind is the most widely used CSS framework in the React ecosystem. New developers joining the team are more likely to already know it than Emotion's CSS-in-JS API. This reduces onboarding time.
Colocated styles. Tailwind classes live directly on the JSX element they style. There's no need to scroll between a styled component definition and its usage, or to name every wrapper (const StyledContainer = styled('div')(...)). You read the element and its styles together:
// Emotion — style is defined elsewhere, applied by component name
const CardHeader = styled('div')(({ color }) => ({
padding: '12px 16px',
backgroundColor: color,
borderRadius: '8px',
fontWeight: 600,
}));
<CardHeader color={themeColor}>Title</CardHeader>
// Tailwind — style is on the element
<div className="p-3 px-4 rounded-lg font-semibold" style={{ backgroundColor: themeColor }}>
Title
</div>No runtime CSS generation. Emotion generates CSS at runtime (in the browser), which adds JavaScript execution time on every render. Tailwind generates all CSS at build time — the browser just applies static classes. This improves performance, especially on low-powered devices.
Smaller bundle when tree-shaken. Emotion ships ~11KB of runtime JavaScript. Tailwind ships zero runtime JS — only the CSS classes you actually use, determined at build time.
Better DevTools experience. Tailwind classes are visible in the browser's element inspector as real class names. Emotion generates hashed class names (css-1a2b3c) that are meaningless in DevTools without the React DevTools extension.
Design token consistency. Tailwind's theme system (--spacing, --color-*, etc.) enforces consistent spacing, colors, and typography across components. Emotion relies on manually importing and using a theme object, which is easy to forget or override inconsistently.
Class objects pattern
For components with multiple variants or states, define Tailwind class strings as named constants at module level. This keeps JSX clean and makes the style definitions reusable, testable, and easy to scan. See the Button component for the canonical example:
// Define class strings as named constants — one per variant
const button1Classes =
'!bg-[#009200] !h-10 !border-2 !border-transparent !text-white transition-all duration-[313ms] ease-in !rounded-[32px] !font-normal !py-3 !px-8 !text-base shadow-[0px_0px_2px_1px_rgba(100,100,100,0.63)] hover:!bg-[#005c00]';
const button2Classes =
'!bg-[#fab62d] !h-10 !border-2 !border-transparent !text-[#131313] transition-all duration-[313ms] ease-in !rounded-[32px] !font-normal !py-3 !px-8 !text-base shadow-[0px_0px_2px_1px_rgba(100,100,100,0.63)] hover:!bg-[#e0a022]';
const customButtonClasses =
'!h-10 !border-2 !border-transparent transition-all duration-[313ms] ease-in !rounded-[32px] !font-normal !py-3 !px-8 !text-base ...';
// Use in JSX — clean, one className per element
if (variant === 1) return <ButtonBase className={button1Classes} {...props}>{children}</ButtonBase>;
if (variant === 2) return <ButtonBase className={button2Classes} {...props}>{children}</ButtonBase>;Why this pattern:
- Readable — each variant's full visual definition is in one place, not scattered across JSX
- Scannable — you can compare two variants by looking at two adjacent constants
- Composable — use
clsx()to merge base classes with conditional classes:className={clsx(baseClasses, active ? activeClasses : inactiveClasses)} - Static — Tailwind's build can scan these constants for class names (unlike template literals with dynamic interpolation)
Migration rules for contributors
All new code must use Tailwind. No new Emotion styled() components, no new css template literals.
Updating an existing Emotion component? Rewrite it in Tailwind first. If you need to modify a component that currently uses Emotion, migrate the entire component to Tailwind before making your changes. This ensures we're always moving forward — every PR that touches a legacy component leaves it fully migrated.
The only exception is a critical production bugfix where the risk of a full rewrite outweighs the migration benefit. In that case, fix the bug in Emotion, file a follow-up ticket for the Tailwind migration, and note it in the PR description.
Tailwind patterns
Static styles — use class string constants (see class objects pattern above):
const cardClasses = 'rounded-lg p-4 bg-white shadow-md';
<div className={cardClasses}>...</div>Runtime-dynamic values (props that change per-instance) — use CSS custom properties with a wrapper div:
<div
style={{
'--card-bg': backgroundColor,
'--card-text': textColor,
display: 'contents',
} as React.CSSProperties}
>
<div className="rounded-lg p-4 !bg-[var(--card-bg)] !text-[var(--card-text)]">
{children}
</div>
</div>The display: contents wrapper is invisible to layout but provides a CSS scope for the custom properties. This avoids Emotion entirely for dynamic values.
Conditional classes — use clsx:
import clsx from 'clsx';
<button className={clsx(
baseClasses,
active ? '!bg-black !text-white' : '!bg-white !text-black',
)}>MUI overrides — use !important prefix (!bg-*, !text-*) when Tailwind classes need to override MUI's built-in styles during the transition period. Once a component is fully off MUI, the ! prefixes can be removed.
Tailwind CSS output
Tailwind CSS is generated at build time via the @tailwindcss/cli postbuild step. The output lives at dist/assets/tailwind.css. Consuming apps import it in their entry point:
import 'jcicl/assets/tailwind.css';Only tailwindcss/theme and tailwindcss/utilities are included — not Tailwind's Preflight base reset, which would conflict with existing app styles.
Extending library component styles in consuming apps
With Tailwind, consuming apps can extend or override a library component's styles directly via className — no custom style prop, no wrapper div, no Emotion override needed:
// Consuming app — add margin and custom width to a library Button
<Button className="mt-4 w-full" variant={1}>
Full Width Submit
</Button>This works because Tailwind classes are just CSS classes. The consuming app's Tailwind build scans its own source files, generates the utilities, and they merge naturally with the library's classes. The consumer has full control without the library needing to expose a style or className forwarding prop for every visual property.
Why this replaces the style prop pattern:
The common pattern across our apps has been passing inline style={{}} objects to customize component appearance:
// Old pattern — inline styles scattered across consuming apps
<FormContainer style={{ paddingTop: '25px', paddingBottom: '25px' }}>
<Button style={{ width: '447px', alignSelf: 'center' }}>
<Flex styles={{ margin: '20px', fontWeight: 500 }}>This approach has problems:
- Not reusable — inline styles can't be shared or composed. If three pages need the same padding, each repeats the same object.
- Not responsive — inline styles don't support media queries, hover states, or focus states. You can't write
style={{ '@media (max-width: 768px)': { padding: '10px' } }}. - Highest specificity — inline styles override everything, making it impossible for the component to provide sensible defaults that consumers can opt out of.
- No design tokens — inline styles use raw pixel values (
'25px') instead of a shared spacing scale. Nothing enforces consistency across pages. - Not scannable by Tailwind — inline styles exist at runtime. They add no value to Tailwind's build-time optimization.
With Tailwind, all of these are solved:
// New pattern — Tailwind classes, composable, responsive, consistent
<FormContainer className="pt-6 pb-6">
<Button className="w-[447px] self-center" variant={1}>
<Flex className="m-5 font-medium">
// Responsive? Just add a breakpoint prefix:
<FormContainer className="pt-4 pb-4 md:pt-6 md:pb-6">
// Hover/focus? Just add the state prefix:
<Button className="w-full hover:w-auto">For component authors: ensure your components forward className to the root element so consumers can extend styles. Most components already do this via ...muiProps or ...rest spread. As we migrate off MUI, make className an explicit prop.
The goal: eliminate all style={{}} props from consuming apps. Every visual customization should be expressible as a Tailwind class. If it can't be (truly dynamic runtime values like theme colors), use the CSS custom property pattern documented above.
Accessibility (a11y)
All components should follow WCAG 2.1 Level AA guidelines. Here's a brief overview of the key principles:
The four principles (POUR)
- Perceivable — information must be presentable in ways all users can perceive (alt text on images, sufficient color contrast, visible focus indicators)
- Operable — UI must be navigable by keyboard, screen reader, and assistive devices (all interactive elements focusable, no keyboard traps, adequate time limits)
- Understandable — content and operation must be understandable (clear labels, predictable navigation, helpful error messages)
- Robust — content must work with current and future assistive technologies (semantic HTML, proper ARIA attributes, valid markup)
Practical checklist for component development
- Color contrast — text must have at least 4.5:1 contrast ratio against its background (3:1 for large text). Use WebAIM Contrast Checker to verify.
- Keyboard navigation — every interactive element must be reachable and operable via keyboard (Tab, Enter, Escape, arrow keys). Test by unplugging your mouse.
- Focus indicators — focused elements must have a visible outline. Never use
outline: nonewithout providing an alternative focus style. - Semantic HTML — use
<button>for buttons (not<div onClick>),<table>for tabular data,<nav>for navigation,<h1>-<h6>in order. - ARIA labels — add
aria-labeloraria-labelledbyto interactive elements that don't have visible text labels (icon buttons, inputs without visible labels). - Screen reader testing — test with NVDA (free, Windows) or the browser's built-in accessibility inspector.
- Form inputs — every input must have an associated
<label>. OurLabeledInput,LabeledDropdown, andLabeledCheckboxcomponents handle this automatically. - Error messages — form errors should be announced to screen readers via
aria-live="polite"orrole="alert".
Resources
- WCAG 2.1 Quick Reference
- WebAIM — Web Accessibility in Mind
- MDN Accessibility Guide
- A11y Project Checklist
- Inclusive Components (book/blog)
Building & Testing Locally
Build the library
npm run buildThis runs TypeScript compilation, Vite build, Tailwind CSS generation, and packages everything into dist/.
Test locally in a consuming app (without publishing)
Instead of publishing to npm, you can install the built dist/ folder directly into a consuming app:
# 1. Build the library
cd /path/to/your/local/JCComponentLibrary
npm run build
# 2. Install from local dist in your consuming app
cd /path/to/your/consumingApplication # or any consuming app
npm install /path/to/your/local/JCComponentLibrary/dist
# 3. Verify it works
npm run devThis creates a symlink-like install in node_modules/jcicl pointing to the local dist/ folder. Changes take effect immediately after rebuilding the library — no version bump or publish needed.
To revert to the published version:
npm install jcicl@latestBuild + publish (when ready to release)
npm run bp # Patch version bump (0.0.x), build, and publish
npm run bpMinor # Minor version bump (0.x.0)
npm run bpMajor # Major version bump (x.0.0)The library also auto-publishes on merges to master via the CI pipeline.
Adding a New Component
- Create the component folder:
src/components/base/MyComponent/ - Create the component:
MyComponent.tsx - Create stories:
MyComponent.stories.tsx - Create barrel export:
index.ts—export { default, type MyComponentProps } from './MyComponent'; - Add to the main barrel:
src/components/index.ts—export { default as MyComponent } from './base/MyComponent'; - Build and test locally (see above)
- Publish when ready
Deploying Storybook
Build Storybook with npm run build-storybook, then copy all files from storybook-static/ to the hosting location.
