WooBottle "Paper & Ink" design system — one API, two platforms.
Warm cream canvas · deep-ink surfaces · ember CTAs · ceremonial gold. Cross-platform React Native + Web, hand-tuned to the WooBottle spec.
WooBottle is a warm, calm, product-trust interface system. It starts from a soft cream canvas instead of stark white, then layers in a tiered ink-and-ember hierarchy so emphasis is earned by role — not shouted through saturation. Buttons are full-pill. Cards are 12px islands. Shadows are a whisper. It's coffeehouse-adjacent: grounded, breathable, confident.
| Role | Token | Hex |
|---|---|---|
| Page canvas | colors.canvas |
#F4EFE6 |
| Section surface | colors.section |
#EAE4D8 |
| Card island | colors.card |
#FFFFFF |
| Inverse surface | colors.inverse |
#171513 |
| Brand ink | colors.brand |
#2A2622 |
| Primary CTA | colors.actionPrimary |
#D35B1F |
| Ceremonial gold | colors.gold |
#C98A3C |
| Error | colors.actionDanger |
#B02818 |
pnpm add woosign-system
# or
npm i woosign-systemHost app also needs peer deps: react, and optionally react-native if
you're shipping to iOS/Android.
import { ThemeProvider, Button, Card, Badge } from 'woosign-system';
export function App() {
return (
<ThemeProvider>
<Card>
<Badge variant="gold">Members</Badge>
<Button onPress={() => order()}>Order now</Button>
</Card>
</ThemeProvider>
);
}The same code renders on web and native — platform extensions
(.web.tsx / .native.tsx) switch implementations automatically.
| Variants | |
|---|---|
| Button | default, secondary, outline, ghost, dark, inverse, destructive, link |
| Card | default (white island), outline, ghost, warm, ceramic, inverse |
| Badge | default, secondary, brand, gold, success, reward, outline, destructive |
| Input | default, error · sm / default / lg |
| Switch | default · sm / default / lg |
| Text | h1–h4, p, lead, large, small, muted |
| Box | Flex-first layout primitive with padding/margin/gap/radius tokens |
| Purpose | |
|---|---|
| Chip | Square-cornered tag — default, solid, outline |
| Pill | Selectable filter — active / inactive, Pressable |
| Tabs | Underline tab rail, light + inverse surfaces |
| Fab | 56px floating action button — ember / ink / gold, layered shadow |
| FeatureBand | Deep-ink, ember, or reward feature band — the brand's hero surface |
| Progress | Gold/ember/ink fill on light or inverse rail |
| StatusDot | Tinted circle wrapper for icons — success / danger / brand / neutral |
| Toast | Floating notification with leading StatusDot |
| Eyebrow | Tracked, uppercased label — default / brand / gold / inverse |
| Divider | Hairline separator, horizontal or vertical, light or inverse |
All components expose the same ButtonProps/CardProps/etc. on both
platforms — TypeScript is the contract.
One source of truth for both platforms (src/core/theme/tokens.ts):
import { colors, typography, borderRadius, shadows, wbSpace } from 'woosign-system';
colors.actionPrimary // '#D35B1F'
borderRadius.pill // 999 — buttons are ALWAYS pill
typography.fontSize.headingMd // { size: 24, lineHeight: 36 }
wbSpace[4] // 24 — WooBottle's named spacing scale
shadows.card // layered low-alpha card elevationShadcn-compat aliases (primary, secondary, muted, ring, …) are
preserved so existing integrations keep working.
Web — drop an @font-face rule pointing at woosign-system/src/assets/fonts:
@font-face {
font-family: 'Woobottle';
src: url('woosign-system/src/assets/fonts/Woobottle-Regular.woff2') format('woff2');
font-display: swap;
}React Native — one-time setup after install:
npx react-native-assetFonts get linked into Xcode + UIAppFonts and copied into
android/app/src/main/assets/fonts/.
Then cross-platform access via the helper:
import { resolveFontFamily } from 'woosign-system';
<Text style={{ fontFamily: resolveFontFamily('display') }}>
A warmer kind of morning.
</Text>pnpm storybook # → http://localhost:6006
pnpm build-storybook # static export → storybook-static/pnpm storybook:native:generate
pnpm storybook:native:ios # or :androidpnpm build # build with react-native-builder-bob (cjs + esm + dts)
pnpm typecheck # tsc --noEmit
pnpm lint # eslint src/**
pnpm test # jest smoke tests (tokens + resolveFontFamily)- CI —
.github/workflows/ci.ymlruns typecheck, lint (core + components), smoke tests, build, and verifies the published tarball has no stories / examples / duplicated font assets. Triggered on every push and PR. - Release —
.github/workflows/release.ymlpublishes to npm with provenance when av*.*.*tag is pushed. Requires theNPM_TOKENsecret and annpmGitHub environment (for approval gating if you want it).
Cutting a release:
npm version patch # bumps package.json + tags
git push --follow-tagssrc/
├── assets/fonts/ Woobottle display + signature faces
├── core/
│ ├── theme/ tokens · types · ThemeContext
│ ├── variants/ createVariants (shared web/native)
│ ├── utils/ platform · colors · resolveFontFamily
│ └── hooks/ useTheme
├── components/
│ ├── Button/ Component.tsx + .web.tsx + .native.tsx
│ ├── Card/ Badge/ Input/ Switch/ Text/ Box/
│ └── Chip/ Pill/ Tabs/ Fab/ FeatureBand/
│ Progress/ StatusDot/ Toast/ Eyebrow/ Divider/
└── examples/ Marketing page + mobile app composed from the library
Each component ships:
Component.tsx— platform-agnostic facadeComponent.web.tsx— pure React + inline stylesComponent.native.tsx— React Native primitivesComponent.styles.ts— shared variantstypes.ts— the contract
- Buttons are full-pill. No square, no "slightly rounded" buttons.
- Cream over white. The page is
#F4EFE6, never#FFFFFF. - Green is role-specific. Ink for bands, brand for headings, ember for CTAs. Don't swap.
- Gold is ceremonial. Rewards, loyalty, achievements. Never a generic accent.
- Hover is for promise, not drama. No lift, no scale on hover. Press is
scale(0.95). - Shadows whisper. Layered, low-alpha — never one heavy drop.