Button
Triggers an action. Five variants (primary, secondary, outline, ghost, link) across five sizes. Tokens map 1:1 to the Foren Figma library.
Preview#
<ForenButton leadingIcon={<CubeIcon />} trailingIcon={<ArrowRightIcon />}>
Get started
</ForenButton>Installation#
Two ways to bring Foren into your project.
1. Install the package
npm install @foren/ui
# or
pnpm add @foren/ui
# or
yarn add @foren/ui2. Add the design tokens to your stylesheet
Import the token CSS once at the root of your app. This exposes every --color-foren-*, --shadow-foren-*, and font-family token used by the components.
@import "@foren/ui/tokens.css";3. Use it
import { ForenButton } from "@foren/ui";
export default function Page() {
return <ForenButton>Get started</ForenButton>;
}Prefer copy-paste? See the full component source below — drop it into components/ui/foren-button.tsxand you're done.
Usage#
Import once, compose anywhere.
import { ForenButton } from "@/components/ui/foren-button";
export function Toolbar() {
return (
<div className="flex gap-2">
<ForenButton variant="primary">Save</ForenButton>
<ForenButton variant="outline">Cancel</ForenButton>
</div>
);
}Source#
The complete component. Drop it in to skip the npm dependency.
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
// base — fully rounded pill, spring scale on press
"inline-flex items-center justify-center whitespace-nowrap font-semibold rounded-[9999px] select-none outline-none cursor-pointer will-change-transform transition-[background-color,color,box-shadow,border-color,transform] duration-150 ease-out active:scale-[0.97] active:duration-75 disabled:cursor-not-allowed disabled:pointer-events-none disabled:active:scale-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-foren-bg-default)] focus-visible:ring-[var(--color-foren-border-focus)] [&_svg]:shrink-0 [&_svg]:pointer-events-none",
{
variants: {
variant: {
primary:
"bg-[var(--color-foren-bg-button-primary,#1e1711)] text-[var(--color-foren-text-inverse)] hover:bg-[var(--color-foren-bg-button-primary-hover)] active:bg-[var(--color-foren-bg-button-primary-pressed)] disabled:bg-[var(--color-foren-bg-button-disabled)] disabled:text-[var(--color-foren-text-disabled)]",
secondary:
"bg-[var(--color-foren-bg-button-secondary)] text-[var(--color-foren-text-secondary)] hover:bg-[var(--color-foren-bg-button-secondary-hover)] hover:text-[var(--color-foren-text-primary)] active:bg-[var(--color-foren-border-default)] disabled:bg-[var(--color-foren-bg-button-disabled)] disabled:text-[var(--color-foren-text-disabled)]",
outline:
"bg-transparent text-[var(--color-foren-text-primary)] border border-[var(--color-foren-border-default)] hover:bg-[var(--color-foren-bg-button-secondary)] active:bg-[var(--color-foren-bg-button-secondary-active)] disabled:bg-transparent disabled:text-[var(--color-foren-text-disabled)] disabled:border-[var(--color-foren-bg-button-disabled)]",
ghost:
"bg-transparent text-[var(--color-foren-text-primary)] hover:bg-[var(--color-foren-bg-button-secondary)] active:bg-[var(--color-foren-bg-button-secondary-active)] disabled:bg-transparent disabled:text-[var(--color-foren-text-disabled)]",
link:
"bg-transparent text-[var(--color-foren-text-primary)] underline-offset-[3px] hover:underline px-0 disabled:text-[var(--color-foren-text-disabled)] disabled:no-underline",
},
size: {
xxs: "h-8 px-3 gap-1.5 text-[12px] leading-[16px] tracking-[0.04em] [&_svg]:size-[12px]",
xs: "h-9 px-3.5 gap-1.5 text-[12px] leading-[16px] tracking-[0.04em] [&_svg]:size-[14px]",
sm: "h-10 px-4 gap-2 text-[14px] leading-[21px] tracking-[0.0125em] [&_svg]:size-[16px]",
md: "h-12 px-6 gap-2 text-[14px] leading-[21px] tracking-[0.0125em] [&_svg]:size-[18px]",
lg: "h-14 px-7 gap-2.5 text-[16px] leading-[24px] tracking-[0.0125em] [&_svg]:size-[20px]",
},
destructive: {
true: "",
false: "",
},
},
compoundVariants: [
// Destructive — Foren spec: bg Feedback/Error Subtle (var(--color-foren-bg-error-subtle)) + text Feedback/Error (#c62828)
{
variant: "primary",
destructive: true,
className:
"bg-[var(--color-foren-bg-error-subtle)] text-[var(--color-foren-error)] hover:bg-[var(--color-foren-bg-error-subtle-hover)] active:bg-[var(--color-foren-bg-error-subtle-pressed)] disabled:bg-[var(--color-foren-bg-button-disabled)] disabled:text-[var(--color-foren-text-disabled)]",
},
{
variant: "secondary",
destructive: true,
className:
"bg-[var(--color-foren-bg-error-subtle)] text-[var(--color-foren-error)] hover:bg-[var(--color-foren-bg-error-subtle-hover)] active:bg-[var(--color-foren-bg-error-subtle-pressed)]",
},
{
variant: "outline",
destructive: true,
className:
"bg-transparent text-[var(--color-foren-error)] border-[var(--color-foren-error)]/30 hover:bg-[var(--color-foren-bg-error-subtle)] hover:border-[var(--color-foren-error)]/60 active:bg-[var(--color-foren-bg-error-subtle-hover)]",
},
{
variant: "ghost",
destructive: true,
className: "bg-transparent text-[var(--color-foren-error)] hover:bg-[var(--color-foren-bg-error-subtle)] active:bg-[var(--color-foren-bg-error-subtle-hover)]",
},
{
variant: "link",
destructive: true,
className: "bg-transparent text-[var(--color-foren-error)]",
},
// link size overrides — remove height/padding, keep type scale only
{ variant: "link", size: "xxs", className: "h-auto text-[12px]" },
{ variant: "link", size: "xs", className: "h-auto text-[12px]" },
{ variant: "link", size: "sm", className: "h-auto text-[14px]" },
{ variant: "link", size: "md", className: "h-auto text-[14px]" },
{ variant: "link", size: "lg", className: "h-auto text-[16px]" },
],
defaultVariants: {
variant: "primary",
size: "md",
destructive: false,
},
}
);
export interface ForenButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children">,
VariantProps<typeof buttonVariants> {
leadingIcon?: React.ReactNode;
trailingIcon?: React.ReactNode;
children?: React.ReactNode;
}
export const ForenButton = React.forwardRef<HTMLButtonElement, ForenButtonProps>(function ForenButton(
{
className,
variant,
size,
destructive,
leadingIcon,
trailingIcon,
children,
type = "button",
...props
},
ref
) {
return (
<button
ref={ref}
type={type}
className={cn(buttonVariants({ variant, size, destructive }), className)}
{...props}
>
{leadingIcon}
{children != null && <span>{children}</span>}
{trailingIcon}
</button>
);
});
export { buttonVariants };
Variants#
Same size, different visual weight.
<ForenButton variant="primary">Primary</ForenButton>
<ForenButton variant="secondary">Secondary</ForenButton>
<ForenButton variant="outline">Outline</ForenButton>
<ForenButton variant="ghost">Ghost</ForenButton>
<ForenButton variant="link">Link</ForenButton>Sizes#
Five sizes — XX-Small to Large. Icon scales with the button.
<ForenButton size="xxs">Size xxs</ForenButton>
<ForenButton size="xs">Size xs</ForenButton>
<ForenButton size="sm">Size sm</ForenButton>
<ForenButton size="md">Size md</ForenButton>
<ForenButton size="lg">Size lg</ForenButton>With icons#
Pass any ReactNode — Lucide, Heroicons, your own SVG.
<ForenButton leadingIcon={<Cube />}>Leading</ForenButton>
<ForenButton trailingIcon={<ArrowRight />}>Trailing</ForenButton>
<ForenButton leadingIcon={<Cube />} trailingIcon={<ArrowRight />}>
Both
</ForenButton>
{/* Icon-only — pass aria-label + tighter padding */}
<ForenButton aria-label="Add" leadingIcon={<Plus />} className="px-3" />Destructive#
Communicates a destructive intent. Use sparingly.
<ForenButton destructive leadingIcon={<Trash />}>Delete</ForenButton>
<ForenButton variant="outline" destructive leadingIcon={<Trash />}>Delete</ForenButton>States#
Hover, active, focus driven by CSS pseudo-classes. Disabled is a prop.
<ForenButton>Default</ForenButton>
<ForenButton disabled>Disabled</ForenButton>
{/* Hover / pressed / focused are CSS states — no prop required */}Interactions#
Click any button — it scales to 0.97 on press, springs back on release. 150ms ease-out → 75ms snap-back.
// Built into the component — no extra props.
// Press the button and watch the scale spring.
<ForenButton onClick={() => console.log("clicked")}>
Click me
</ForenButton>Under the hood: active:scale-[0.97] with transition-[transform] at 150ms ease-out, dropping to 75ms on press for a tactile pop. Disabled buttons skip the animation.
Full matrix#
Every variant at every size. Verify spacing + radius.