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
- 🔗 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
```bash
pnpm add @page-speed/pressable
npm install @page-speed/pressable
yarn add @page-speed/pressable ```
```json { "react": ">=17.0.0", "react-dom": ">=17.0.0" } ```
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.
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 ```
```tsx import { Pressable } from "@page-speed/pressable";
function Navigation() { return About Us; } ```
Automatically gets `target="_blank"` and `rel="noopener noreferrer"`:
```tsx Visit Google ```
```tsx Contact Us ```
Automatically normalized to `tel:` format:
```tsx Call Us // Renders: Call Us ```
Automatically normalized to `mailto:` format:
```tsx Email Us // Renders: Email Us ```
```tsx <Pressable onClick={() => alert("Clicked")} asButton variant="default"> Click Me ```
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 ```
```tsx Small Default Medium Large
// Icon sizes ```
Full control over children:
```tsx
Learn more about what we offer
\`\`\````tsx <Pressable href="/important" aria-label="Important action" aria-describedby="description" id="important-link"
Click here for important information ```
```tsx const linkRef = useRef(null);
Link with Ref \`\`\`| Prop | Type | Default | Description |
|---|---|---|---|
| `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 `` |
| Prop | Type | Default | Description |
|---|---|---|---|
| `variant` | `'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'` | - | Button variant style |
| `size` | `'default' | 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg'` | - | Button size |
| Prop | Type | Default | Description |
|---|---|---|---|
| `componentType` | `'a' | 'button' | 'span' | 'div'` | auto | Explicit component type to render |
| `fallbackComponentType` | `'span' | 'div' | 'button'` | `'span'` | Component to render when no href/onClick |
| Prop | Type | Default | Description |
|---|---|---|---|
| `aria-label` | `string` | - | ARIA label for accessibility |
| `aria-describedby` | `string` | - | ARIA describedby reference |
| `id` | `string` | - | Element ID |
Any `data-*` attributes are automatically forwarded to the rendered element.
Full URLs matching the current origin are automatically converted to relative paths:
```tsx // On https://example.com About // Renders: About ```
Supports various phone number formats:
```tsx // → tel:+14322386131 // → tel:+5122322212 // → tel:+5122322212 // → tel:+14322386131 // → tel:+5122322212;ext=123 ```
Automatically detects email addresses:
```tsx // → mailto:hello@example.com // → mailto:test@ex.com (unchanged) ```
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} ); } ```
| Property | Type | Description |
|---|---|---|
| `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 |
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 ); } ```
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 ```
The component supports extensive CSS variable customization for button styles. See the button-variants.ts file for the complete list of CSS 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; } ```
```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 */ } ```
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"; ```
- 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
- Modern browsers (Chrome, Firefox, Safari, Edge)
- React 17+
- Server-side rendering (SSR)
- Static site generation (SSG)
MIT
Contributions are welcome! Please follow the DashTrack ecosystem guidelines.
- @page-speed/img - Performance-optimized image component
- @page-speed/markdown-to-jsx - Markdown renderer with Pressable integration
- @opensite/blocks - Chai design payload renderer
