Local: relating or restricted to a particular area or one's neighbourhood.
A curated React component library designed specifically for personal sites and portfolio pages. Built with React 19, TypeScript, Tailwind CSS v4, and shadcn/ui patterns.
- 7 Foundational Components: Hero, Layout, Section, Typography, Card, Toggle, Button
- Built-in Theme System: Dark/light mode with automatic OS preference detection
- Type-Safe: Full TypeScript support with comprehensive type definitions
- Tailwind-First: Easy customization using Tailwind CSS utility classes
- Tree-Shakeable: Import only the components you need
- Accessible: Built on Radix UI primitives for accessibility
- React 19+
- React DOM 19+
- Tailwind CSS 4+
- TypeScript 5+ (recommended)
npm install local-componentsClone the repository and build locally:
git clone https://github.com/dpaez/local-components.git
cd local-components
bun installThis library supports two integration modes:
If your app already uses Tailwind, you should not import globals.css (it includes Tailwind initialization).
Instead, import the library index.css into your app’s main Tailwind CSS entry.
@import "tailwindcss";
@plugin "local-components/tw";You can configure some options like this:
@plugin "local-components/tw" {
root: "#app";
darkSelector: "[data-theme='dark']";
}npm i @toolwind/corner-shapeThen in your app CSS (the same file where you import Tailwind):
@plugin "@toolwind/corner-shape";You can override or extend the theme by adding your own @theme inline { ... } after importing the library theme: You can override or extend the theme by adding your own @theme inline { ... } after importing the library theme:
@theme inline {
--color-primary: oklch(0.62 0.2 255);
--color-brand: oklch(0.7 0.18 160);
}If your app does not use Tailwind, import the full stylesheet:
@import "local-components/styles/globals.css";globals.css includes Tailwind initialization, the library theme and the plugin.
import { Layout } from "local-components/layout"
function App() {
return (
<Layout defaultTheme="system" storageKey="theme-preference">
<YourApp />
</Layout>
)
}import { Hero } from "local-components/hero"
function HomePage() {
return (
<Hero
title="Welcome to My Site"
subtitle="A brief description"
cta={{ label: "Get Started", href: "#about" }}
/>
)
}The library includes a theme system with automatic dark mode support.
The Layout component provides the theme context for your app:
import { Layout } from "local-components/layout"
<Layout
defaultTheme="system" // "light" | "dark" | "system"
storageKey="my-app-theme"
maxWidth="lg"
padding="md"
centered
>
{children}
</Layout>Add a theme toggle button anywhere in your app:
import { Toggle } from "local-components/toggle"
// Icon-only toggle (default)
<Toggle variant="icon" />
// Button with label
<Toggle variant="button" showLabel />
// Switch style
<Toggle variant="switch" />The theme uses CSS variables defined in globals.css. Override them in your own CSS:
:root {
--background: #ffffff;
--foreground: #000000;
--primary: oklch(0.5 0.2 270);
/* ... other variables */
}
.dark {
--background: #000000;
--foreground: #ffffff;
/* ... other variables */
}import { Button } from "local-components/button"
// Variants
<Button variant="primary">Primary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="outline">Outline</Button>
<Button variant="disabled">Disabled</Button>
// Sizes
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="icon">
<Icon />
</Button>
// With icons
<Button icon={<Download />} iconPosition="start">
Download
</Button>import { Hero } from "local-components/hero"
// Basic hero
<Hero
title="Hello, I'm John"
subtitle="Full-stack developer"
/>
// With CTA
<Hero
title="Welcome"
subtitle="Check out my work"
cta={{ label: "View Projects", href: "/projects" }}
variant="default"
alignment="center"
/>
// With background image
<Hero
title="Portfolio"
subtitle="My latest work"
background={{ type: "image", value: "/bg.jpg" }}
/>import { Card } from "local-components/card"
import { Button } from "local-components/button"
// Basic card
<Card title="Project Name" description="A brief description">
Additional content
</Card>
// With image
<Card
title="Blog Post"
description="Read more about this topic"
image={{ src: "/image.jpg", alt: "Description" }}
footer={<Button>Read More</Button>}
/>
// Clickable card
<Card
title="Linked Card"
description="Click to navigate"
href="/destination"
/>
// Variants
<Card variant="default">Default</Card>
<Card variant="bordered">Bordered</Card>
<Card variant="ghost">Ghost</Card>
<Card variant="elevated">Elevated</Card>import { Section } from "local-components/section"
// Basic section
<Section title="About" subtitle="Learn more about me">
Content here
</Section>
// With background colors
<Section background="primary" title="Features">
Primary background content
</Section>
<Section background="alternate" spacing="spaced">
Alternate background with more spacing
</Section>import { Heading } from "local-components/typography/heading"
import { Text } from "local-components/typography/text"
import { Lead } from "local-components/typography/lead"
import { Blockquote } from "local-components/typography/blockquote"
import { Code } from "local-components/typography/code"
// Heading
<Heading size="2xl">Page Title</Heading>
<Heading as="h2" size="lg" weight="semibold">
Section Heading
</Heading>
// Text
<Text size="lg" color="muted">
Large muted text
</Text>
// Lead (intro text)
<Lead>
This is an introductory paragraph with larger text.
</Lead>
// Blockquote
<Blockquote cite="Author Name">
A memorable quote goes here.
</Blockquote>
// Code
<Code>const example = "code"</Code>Import individual components for optimal bundle size:
// Good - Tree-shakeable
import { Button } from "local-components/button"
import { Hero } from "local-components/hero"
import { Card } from "local-components/card"
// Good - Typography sub-components (tree-shakeable)
import { Heading } from "local-components/typography/heading"
import { Text } from "local-components/typography/text"
import { Lead } from "local-components/typography/lead"
import { Blockquote } from "local-components/typography/blockquote"
import { Code } from "local-components/typography/code"
// Utilities
import { cn } from "local-components/utils"
import { ThemeProvider, useTheme } from "local-components/theme-context"The library uses Tailwind CSS v4 with CSS-first configuration. Ensure your project:
- Has Tailwind CSS v4 installed
- Imports the library's CSS variables (via
globals.css) - Uses the same CSS variable naming convention for theming
| Component | Props | Description |
|---|---|---|
Button |
variant, size, icon, iconPosition, asChild |
Interactive button element |
Hero |
title, subtitle, cta, background, variant, alignment |
Landing section component |
Card |
title, description, image, footer, href, variant |
Content container |
Section |
title, subtitle, background, spacing |
Page section wrapper |
Toggle |
variant, showLabel |
Theme toggle button |
Layout |
maxWidth, padding, centered, defaultTheme, storageKey |
App wrapper with theme |
Heading |
size, weight, align, as |
Typography heading |
Text |
size, weight, color, align, as |
Typography text |
Lead |
Same as Text | Introductory text |
Blockquote |
cite |
Quote block |
Code |
- | Inline code |
| Hook | Returns | Description |
|---|---|---|
useTheme() |
{ theme, resolvedTheme, setTheme, toggleTheme } |
Access theme state |
| Function | Description |
|---|---|
cn(...inputs) |
Merge Tailwind classes with conflict resolution |
Explore all components with interactive props at our Storybook:
# Run Storybook locally
bun run storybook# Build for production
bun run build:lib# Type check
bun run lint
# Format check
bun run format:checkContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Seems like a good opportunity to work with agents and update my personal site. 🤝
MIT © Diego Paez