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

Интерфейс

Кнопка

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


Кнопка

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

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

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

Пропсы

Вариант
Размер
Текст
Отключена
<script lang="ts">
	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 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 = variant === 'primary' || variant === 'secondary';

	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={resolve(href)} class={innerClass} aria-disabled={disabled}>
				{@render children?.()}
			</a>
		</span>
	{:else}
		<span class={outerClass}>
			<button {type} {disabled} {onclick} class={innerClass}>
				{@render children?.()}
			</button>
		</span>
	{/if}
{:else}
	{#if href}
		<a href={resolve(href)} class={flatClass} aria-disabled={disabled}>
			{@render children?.()}
		</a>
	{:else}
		<button {type} {disabled} {onclick} class={flatClass}>
			{@render children?.()}
		</button>
	{/if}
{/if}
<script lang="ts">
	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 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 = variant === 'primary' || variant === 'secondary';

	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={resolve(href)} class={innerClass} aria-disabled={disabled}>
				{@render children?.()}
			</a>
		</span>
	{:else}
		<span class={outerClass}>
			<button {type} {disabled} {onclick} class={innerClass}>
				{@render children?.()}
			</button>
		</span>
	{/if}
{:else}
	{#if href}
		<a href={resolve(href)} class={flatClass} aria-disabled={disabled}>
			{@render children?.()}
		</a>
	{:else}
		<button {type} {disabled} {onclick} class={flatClass}>
			{@render children?.()}
		</button>
	{/if}
{/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 Дополнительные классы