4.3 KiB
Forge - Structured CSS Authoring
A typed, local, variant-driven way to author CSS. Compiles to real CSS but removes the chaos: no global conflicts, no selector gymnastics, no inline styles. Built for Hono JSX with SSR support.
The Problem
CSS is hostile to humans at scale: global namespace, no markup-to-definition link, requires inline styles for per-instance variance, silent conflicts, selector complexity.
The Solution
- Local styles - Attached via generated class names, not strings
- Parts - Named sub-components replace selectors (no
.Button > .Iconnonsense) - Variants - Typed parameters replace inline styles (no
style={{ color: x }}) - Deterministic - Known merge order, dev warnings for conflicts
- Compiles to CSS - Not a new language, not runtime CSS-in-JS, just organized CSS generation
Core Concepts
define(name?, def) - Creates a styled component. Returns a component function.
- Accepts CSS properties in camelCase (auto-converts to kebab-case)
- Numbers auto-converted to
px(except unitless props likeopacity,zIndex) - Generates CSS classes and registers styles globally
Parts - Sub-components within a component (e.g., Header, Body, Footer)
- Defined via
parts: { PartName: { ...styles } } - Accessible in render as
parts.PartName - Generate classes like
ComponentName_PartName
Variants - Conditional styling based on props
- Boolean:
variants: { active: { color: 'blue' } }→<Component active /> - Keyed:
variants: { size: { small: {...}, large: {...} } }→<Component size="small" /> - Work on both root and parts
- Generate classes like
ComponentName.variant-key
States - Pseudo-selectors like hover, focus
states: { hover: { background: 'blue' } }→.Class:hover { ... }
Custom Render - Override default rendering
render({ props, parts }) { return <parts.Root>...</parts.Root> }- Compose parts manually, pass props through
File Structure
src/index.tsx- Main implementation (define,Styles, CSS generation)src/types.ts-TagDeftype with all CSS properties, helper setsexamples/- REFERENCE THESE for real-world usage patterns:helpers.tsx- Layout wrapper, reusable components (Body, Header, ThemeToggle)index.tsx- Landing page with grid, cards, parts, and custom renderbutton.tsx- Button variants (intent, size, disabled)profile.tsx- Complex component with multiple parts and variantsnavigation.tsx- Tabs, pills, breadcrumbs, vertical nav patterns
src/tests/- Comprehensive test suite with test helpers
Implementation Details
- Static CSS generation - CSS created at component definition time, not runtime
- Global styles registry -
stylesobject stores all CSS as plain objects Stylescomponent - Renders<style>tag with all registered CSS (include in HTML<head>)- Real CSS classes - Generates actual CSS selectors, not inline styles or CSS-in-JS
- Class naming:
- Root:
ComponentName - Parts:
ComponentName_PartName - Variants:
ComponentName.variant-keyor justvariantfor boolean - States:
.ClassName:state
- Root:
- No duplicate names - Throws if same name registered twice
- Anonymous components - Auto-named
Def1,Def2, etc. when name omitted
Usage Pattern
IMPORTANT: Check examples/ for real-world patterns before writing new components.
import { define } from 'forge'
export const Button = define('Button', {
base: 'button', // HTML tag (default: 'div')
padding: 20,
background: 'blue',
states: {
hover: { background: 'darkblue' }
},
variants: {
danger: { background: 'red' }, // boolean
size: { // keyed
small: { padding: 10 },
large: { padding: 30 }
}
}
})
// <Button>Click</Button>
// <Button danger>Danger!</Button>
// <Button size="large">Big</Button>
For complex patterns (parts with variants, custom render, states within variants), see:
examples/index.tsx- ExampleCard with parts, custom render, nested variantsexamples/profile.tsx- Multi-part component with size/theme variantsexamples/navigation.tsx- Multiple component patterns (tabs, pills, breadcrumbs)
Testing
Tests use Bun's test runner. Helpers in src/tests/test_helpers.ts for rendering JSX to HTML strings and parsing CSS.