@page-speed/pressable
Performance-optimized universal link/button component with automatic URL detection and normalization for the OpenSite Semantic Site Builder ecosystem. Provides tree-shakable, performance-optimized components with abstract styling support


Features
- 🔗 Universal Component: Automatically renders
<a>, <button>, or fallback elements based on props
- 🌐 Smart URL Detection: Automatically detects and normalizes internal, external, mailto, and tel links
- 📱 Phone Number Normalization: Converts various phone formats to standard
tel: format
- ✉️ Email Normalization: Automatically adds
mailto: prefix to email addresses
- 🎨 ShadCN Button Variants: Full integration with ShadCN button styles and variants
- ♿ Accessibility First: Proper ARIA attributes, keyboard navigation, and screen reader support
- 🎯 SEO Optimized: Internal links always render as
<a> tags for proper SEO
- 🌲 Tree-Shakable: Granular exports for minimal bundle size
- 🚀 Zero Runtime Overhead: Efficient memoization and minimal re-renders
- 🔒 Type Safe: Full TypeScript support with comprehensive types
Installation
```bash
Using pnpm (recommended)
pnpm add @page-speed/pressable
Using npm
npm install @page-speed/pressable
Using yarn
yarn add @page-speed/pressable
```
Peer Dependencies
```json
{
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
```
Setup Requirements
1. Tailwind CSS Configuration
CRITICAL: Add @page-speed/pressable to your Tailwind content paths so button styles are included:
```ts
// tailwind.config.ts
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./app//*.{js,ts,jsx,tsx,mdx}",
"./components//*.{js,ts,jsx,tsx,mdx}",
// Add one of these lines to scan Pressable's button-variant classes:
// For standard npm/yarn installations:
"./node_modules/@page-speed/pressable/dist/**/*.{js,cjs}",
// For pnpm monorepos (use both if unsure):
"./node_modules/.pnpm/@page-speed+pressable*/node_modules/@page-speed/pressable/**/*.{js,jsx,ts,tsx}",
],
// ...rest of config
};
```
Without this, button variants won't have styles applied because Tailwind will purge the classes.
2. Router Setup (For Navigation)
Wrap your app with `RouterProvider` from `@page-speed/router` to enable internal navigation.
For Next.js App Router (requires client component wrapper):
```tsx
// components/providers/RouterWrapper.tsx
"use client";
import { RouterProvider } from "@page-speed/router";
import { ReactNode } from "react";
export function RouterWrapper({ children }: { children: ReactNode }) {
return {children};
}
```
```tsx
// app/layout.tsx
import { RouterWrapper } from "@/components/providers/RouterWrapper";
export default function RootLayout({ children }) {
return (
{children}
);
}
```
For standard React apps (Create React App, Vite, etc.):
```tsx
// App.tsx
import { RouterProvider } from "@page-speed/router";
function App() {
return (
{/* your app */}
);
}
```
Install `@page-speed/router` directly for better type support:
```bash
pnpm add @page-speed/router
```
Basic Usage
Simple Link
```tsx
import { Pressable } from "@page-speed/pressable";
function Navigation() {
return About Us;
}
```
External Link
Automatically gets `target="_blank"` and `rel="noopener noreferrer"`:
```tsx
Visit Google
```
Button-Styled Link
```tsx
Contact Us
```
Phone Link
Automatically normalized to `tel:` format:
```tsx
Call Us
// Renders: Call Us
```
Email Link
Automatically normalized to `mailto:` format:
```tsx
Email Us
// Renders: Email Us
```
Button with onClick
```tsx
<Pressable onClick={() => alert("Clicked")} asButton variant="default">
Click Me
```
Advanced Usage
Button Variants
Supports all ShadCN button variants:
```tsx
// Default variant
Primary
// Outline variant
Outline
// Secondary variant
Secondary
// Ghost variant
Ghost
// Link variant
Link Style
// Destructive variant
Delete
```
Button Sizes
```tsx
Small
Default
Medium
Large
// Icon sizes
```
Custom Layouts
Full control over children:
```tsx
Our Services
Learn more about what we offer
\`\`\`
Accessibility
```tsx
<Pressable
href="/important"
aria-label="Important action"
aria-describedby="description"
id="important-link"
Click here for important information
```
Refs
```tsx
const linkRef = useRef(null);
Link with Ref
\`\`\`
API Reference
Props
Core Props
| `children` | `ReactNode` | - | Content inside the component |
| `href` | `string` | - | URL to navigate to (supports internal, external, mailto, tel) |
| `onClick` | `MouseEventHandler` | - | Click handler function |
| `className` | `string` | - | Additional CSS classes |
| `asButton` | `boolean` | `false` | Apply button styles even when rendering as `` |
Button Styling
| `variant` | `'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'` | - | Button variant style |
| `size` | `'default' | 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg'` | - | Button size |
Component Type
| `componentType` | `'a' | 'button' | 'span' | 'div'` | auto | Explicit component type to render |
| `fallbackComponentType` | `'span' | 'div' | 'button'` | `'span'` | Component to render when no href/onClick |
Accessibility
| `aria-label` | `string` | - | ARIA label for accessibility |
| `aria-describedby` | `string` | - | ARIA describedby reference |
| `id` | `string` | - | Element ID |
Data Attributes
Any `data-*` attributes are automatically forwarded to the rendered element.
URL Detection & Normalization
Internal Links
Full URLs matching the current origin are automatically converted to relative paths:
```tsx
// On https://example.com
About
// Renders: About
```
Phone Number Formats
Supports various phone number formats:
```tsx
// → tel:+14322386131
// → tel:+5122322212
// → tel:+5122322212
// → tel:+14322386131
// → tel:+5122322212;ext=123
```
Email Detection
Automatically detects email addresses:
```tsx
// → mailto:hello@example.com
// → mailto:test@ex.com (unchanged)
```
Hooks
useNavigation
Low-level hook for custom navigation logic:
```tsx
import { useNavigation } from "@page-speed/pressable/hooks";
function CustomLink({ href }) {
const {
linkType,
normalizedHref,
target,
rel,
isInternal,
isExternal,
handleClick,
} = useNavigation({ href });
return (
{href}
);
}
```
useNavigation Return Values
| `linkType` | `'internal' | 'external' | 'mailto' | 'tel' | 'none' | 'unknown'` | Detected link type |
| `normalizedHref` | `string | undefined` | Normalized URL |
| `target` | `'_blank' | '_self' | undefined` | Link target attribute |
| `rel` | `string | undefined` | Link rel attribute |
| `isInternal` | `boolean` | Whether link is internal |
| `isExternal` | `boolean` | Whether link is external |
| `shouldUseRouter` | `boolean` | Whether to use client-side routing |
| `handleClick` | `MouseEventHandler` | Click handler function |
Utilities
cn
Utility for merging Tailwind classes:
```tsx
import { cn } from "@page-speed/pressable/utils";
function CustomButton() {
return (
<Pressable
href="/test"
className={cn(
"base-class",
isActive && "active-class",
{ "conditional": someCondition }
)}
>
Custom Button
);
}
```
Integration with opensite-blocks
The Pressable component integrates seamlessly with the opensite-blocks navigation system:
```tsx
// Set up navigation handler (typically done in opensite-blocks)
window.__opensiteNavigationHandler = (href, event) => {
// Custom navigation logic (e.g., React Router)
navigate(href);
return true; // Indicates navigation was handled
};
// Pressable automatically uses the handler for internal links
About
```
CSS Variables
The component supports extensive CSS variable customization for button styles. See the button-variants.ts file for the complete list of CSS variables.
Master Variables
```css
:root {
--button-font-family: inherit;
--button-font-weight: 500;
--button-letter-spacing: 0;
--button-line-height: 1.25;
--button-text-transform: none;
--button-transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
--button-radius: 0.375rem;
--button-shadow: none;
--button-shadow-hover: none;
}
```
Per-Variant Variables
```css
:root {
/* Default variant */
--button-default-bg: hsl(var(--primary));
--button-default-fg: hsl(var(--primary-foreground));
--button-default-hover-bg: hsl(var(--primary) / 0.9);
/* Outline variant */
--button-outline-bg: hsl(var(--background));
--button-outline-border: hsl(var(--border));
--button-outline-border-width: 1px;
/* ... and more */
}
```
Tree-Shaking
The package is fully tree-shakable. Import only what you need:
```tsx
// Import specific components
import { Pressable } from "@page-speed/pressable/core";
import { useNavigation } from "@page-speed/pressable/hooks";
import { cn } from "@page-speed/pressable/utils";
// Or use granular imports
import { Pressable } from "@page-speed/pressable/core/Pressable";
import { buttonVariants } from "@page-speed/pressable/core/button-variants";
```
Performance
- Bundle Size: ~8KB gzipped (including all dependencies)
- Tree-Shaking: Unused code is automatically eliminated
- Memoization: All computed values are memoized with React.useMemo
- Zero Runtime Overhead: Efficient URL detection and normalization
- SSR Compatible: Works seamlessly with server-side rendering
Browser Support
- Modern browsers (Chrome, Firefox, Safari, Edge)
- React 17+
- Server-side rendering (SSR)
- Static site generation (SSG)
License
MIT
Contributing
Contributions are welcome! Please follow the DashTrack ecosystem guidelines.
Related Packages
Support