Primitives
Button
Three variants — primary (the one saturated color on screen), secondary (outlined), tertiary (text-only). Only primary gets hover/tap micro-motion.
Preview
Code
src/components/ui/Button.tsx
"use client";
import { motion, type HTMLMotionProps } from "framer-motion";
/**
* Button — button-primary/secondary/tertiary tokens from
* hyliox-waitlist-card.md. Only `primary` gets hover/tap micro-motion
* (hyliox-motion-system.md reserves `emphasized` easing for the primary
* button and the logo mark, nothing else).
*/
type ButtonVariant = "primary" | "secondary" | "tertiary";
const base = "inline-flex items-center justify-center rounded-md font-medium transition-colors disabled:opacity-40 disabled:pointer-events-none";
const variants: Record<ButtonVariant, string> = {
primary: "h-11 px-[22px] bg-primary text-primary-contrast",
secondary: "h-11 px-[22px] border border-border-subtle text-on-surface bg-transparent hover:border-primary",
tertiary: "h-auto p-0 text-sm text-secondary hover:text-on-surface bg-transparent",
};
export function Button({
variant = "primary",
className = "",
children,
...props
}: {
variant?: ButtonVariant;
className?: string;
} & HTMLMotionProps<"button">) {
const classes = `${base} ${variants[variant]} ${className}`;
if (variant === "primary") {
return (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
transition={{ duration: 0.2, ease: [0.34, 1.56, 0.64, 1] }}
className={classes}
{...props}
>
{children}
</motion.button>
);
}
return (
<motion.button className={classes} {...props}>
{children}
</motion.button>
);
}
Installation
Usage
import { Button } from "@/components/ui/Button";
<Button variant="primary">Start a Project</Button>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "primary" | "secondary" | "tertiary" | "primary" | Visual style. Never use more than one primary button on the same screen. |
| className | string | "" | Extra classes merged onto the button. |
| ...props | HTMLMotionProps<'button'> | — | Any native button prop (onClick, disabled, type, etc.) plus framer-motion props. |
Accessibility
Renders a native <button>; disabled state uses the native disabled attribute (not just a visual style), so it's excluded from the tab order automatically.