Domphy

Link Button

Use linkButton on an <a> element to give it the visual appearance of a button while preserving full link semantics — href, middle-click to open in new tab, right-click context menu, and browser history.

Identical styling to button(), but the host element must be <a>. A console warning fires if applied to any other tag.

Props

PropTypeDefaultDescription
colorValueOrState<ThemeColor>"primary"Button color tone. Reactive — pass a State<ThemeColor> to switch theme at runtime.

Usage

import { linkButton } from "@domphy/ui"

{ a: "Open app", href: "/app", $: [linkButton()] }

{ a: "Settings", href: "/settings", $: [linkButton({ color: "neutral" })] }

For a button that triggers JavaScript (no URL), use button() instead. linkButton is for navigational actions that should be a real anchor.

Details

Customization

!!!include(snippets/customization.md)!!!

Details

Formulas

!!!include(snippets/formulas.md)!!!

import { type PartialElement, toState, type ValueOrState } from "@domphy/core";
import {
  type ThemeColor,
  themeColor,
  themeDensity,
  themeSize,
  themeSpacing,
} from "@domphy/theme";

/**
 * An `<a>` element styled to look like a button — same visual appearance as
 * `button()` but preserves link semantics (href, middle-click, right-click).
 * Apply to an `<a>` element.
 *
 * @hostTag a
 * @param props.color - Button color tone. Optional `ValueOrState<ThemeColor>`, default "primary".
 * @example { a: "Open app", href: "/app", $: [linkButton({ color: "primary" })] }
 */
function linkButton(
  props: { color?: ValueOrState<ThemeColor> } = {},
): PartialElement {
  const color = toState(props.color ?? "primary", "color");

  return {
    _onInsert: (node) => {
      if (node.tagName !== "a") {
        console.warn(`"linkButton" primitive patch must use a tag`);
      }
    },
    style: {
      fontSize: (listener) => themeSize(listener, "inherit"),
      textDecoration: "none",
      paddingBlock: (listener) => themeSpacing(themeDensity(listener) * 1),
      paddingInline: (listener) => themeSpacing(themeDensity(listener) * 3),
      borderRadius: (listener) => themeSpacing(themeDensity(listener) * 1),
      width: "fit-content",
      display: "inline-flex",
      justifyContent: "center",
      alignItems: "center",
      gap: (listener) => themeSpacing(themeDensity(listener) * 1),
      userSelect: "none",
      cursor: "pointer",
      outlineOffset: "-1px",
      outlineWidth: "1px",
      outline: (listener) =>
        `1px solid ${themeColor(listener, "shift-4", color.get(listener))}`,
      color: (listener) => themeColor(listener, "shift-9", color.get(listener)),
      backgroundColor: (listener) =>
        themeColor(listener, "inherit", color.get(listener)),
      "&:hover": {
        color: (listener) =>
          themeColor(listener, "shift-10", color.get(listener)),
        backgroundColor: (listener) =>
          themeColor(listener, "shift-2", color.get(listener)),
      },
      "&:focus-visible": {
        boxShadow: (listener) =>
          `inset 0 0 0 ${themeSpacing(0.5)} ${themeColor(listener, "shift-6", color.get(listener))}`,
      },
      "&[aria-disabled=true]": {
        opacity: 0.7,
        cursor: "not-allowed",
        pointerEvents: "none",
        backgroundColor: (listener) =>
          themeColor(listener, "shift-2", "neutral"),
        outline: (listener) =>
          `1px solid ${themeColor(listener, "shift-4", "neutral")}`,
        color: (listener) => themeColor(listener, "shift-8", "neutral"),
      },
    },
  };
}

export { linkButton };