Перейти к содержимому

Интерфейс

Кнопка

Универсальная кнопка с вариантами оформления, размерами и поддержкой ссылок. Интерактивное демо с управлением пропсами.


Кнопка

Универсальная кнопка с пятью вариантами оформления, тремя размерами и поддержкой режима ссылки. Построена на CSS-переменных темы.

Интерактивное демо

Меняйте вариант, размер, текст и состояние кнопки в панели управления. Кнопка сброса вернёт все значения к значениям по умолчанию.

Пропсы

Вариант
Размер
Текст
Отключена
<script lang="ts">
  /* eslint-disable svelte/no-navigation-without-resolve */
  import { resolve } from "$app/paths";
  import { cn } from "$lib/utils/cn";

  type Variant = "primary" | "secondary" | "outline" | "ghost" | "danger";
  type Size = "sm" | "md" | "lg";

  type Props = {
    variant?: Variant;
    size?: Size;
    class?: string;
    href?: string;
    type?: "button" | "submit" | "reset";
    disabled?: boolean;
    children?: import("svelte").Snippet;
    onclick?: (e: MouseEvent) => void;
  };

  let {
    variant = "primary",
    size = "md",
    class: className = "",
    href,
    type = "button",
    disabled = false,
    children,
    onclick,
  }: Props = $props();

  const resolvePath = resolve as unknown as (path: string) => string;

  const sizeClasses: Record<Size, string> = {
    sm: "h-7 px-2.5 text-xs",
    md: "h-9 px-4 text-sm",
    lg: "h-11 px-6 text-base",
  };

  const isBordered = $derived(variant === "primary" || variant === "secondary");
  const resolvedHref = $derived(
    href
      ? /^(https?:)?\/\//.test(href) ||
        href.startsWith("mailto:") ||
        href.startsWith("#")
        ? href
        : resolvePath(href)
      : undefined,
  );
  const externalRel = $derived(
    resolvedHref && /^(https?:)?\/\//.test(resolvedHref)
      ? "noreferrer"
      : undefined,
  );

  const outerVariantClasses: Record<"primary" | "secondary", string> = {
    primary: "bg-accent/30",
    secondary: "bg-background-inset",
  };

  const innerVariantClasses: Record<Variant, string> = {
    primary: "bg-accent text-background hover:bg-accent/85",
    secondary: "bg-background text-foreground hover:bg-background-muted",
    outline:
      "border border-border bg-background text-foreground hover:bg-background-muted",
    ghost:
      "text-foreground-muted hover:bg-background-muted hover:text-foreground",
    danger: "bg-red-500/10 text-red-500 hover:bg-red-500/20",
  };

  const outerClass = $derived(
    cn(
      "inset-shadow inline-flex rounded-sm p-[1.5px]",
      outerVariantClasses[variant as "primary" | "secondary"],
      disabled && "opacity-50 cursor-not-allowed",
      className,
    ),
  );

  const innerClass = $derived(
    cn(
      "inline-flex items-center justify-center gap-2 rounded-xs font-medium transition-colors duration-150 ease-out active:scale-[0.98] card",
      innerVariantClasses[variant],
      sizeClasses[size],
      disabled && "pointer-events-none",
    ),
  );

  const flatClass = $derived(
    cn(
      "inset-shadow inline-flex items-center justify-center gap-2 rounded-sm font-medium transition-colors duration-150 ease-out active:scale-[0.98]",
      innerVariantClasses[variant],
      sizeClasses[size],
      disabled && "opacity-50 cursor-not-allowed pointer-events-none",
      className,
    ),
  );
</script>

