Button
A button, wrapping Morphos's polymorphic Button primitive with shadcn-style variants.
Installation
npx kosmesis add buttonpnpm dlx kosmesis add buttonyarn dlx kosmesis add buttonbunx kosmesis add buttonInstall the following dependencies:
npm install @morphos/inputs class-variance-authoritypnpm add @morphos/inputs class-variance-authorityyarn add @morphos/inputs class-variance-authoritybun add @morphos/inputs class-variance-authorityCopy and paste the following code into your project.
import { cva, type VariantProps } from "class-variance-authority";import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Button as MorphosButton, type ButtonProps as MorphosButtonProps } from "@morphos/inputs";import { cn } from "@/lib/utils";export const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20", outline: "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2", sm: "h-8 rounded-md px-3 text-xs", lg: "h-10 rounded-md px-6", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, },);export interface ButtonProps extends MorphosButtonProps, VariantProps<typeof buttonVariants> {}@Component()export class Button extends StatelessComponent<ButtonProps> { render() { const { variant, size, class: cls, ...rest } = this.props; return <MorphosButton class={cn(buttonVariants({ variant, size }), cls)} {...rest} />; }}Install the following dependencies:
npm install @morphos/inputs @praxisjs/csspnpm add @morphos/inputs @praxisjs/cssyarn add @morphos/inputs @praxisjs/cssbun add @morphos/inputs @praxisjs/cssCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { Button as MorphosButton, type ButtonProps as MorphosButtonProps } from "@morphos/inputs";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);/** * `$variant*`/`$size*` fields are looked up by name in `render()` rather than composed through a * `cva`-style helper — `@praxisjs/css` has no variant-authority equivalent, so a plain * `Record<string, string>` lookup plays that role for every Kosmesis component in this style * system. Exported so components that render a native button-like element out of the Button * primitive (e.g. `AlertDialogAction`/`AlertDialogCancel`) can reuse the exact same scoped * classes via their own `@Styled(ButtonStyles)` field. */export class ButtonStyles extends Stylesheet { $root = this.css({ display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "0.5rem", whiteSpace: "nowrap", borderRadius: `calc(${t.radius} - 2px)`, fontSize: "0.875rem", fontWeight: 500, outline: "none", cursor: "pointer", transition: "color 120ms ease, background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease", }) .on("&[data-disabled], &:disabled", { pointerEvents: "none", opacity: 0.5 }) .focusVisible({ borderColor: t.ring, boxShadow: `0 0 0 3px color-mix(in oklab, ${t.ring} 50%, transparent)`, }) .on("& svg", { pointerEvents: "none", flexShrink: 0, width: "1rem", height: "1rem" }); $variantDefault = this.css({ backgroundColor: t.primary, color: t.primaryForeground }).hover({ backgroundColor: `color-mix(in oklab, ${t.primary} 90%, transparent)`, }); $variantDestructive = this.css({ backgroundColor: t.destructive, color: "white" }) .hover({ backgroundColor: `color-mix(in oklab, ${t.destructive} 90%, transparent)` }) .focusVisible({ boxShadow: `0 0 0 3px color-mix(in oklab, ${t.destructive} 20%, transparent)` }); $variantOutline = this.css({ border: `1px solid ${t.input}`, backgroundColor: t.background, }).hover({ backgroundColor: t.accent, color: t.accentForeground }); $variantSecondary = this.css({ backgroundColor: t.secondary, color: t.secondaryForeground }).hover({ backgroundColor: `color-mix(in oklab, ${t.secondary} 80%, transparent)`, }); $variantGhost = this.css({ backgroundColor: "transparent" }).hover({ backgroundColor: t.accent, color: t.accentForeground, }); $variantLink = this.css({ color: t.primary, textUnderlineOffset: "4px" }).hover({ textDecoration: "underline" }); $sizeDefault = this.css({ height: "2.25rem", padding: "0 1rem" }); $sizeSm = this.css({ height: "2rem", padding: "0 0.75rem", fontSize: "0.75rem" }); $sizeLg = this.css({ height: "2.5rem", padding: "0 1.5rem" }); $sizeIcon = this.css({ height: "2.25rem", width: "2.25rem", padding: "0" });}export type ButtonVariant = "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";export type ButtonSize = "default" | "sm" | "lg" | "icon";export interface ButtonProps extends MorphosButtonProps { variant?: ButtonVariant; size?: ButtonSize;}@Component()export class Button extends StatelessComponent<ButtonProps> { @Styled(ButtonStyles) $s!: ButtonStyles; render() { const { variant = "default", size = "default", class: cls, ...rest } = this.props; const variants: Record<ButtonVariant, string> = { default: this.$s.$variantDefault, destructive: this.$s.$variantDestructive, outline: this.$s.$variantOutline, secondary: this.$s.$variantSecondary, ghost: this.$s.$variantGhost, link: this.$s.$variantLink, }; const sizes: Record<ButtonSize, string> = { default: this.$s.$sizeDefault, sm: this.$s.$sizeSm, lg: this.$s.$sizeLg, icon: this.$s.$sizeIcon, }; return <MorphosButton class={cx(this.$s.$root, variants[variant], sizes[size], cls)} {...rest} />; }}Examples
About
Wraps @morphos/inputs's Button — a polymorphic primitive supporting an as prop for
link-buttons. Kosmesis adds variant/size props backed by class-variance-authority,
matching shadcn/ui's own API exactly.
Usage
import { Button } from "@/components/ui/button";
@Component()
class MyPage extends StatefulComponent {
render() {
return (
<div>
<Button>Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button as="a" href="/dashboard">Go to dashboard</Button>
</div>
);
}
}Props
| Prop | Type | Default |
|---|---|---|
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "default" |
size | "default" | "sm" | "lg" | "icon" | "default" |
as | "button" | "a" | "span" | "div" | "button" |
disabled | boolean | — |
onClick | (event: MouseEvent) => void | — |
All other props (href, type, aria-*, class, id, children, ...) pass through to
Morphos's Button unchanged — see its docs
for the full list.
disabled only becomes the native disabled attribute when as="button"; otherwise it's
exposed as aria-disabled="true", since non-button elements don't support disabled natively.