Veil Theme Setup and Component Overrides

Apply Veil theme tokens and update button, card, input, and textarea primitives for consistent UI styling.

Overview

Veil is a warm, earthy variant for minimal and elegant product interfaces. To make Veil blocks render correctly, keep theme variables in place and update these four UI primitives: Button, Card, Input, and Textarea.

  • Use this page as the source for Veil-specific primitive overrides.
  • Keep import paths under $lib/components/ui/veil/*.
  • Apply theme tokens globally using .theme-container.

Theme Quickstart

Add the Veil tokens to your global stylesheet (for example app.css or globals.css) so all Veil primitives resolve color, border, focus ring, and typography consistently.

globals.css
[data-theme="veil"] .theme-container {
	--radius: 0.625rem;

	--background: oklch(0.9779 0.0042 56.38);
	--foreground: oklch(0.3421 0.0379 61.15);

	--card: var(--color-white);
	--card-foreground: oklch(0.3421 0.0379 61.15);

	--popover: var(--color-white);
	--popover-foreground: oklch(0.3421 0.0379 61.15);

	--primary: oklch(0.5967 0.0558 61.59);
	--primary-foreground: oklch(0.1448 0 0);

	--secondary: --alpha(var(--primary)/15%);
	--secondary-foreground: oklch(0.3421 0.0379 61.15);

	--muted: --alpha(var(--foreground)/5%);
	--muted-foreground: oklch(0.4563 0.0061 48.59);

	--accent: oklch(0.9068 0.0112 89.73);
	--accent-foreground: oklch(0.3467 0.0231 86.12);

	--destructive: var(--color-red-600);
	--destructive-foreground: var(--color-white);

	--border: --alpha(var(--foreground)/7.5%);
	--input: --alpha(var(--foreground)/20%);
	--ring: var(--primary);

	--font-family: "Geist", sans-serif;

	@variant dark {
		--font-family: "Geist", sans-serif;
		--background: oklch(0.1448 0 0);
		--foreground: oklch(0.9027 0.0137 60.56);

		--card: oklch(0.1924 0.0016 17.3);
		--card-foreground: oklch(0.9027 0.0137 60.56);

		--popover: var(--color-white);
		--popover-foreground: oklch(0.9027 0.0137 60.56);

		--primary-foreground: var(--color-white);
		--secondary: --alpha(var(--primary)/10%);
		--secondary-foreground: oklch(0.9027 0.0137 60.56);

		--muted: var(--background);
		--muted-foreground: oklch(0.7262 0.0037 67.77);

		--accent: var(--color-zinc-700);
		--accent-foreground: var(--color-white);

		--input: --alpha(var(--foreground)/15%);
	}

	@apply *:text-foreground selection:bg-muted selection:text-primary;
}

Global Container

Ensure your root body includes theme-container so the selected theme scope is applied across the application.

src/app.html
<body class="theme-container">
	<!-- Your Application -->
</body>

Required Components

After theme tokens are configured, update these four Veil primitives in your app.

Component Import Path Update Scope
Button $lib/components/ui/veil/button Variant and size classes, anchor/button rendering
Card $lib/components/ui/veil/card Root variants and all card composition primitives
Input $lib/components/ui/veil/input Field styling, file input branch, focus/error states
Textarea $lib/components/ui/veil/textarea Multiline field sizing, focus/error/disabled states

Button Component

Veil button uses rounded-full geometry, compact sizing, and contrast-aware variants.

Usage

button-usage.svelte
<script lang="ts">
	import { Button } from "$lib/components/ui/veil/button";
</script>

<Button variant="default">Default Button</Button>
<Button variant="outline">Outline Button</Button>
<Button size="lg">Large Button</Button>

Source

button.svelte
<script lang="ts" module>
	import type { WithElementRef } from "bits-ui";
	import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
	import { type VariantProps, tv } from "tailwind-variants";

	export const buttonVariants = tv({
		base: "inline-flex cursor-pointer items-center justify-center gap-2 rounded-full text-sm font-medium whitespace-nowrap duration-200 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none active:scale-99 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
		variants: {
			variant: {
				default: "bg-foreground text-background hover:brightness-95",
				neutral: "bg-foreground text-background hover:brightness-95",
				destructive:
					"text-destructive-foreground bg-destructive shadow-md hover:bg-destructive/90",
				outline:
					"border border-transparent bg-card text-foreground shadow-sm ring-1 shadow-black/6.5 ring-foreground/15 duration-200 hover:bg-muted/50",
				secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
				ghost: "text-foreground/75 hover:bg-foreground/5 hover:text-foreground",
				link: "text-primary underline-offset-4 hover:underline",
			},
			size: {
				default: "h-8 px-3 py-2",
				sm: "h-7 px-2.5 text-sm",
				lg: "h-11 px-6 text-base font-medium",
				icon: "size-9",
			},
		},
		defaultVariants: {
			variant: "default",
			size: "default",
		},
	});

	export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
	export type ButtonSize = VariantProps<typeof buttonVariants>["size"];

	export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
		WithElementRef<HTMLAnchorAttributes> & {
			variant?: ButtonVariant;
			size?: ButtonSize;
		};
</script>

<script lang="ts">
	import { cn } from "$lib/utils.js";

	let {
		class: className,
		variant = "default",
		size = "default",
		ref = $bindable(null),
		href = undefined,
		type = "button",
		children,
		...restProps
	}: ButtonProps = $props();
</script>

{#if href}
	<a
		bind:this={ref}
		class={cn(buttonVariants({ variant, size }), className)}
		{href}
		{...restProps}
	>
		{@render children?.()}
	</a>
{:else}
	<button
		bind:this={ref}
		class={cn(buttonVariants({ variant, size }), className)}
		{type}
		{...restProps}
	>
		{@render children?.()}
	</button>
{/if}
index.ts
import Button from "./button.svelte";

export { Button };

Card Component

Veil card supports four variants: default, soft, mixed, and outline, with composition primitives for header/content/footer layouts.

Usage

card-usage.svelte
<script lang="ts">
	import { Card, CardContent, CardHeader, CardTitle } from "$lib/components/ui/veil/card";
</script>

<Card variant="default">
	<CardHeader>
		<CardTitle>Default Card</CardTitle>
	</CardHeader>
	<CardContent>...</CardContent>
</Card>

<Card variant="outline">
	<CardHeader>
		<CardTitle>Outline Card</CardTitle>
	</CardHeader>
	<CardContent>...</CardContent>
</Card>

Source

card.svelte
<script module lang="ts">
	import type { WithElementRef } from "bits-ui";
	import { type VariantProps, tv } from "tailwind-variants";

	export const cardVariants = tv({
		base: "rounded-2xl text-card-foreground",
		variants: {
			variant: {
				default:
					"bg-card shadow-lg ring-1 shadow-foreground/5 ring-foreground/6.5 dark:shadow-black/10",
				soft: "bg-muted",
				mixed: "border bg-muted",
				outline: "bg-card ring-1 ring-border",
			},
		},
		defaultVariants: {
			variant: "default",
		},
	});

	export type CardVariant = VariantProps<typeof cardVariants>["variant"];

	export type CardProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
		variant?: CardVariant;
	};
</script>

<script lang="ts">
	import type { HTMLAttributes } from "svelte/elements";
	import { cn } from "$lib/utils.js";

	let {
		ref = $bindable(null),
		class: className,
		children,
		variant = "default",
		...restProps
	}: CardProps = $props();
</script>

<div bind:this={ref} class={cn(cardVariants({ variant }), className)} {...restProps}>
	{@render children?.()}
</div>
card-header.svelte
<script lang="ts">
	import type { WithElementRef } from "bits-ui";
	import type { HTMLAttributes } from "svelte/elements";
	import { cn } from "$lib/utils.js";

	let {
		ref = $bindable(null),
		class: className,
		children,
		...restProps
	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<!--  pb-0 -->
<div bind:this={ref} class={cn("flex flex-col space-y-1.5 p-6", className)} {...restProps}>
	{@render children?.()}
</div>
card-title.svelte
<script lang="ts">
	import type { WithElementRef } from "bits-ui";
	import type { HTMLAttributes } from "svelte/elements";
	import { cn } from "$lib/utils.js";

	let {
		ref = $bindable(null),
		class: className,
		level = 3,
		children,
		...restProps
	}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
		level?: 1 | 2 | 3 | 4 | 5 | 6;
	} = $props();
</script>

<div
	role="heading"
	aria-level={level}
	bind:this={ref}
	class={cn("leading-none font-semibold tracking-tight", className)}
	{...restProps}
>
	{@render children?.()}
</div>
card-description.svelte
<script lang="ts">
	import type { WithElementRef } from "bits-ui";
	import type { HTMLAttributes } from "svelte/elements";
	import { cn } from "$lib/utils.js";

	let {
		ref = $bindable(null),
		class: className,
		children,
		...restProps
	}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>

<p bind:this={ref} class={cn("text-sm text-muted-foreground", className)} {...restProps}>
	{@render children?.()}
</p>
card-content.svelte
<script lang="ts">
	import type { WithElementRef } from "bits-ui";
	import type { HTMLAttributes } from "svelte/elements";
	import { cn } from "$lib/utils.js";

	let {
		ref = $bindable(null),
		class: className,
		children,
		...restProps
	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<div bind:this={ref} class={cn("p-6 pt-0", className)} {...restProps}>
	{@render children?.()}
</div>
card-footer.svelte
<script lang="ts">
	import type { WithElementRef } from "bits-ui";
	import type { HTMLAttributes } from "svelte/elements";
	import { cn } from "$lib/utils.js";

	let {
		ref = $bindable(null),
		class: className,
		children,
		...restProps
	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>

<div bind:this={ref} class={cn("flex items-center p-6 pt-0", className)} {...restProps}>
	{@render children?.()}
</div>
index.ts
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";

export {
	Root,
	Content,
	Description,
	Footer,
	Header,
	Title,
	//
	Root as Card,
	Content as CardContent,
	Description as CardDescription,
	Footer as CardFooter,
	Header as CardHeader,
	Title as CardTitle,
};

Input Component

Veil input covers standard and file fields while preserving unified focus and invalid states.

Usage

input-usage.svelte
<script lang="ts">
	import { Input } from "$lib/components/ui/veil/input";
</script>

<Input type="email" placeholder="you@example.com" />
<Input aria-invalid="true" placeholder="Invalid state" />
<Input type="file" />

Source

input.svelte
<script lang="ts">
	import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
	import type { WithElementRef } from "bits-ui";
	import { cn } from "$lib/utils.js";

	type InputType = Exclude<HTMLInputTypeAttribute, "file">;

	type Props = WithElementRef<
		Omit<HTMLInputAttributes, "type"> &
			({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
	>;

	let {
		ref = $bindable(null),
		value = $bindable(),
		type,
		files = $bindable(),
		class: className,
		...restProps
	}: Props = $props();
</script>

{#if type === "file"}
	<input
		bind:this={ref}
		class={cn(
			"flex h-8 w-full min-w-0 rounded-md border border-input bg-card px-3 py-1 text-sm outline-none not-dark:bg-card selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
			"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/15",
			"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
			className
		)}
		type="file"
		bind:files
		bind:value
		{...restProps}
	/>
{:else}
	<input
		bind:this={ref}
		class={cn(
			"flex h-8 w-full min-w-0 rounded-md border border-input bg-card px-3 py-1 text-sm outline-none not-dark:bg-card selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
			"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/15",
			"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
			className
		)}
		{type}
		bind:value
		{...restProps}
	/>
{/if}
index.ts
import Root from "./input.svelte";

export {
	Root,
	//
	Root as Input,
};

Textarea Component

Veil textarea shares input tokens, adds multiline sizing, and keeps focus/validation states aligned with other form controls.

Usage

textarea-usage.svelte
<script lang="ts">
	import { Textarea } from "$lib/components/ui/veil/textarea";
</script>

<Textarea placeholder="Enter details..." />
<Textarea aria-invalid="true" placeholder="Invalid state" />

Source

textarea.svelte
<script lang="ts">
	import type { WithElementRef, WithoutChildren } from "bits-ui";
	import type { HTMLTextareaAttributes } from "svelte/elements";
	import { cn } from "$lib/utils.js";

	let {
		ref = $bindable(null),
		value = $bindable(),
		class: className,
		...restProps
	}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
</script>

<textarea
	bind:this={ref}
	bind:value
	class={cn(
		"flex field-sizing-content min-h-16 w-full rounded-md border border-input px-3 py-2 text-base transition-colors outline-none not-dark:bg-card placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/15 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:aria-invalid:ring-destructive/40",
		className
	)}
	{...restProps}
></textarea>
index.ts
import Root from "./textarea.svelte";

export {
	Root,
	//
	Root as Textarea,
};

Verification

Use this checklist after setup:

  • data-theme="veil" is applied where theme switching is controlled.
  • theme-container is present on the root body wrapper.
  • Imports resolve from $lib/components/ui/veil/* without type errors.
  • Button/Card variants and Input/Textarea focus states render correctly in light and dark mode.