{#if isBordered}
  {#if href}
    <span class={outerClass}>
      <a
        href={resolvedHref}
        class={innerClass}
        aria-disabled={disabled}
        rel={externalRel}
      >
        {@render children?.()}
      </a>
    </span>
  {:else}
    <span class={outerClass}>
      <button {type} {disabled} {onclick} class={innerClass}>
        {@render children?.()}
      </button>
    </span>
  {/if}
{:else if href}
  <a
    href={resolvedHref}
    class={flatClass}
    aria-disabled={disabled}
    rel={externalRel}
  >
    {@render children?.()}
  </a>
{:else}
  <button {type} {disabled} {onclick} class={flatClass}>
    {@render children?.()}
  </button>
{/if}
<script lang="ts">
  /* eslint-disable svelte/no-navigation-without-resolve */
  import { resolve } from "$app/paths";
  import { cn } from "$lib/utils/cn";

  type Variant = "primary" | "secondary" | "outline" | "ghost" | "danger";
  type Size = "sm" | "md" | "lg";

  type Props = {
    variant?: Variant;
    size?: Size;
    class?: string;
    href?: string;
    type?: "button" | "submit" | "reset";
    disabled?: boolean;
    children?: import("svelte").Snippet;
    onclick?: (e: MouseEvent) => void;
  };

  let {
    variant = "primary",
    size = "md",
    class: className = "",
    href,
    type = "button",
    disabled = false,
    children,
    onclick,
  }: Props = $props();

  const resolvePath = resolve as unknown as (path: string) => string;

  const sizeClasses: Record<Size, string> = {
    sm: "h-7 px-2.5 text-xs",
    md: "h-9 px-4 text-sm",
    lg: "h-11 px-6 text-base",
  };

  const isBordered = $derived(variant === "primary" || variant === "secondary");
  const resolvedHref = $derived(
    href
      ? /^(https?:)?\/\//.test(href) ||
        href.startsWith("mailto:") ||
        href.startsWith("#")
        ? href
        : resolvePath(href)
      : undefined,
  );
  const externalRel = $derived(
    resolvedHref && /^(https?:)?\/\//.test(resolvedHref)
      ? "noreferrer"
      : undefined,
  );

  const outerVariantClasses: Record<"primary" | "secondary", string> = {
    primary: "bg-accent/30",
    secondary: "bg-background-inset",
  };

  const innerVariantClasses: Record<Variant, string> = {
    primary: "bg-accent text-background hover:bg-accent/85",
    secondary: "bg-background text-foreground hover:bg-background-muted",
    outline:
      "border border-border bg-background text-foreground hover:bg-background-muted",
    ghost:
      "text-foreground-muted hover:bg-background-muted hover:text-foreground",
    danger: "bg-red-500/10 text-red-500 hover:bg-red-500/20",
  };

  const outerClass = $derived(
    cn(
      "inset-shadow inline-flex rounded-sm p-[1.5px]",
      outerVariantClasses[variant as "primary" | "secondary"],
      disabled && "opacity-50 cursor-not-allowed",
      className,
    ),
  );

  const innerClass = $derived(
    cn(
      "inline-flex items-center justify-center gap-2 rounded-xs font-medium transition-colors duration-150 ease-out active:scale-[0.98] card",
      innerVariantClasses[variant],
      sizeClasses[size],
      disabled && "pointer-events-none",
    ),
  );

  const flatClass = $derived(
    cn(
      "inset-shadow inline-flex items-center justify-center gap-2 rounded-sm font-medium transition-colors duration-150 ease-out active:scale-[0.98]",
      innerVariantClasses[variant],
      sizeClasses[size],
      disabled && "opacity-50 cursor-not-allowed pointer-events-none",
      className,
    ),
  );
</script>

{#if isBordered}
  {#if href}
    <span class={outerClass}>
      <a
        href={resolvedHref}
        class={innerClass}
        aria-disabled={disabled}
        rel={externalRel}
      >
        {@render children?.()}
      </a>
    </span>
  {:else}
    <span class={outerClass}>
      <button {type} {disabled} {onclick} class={innerClass}>
        {@render children?.()}
      </button>
    </span>
  {/if}
{:else if href}
  <a
    href={resolvedHref}
    class={flatClass}
    aria-disabled={disabled}
    rel={externalRel}
  >
    {@render children?.()}
  </a>
{:else}
  <button {type} {disabled} {onclick} class={flatClass}>
    {@render children?.()}
  </button>
{/if}

Варианты оформления

Все варианты построены на CSS-переменных темы и поддерживают hover/active состояния с плавными переходами.

Primary (основная)

<Button variant="primary">Основная кнопка</Button>
<Button variant="primary">Основная кнопка</Button>

Secondary (вторичная)

<Button variant="secondary">Вторичная кнопка</Button>
<Button variant="secondary">Вторичная кнопка</Button>

Outline (контурная)

<Button variant="outline">Контурная кнопка</Button>
<Button variant="outline">Контурная кнопка</Button>

Ghost (призрачная)

<Button variant="ghost">Призрачная кнопка</Button>
<Button variant="ghost">Призрачная кнопка</Button>

Danger (опасное действие)

<Button variant="danger">Удалить</Button>
<Button variant="danger">Удалить</Button>

Размеры

Размер Высота Шрифт Паддинг
Small h-7 text-xs px-2.5
Medium h-9 text-sm px-4
Large h-11 text-base px-6

Кнопка-ссылка

Используйте проп href для превращения кнопки в ссылку:

<Button href="/docs" variant="primary">
	Перейти в документацию
</Button>
<Button href="/docs" variant="primary">
	Перейти в документацию
</Button>

С иконкой

<Button variant="primary">
	<SomeIcon size={16} />
	Добавить
</Button>
<Button variant="primary">
	<SomeIcon size={16} />
	Добавить
</Button>

Состояния

  • :hover — плавное изменение фона / цвета
  • :activescale(0.98) для тактильного отклика
  • :focus-visible — ring для навигации с клавиатуры
  • :disabledopacity-50 cursor-not-allowed

Пропсы

Пропс Тип По умолчанию Описание
variant 'primary' 'secondary' 'outline' 'ghost' 'danger' 'primary' Вариант оформления
size 'sm' 'md' 'lg' 'md' Размер кнопки
href string Превращает кнопку в ссылку
type 'button' 'submit' 'reset' 'button' Тип кнопки
disabled boolean false Отключённое состояние
class string Дополнительные классы