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.
[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.
<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
<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
<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} 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
<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
<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> <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> <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> <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> <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> <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> 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
<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
<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} 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
<script lang="ts">
import { Textarea } from "$lib/components/ui/veil/textarea";
</script>
<Textarea placeholder="Enter details..." />
<Textarea aria-invalid="true" placeholder="Invalid state" /> Source
<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> 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-containeris 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.