Field
Composes label, control, description and error into one accessible unit.
import { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle,} from "@/components/ui/field";import { Input } from "@/components/ui/input";import { Switch } from "@/components/ui/switch";
export default function Demo() { return ( <FieldSet className="w-full max-w-sm"> <FieldLegend>Mandate</FieldLegend> <FieldDescription> Rules the keeper follows after launch. </FieldDescription> <FieldGroup> <Field> <FieldLabel htmlFor="field-name">Index name</FieldLabel> <Input id="field-name" defaultValue="Magnificent Four" /> </Field> <Field data-invalid="true"> <FieldLabel htmlFor="field-ticker">Ticker</FieldLabel> <Input id="field-ticker" aria-invalid defaultValue="mag 4" /> <FieldError>Use 2-6 capital letters.</FieldError> </Field> <Field orientation="horizontal"> <FieldContent> <FieldTitle>Allow keeper</FieldTitle> <FieldDescription> Rebalance automatically when drift passes the limit. </FieldDescription> </FieldContent> <Switch defaultChecked /> </Field> </FieldGroup> </FieldSet> );}<script setup lang="ts">import { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle,} from "@/components/ui/field";import { Input } from "@/components/ui/input";import { Switch } from "@/components/ui/switch";</script>
<template> <FieldSet class="w-full max-w-sm"> <FieldLegend>Mandate</FieldLegend> <FieldDescription>Rules the keeper follows after launch.</FieldDescription> <FieldGroup> <Field> <FieldLabel for="field-name">Index name</FieldLabel> <Input id="field-name" default-value="Magnificent Four" /> </Field> <Field data-invalid="true"> <FieldLabel for="field-ticker">Ticker</FieldLabel> <Input id="field-ticker" aria-invalid="true" default-value="mag 4" /> <FieldError>Use 2-6 capital letters.</FieldError> </Field> <Field orientation="horizontal"> <FieldContent> <FieldTitle>Allow keeper</FieldTitle> <FieldDescription> Rebalance automatically when drift passes the limit. </FieldDescription> </FieldContent> <Switch default-value="true" /> </Field> </FieldGroup> </FieldSet></template><script lang="ts"> import { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle, } from "$lib/components/ui/field"; import { Input } from "$lib/components/ui/input"; import { Switch } from "$lib/components/ui/switch";
let ticker = $state("mag 4"); let keeper = $state(true); const invalid = $derived(!/^[A-Z]{2,6}$/.test(ticker));</script>
<FieldSet class="w-full max-w-sm"> <FieldLegend>Mandate</FieldLegend> <FieldDescription>Rules the keeper follows after launch.</FieldDescription> <FieldGroup> <Field> <FieldLabel for="field-name">Index name</FieldLabel> <Input id="field-name" value="Magnificent Four" /> </Field> <Field data-invalid={invalid}> <FieldLabel for="field-ticker">Ticker</FieldLabel> <Input id="field-ticker" aria-invalid={invalid} bind:value={ticker} /> {#if invalid} <FieldError>Use 2-6 capital letters.</FieldError> {/if} </Field> <Field orientation="horizontal"> <FieldContent> <FieldTitle>Allow keeper</FieldTitle> <FieldDescription> Rebalance automatically when drift passes the limit. </FieldDescription> </FieldContent> <Switch bind:checked={keeper} /> </Field> </FieldGroup></FieldSet>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/fieldpnpm dlx shadcn@latest add @edmi-ui/fieldyarn dlx shadcn@latest add @edmi-ui/fieldbunx shadcn@latest add @edmi-ui/fieldPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install class-variance-authorityTerminal window pnpm add class-variance-authorityTerminal window yarn add class-variance-authorityTerminal window bun add class-variance-authorityAdd the Edmi components this one builds on:
label,separator,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/fieldpnpm dlx shadcn-vue@latest add @edmi-ui/fieldyarn dlx shadcn-vue@latest add @edmi-ui/fieldbunx shadcn-vue@latest add @edmi-ui/fieldPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install class-variance-authorityTerminal window pnpm add class-variance-authorityTerminal window yarn add class-variance-authorityTerminal window bun add class-variance-authorityAdd the Edmi components this one builds on:
label,separator,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/field.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/field.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/field.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/field.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
label,separator,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle,} from "@/components/ui/field";import { Input } from "@/components/ui/input";import { Switch } from "@/components/ui/switch";
<FieldSet className="w-full max-w-sm"> <FieldLegend>Mandate</FieldLegend> <FieldDescription> Rules the keeper follows after launch. </FieldDescription> <FieldGroup> <Field> <FieldLabel htmlFor="field-name">Index name</FieldLabel> <Input id="field-name" defaultValue="Magnificent Four" /> </Field> <Field data-invalid="true"> <FieldLabel htmlFor="field-ticker">Ticker</FieldLabel> <Input id="field-ticker" aria-invalid defaultValue="mag 4" /> <FieldError>Use 2-6 capital letters.</FieldError> </Field> <Field orientation="horizontal"> <FieldContent> <FieldTitle>Allow keeper</FieldTitle> <FieldDescription> Rebalance automatically when drift passes the limit. </FieldDescription> </FieldContent> <Switch defaultChecked /> </Field> </FieldGroup></FieldSet><script setup lang="ts">import { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle,} from "@/components/ui/field";import { Input } from "@/components/ui/input";import { Switch } from "@/components/ui/switch";</script>
<template> <FieldSet class="w-full max-w-sm"> <FieldLegend>Mandate</FieldLegend> <FieldDescription>Rules the keeper follows after launch.</FieldDescription> <FieldGroup> <Field> <FieldLabel for="field-name">Index name</FieldLabel> <Input id="field-name" default-value="Magnificent Four" /> </Field> <Field data-invalid="true"> <FieldLabel for="field-ticker">Ticker</FieldLabel> <Input id="field-ticker" aria-invalid="true" default-value="mag 4" /> <FieldError>Use 2-6 capital letters.</FieldError> </Field> <Field orientation="horizontal"> <FieldContent> <FieldTitle>Allow keeper</FieldTitle> <FieldDescription> Rebalance automatically when drift passes the limit. </FieldDescription> </FieldContent> <Switch default-value="true" /> </Field> </FieldGroup> </FieldSet></template><script lang="ts"> import { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle, } from "$lib/components/ui/field"; import { Input } from "$lib/components/ui/input"; import { Switch } from "$lib/components/ui/switch";</script>
<FieldSet class="w-full max-w-sm"> <FieldLegend>Mandate</FieldLegend> <FieldDescription>Rules the keeper follows after launch.</FieldDescription> <FieldGroup> <Field> <FieldLabel for="field-name">Index name</FieldLabel> <Input id="field-name" value="Magnificent Four" /> </Field> <Field> <FieldLabel for="field-ticker">Ticker</FieldLabel> <Input id="field-ticker" /> {#if invalid} <FieldError>Use 2-6 capital letters.</FieldError> {/if} </Field> <Field orientation="horizontal"> <FieldContent> <FieldTitle>Allow keeper</FieldTitle> <FieldDescription> Rebalance automatically when drift passes the limit. </FieldDescription> </FieldContent> <Switch /> </Field> </FieldGroup></FieldSet>Component source
Section titled “Component source”"use client";
import { cva, type VariantProps } from "class-variance-authority";import { cn } from "cn";import { useMemo } from "react";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";import { Label } from "@/registry/edmi/ui/label";import { Separator } from "@/registry/edmi/ui/separator";
// ✦ choice-card depth (v4); a checked card keeps its ringconst ck = "has-[>[data-slot=field]]:has-data-[checked]:border-ring has-[>[data-slot=field]]:has-data-[checked]:shadow-[0_0_0_1px_var(--ring)]";const choiceCardElevation = { sunken: `has-[>[data-slot=field]]:border-sk-bd has-[>[data-slot=field]]:bg-sk-bg has-[>[data-slot=field]]:shadow-sunken ${ck}`, flat: "", raised: `has-[>[data-slot=field]]:border-transparent has-[>[data-slot=field]]:shadow-raised ${ck}`, floating: `has-[>[data-slot=field]]:border-transparent has-[>[data-slot=field]]:shadow-floating ${ck}`,};
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { return ( <fieldset data-slot="field-set" className={cn( "flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", className, )} {...props} /> );}
function FieldLegend({ className, variant = "legend", ...props}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { return ( <legend data-slot="field-legend" data-variant={variant} className={cn( "mb-1.5 font-medium text-foreground data-[variant=label]:text-sm data-[variant=legend]:text-base", className, )} {...props} /> );}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="field-group" className={cn( "group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4", className, )} {...props} /> );}
const fieldVariants = cva( "group/field flex w-full gap-2 data-[invalid=true]:text-destructive-text", { variants: { orientation: { vertical: "flex-col *:w-full [&>.sr-only]:w-auto", horizontal: "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", responsive: "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", }, }, defaultVariants: { orientation: "vertical", }, },);
function Field({ className, orientation = "vertical", ...props}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) { return ( // biome-ignore lint/a11y/useSemanticElements: stock markup; div keeps layout predictable <div role="group" data-slot="field" data-orientation={orientation} className={cn(fieldVariants({ orientation }), className)} {...props} /> );}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="field-content" className={cn( "group/field-content flex flex-1 flex-col gap-0.5 leading-snug", className, )} {...props} /> );}
function FieldLabel({ className, elevation, ...props}: React.ComponentProps<typeof Label> & { /** ✦ choice-card depth (only when the label wraps a Field): sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation;}) { const level = useElevation(elevation, "control"); return ( <Label data-slot="field-label" className={cn( "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-[>[data-slot=field]]:border-border has-[>[data-slot=field]]:bg-card has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted has-[>[data-slot=field]]:has-[:focus-visible]:border-ring has-[>[data-slot=field]]:has-[:focus-visible]:shadow-ring has-data-[checked]:border-ring has-data-[checked]:shadow-[0_0_0_1px_var(--ring)] *:data-[slot=field]:p-3.5", choiceCardElevation[level], "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col", className, )} {...props} /> );}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="field-label" className={cn( "flex w-fit items-center gap-2 text-sm font-medium text-foreground group-data-[disabled=true]/field:opacity-50", className, )} {...props} /> );}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { return ( <p data-slot="field-description" className={cn( "text-left text-[13px] leading-normal font-normal text-muted-foreground group-has-data-[orientation=horizontal]/field:text-balance [[data-variant=legend]+&]:-mt-1.5", "last:mt-0 nth-last-2:-mt-1", "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-foreground", className, )} {...props} /> );}
function FieldSeparator({ children, className, ...props}: React.ComponentProps<"div"> & { children?: React.ReactNode;}) { return ( <div data-slot="field-separator" data-content={!!children} className={cn( "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", className, )} {...props} > <Separator className="absolute inset-0 top-1/2" /> {children && ( <span className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground" data-slot="field-separator-content" > {children} </span> )} </div> );}
function FieldError({ className, children, errors, ...props}: React.ComponentProps<"div"> & { errors?: Array<{ message?: string } | undefined>;}) { const content = useMemo(() => { if (children) { return children; }
if (!errors?.length) { return null; }
const uniqueErrors = [ ...new Map(errors.map((error) => [error?.message, error])).values(), ];
if (uniqueErrors?.length === 1) { return uniqueErrors[0]?.message; }
return ( <ul className="ml-4 flex list-disc flex-col gap-1"> {uniqueErrors.map( (error) => error?.message && <li key={error.message}>{error.message}</li>, )} </ul> ); }, [children, errors]);
if (!content) { return null; }
return ( <div role="alert" data-slot="field-error" className={cn("text-[13px] font-normal text-destructive-text", className)} {...props} > {content} </div> );}
export { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle,};<script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { FieldVariants } from '.'import { cn } from '@/registry/edmi/lib/utils'import { fieldVariants } from '.'
const props = defineProps<{ class?: HTMLAttributes['class'] orientation?: FieldVariants['orientation']}>()</script>
<template> <div role="group" data-slot="field" :data-orientation="orientation ?? 'vertical'" :class="cn( fieldVariants({ orientation }), props.class, )" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="field-content" :class="cn( 'group/field-content flex flex-1 flex-col gap-0.5 leading-snug', props.class, )" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <p data-slot="field-description" :class="cn( 'text-left text-[13px] leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance [[data-variant=legend]+&]:-mt-1.5', 'last:mt-0 nth-last-2:-mt-1', '[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-foreground', props.class, )" > <slot /> </p></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { computed } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] errors?: Array<string | { message: string | undefined } | undefined>}>()
const content = computed(() => { if (!props.errors || props.errors.length === 0) return null
const uniqueErrors = [ ...new Map( props.errors .filter(Boolean) .map((error) => { const message = typeof error === 'string' ? error : error?.message return [message, error] }), ).values(), ]
if (uniqueErrors.length === 1 && uniqueErrors[0]) { return typeof uniqueErrors[0] === 'string' ? uniqueErrors[0] : uniqueErrors[0].message }
return uniqueErrors.map(error => typeof error === 'string' ? error : error?.message)})</script>
<template> <div v-if="$slots.default || content" role="alert" data-slot="field-error" :class="cn('text-[13px] font-normal text-destructive-text', props.class)" > <slot v-if="$slots.default" />
<template v-else-if="typeof content === 'string'"> {{ content }} </template>
<ul v-else-if="Array.isArray(content)" class="ml-4 flex list-disc flex-col gap-1"> <li v-for="(error, index) in content" :key="index"> {{ error }} </li> </ul> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="field-group" :class="cn( 'group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4', props.class, )" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'import { Label } from '@/registry/edmi/ui/label'
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] /** ✦ choice-card depth (only when the label wraps a Field): sunken -1, flat 0, raised +1, floating +2 */ elevation?: Elevation}>(), { elevation: undefined })
const level = useElevation(() => props.elevation, 'control')
// ✦ choice-card depth (v4); a checked card keeps its ringconst ck = 'has-[>[data-slot=field]]:has-data-[state=checked]:border-ring has-[>[data-slot=field]]:has-data-[state=checked]:shadow-[0_0_0_1px_var(--ring)]'const choiceCardElevation = { sunken: `has-[>[data-slot=field]]:border-sk-bd has-[>[data-slot=field]]:bg-sk-bg has-[>[data-slot=field]]:shadow-sunken ${ck}`, flat: '', raised: `has-[>[data-slot=field]]:border-transparent has-[>[data-slot=field]]:shadow-raised ${ck}`, floating: `has-[>[data-slot=field]]:border-transparent has-[>[data-slot=field]]:shadow-floating ${ck}`,}</script>
<template> <Label data-slot="field-label" :class="cn( 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-[>[data-slot=field]]:border-border has-[>[data-slot=field]]:bg-card has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted has-[>[data-slot=field]]:has-[:focus-visible]:border-ring has-[>[data-slot=field]]:has-[:focus-visible]:shadow-ring has-[>[data-slot=field]]:has-data-[state=checked]:border-ring has-[>[data-slot=field]]:has-data-[state=checked]:shadow-[0_0_0_1px_var(--ring)] *:data-[slot=field]:p-3.5', 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col', choiceCardElevation[level], props.class, )" > <slot /> </Label></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] variant?: 'legend' | 'label'}>(), { variant: 'legend',})</script>
<template> <legend data-slot="field-legend" :data-variant="variant" :class="cn( 'mb-1.5 font-medium text-foreground data-[variant=label]:text-sm data-[variant=legend]:text-base', props.class, )" > <slot /> </legend></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import { Separator } from '@/registry/edmi/ui/separator'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="field-separator" :data-content="!!$slots.default" :class="cn( 'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2', props.class, )" > <Separator class="absolute inset-0 top-1/2" /> <span v-if="$slots.default" class="relative mx-auto block w-fit bg-background px-2 text-muted-foreground" data-slot="field-separator-content" > <slot /> </span> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <fieldset data-slot="field-set" :class="cn( 'flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3', props.class, )" > <slot /> </fieldset></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="field-label" :class="cn( 'flex w-fit items-center gap-2 text-sm font-medium text-foreground group-data-[disabled=true]/field:opacity-50', props.class, )" > <slot /> </div></template>import type { VariantProps } from "class-variance-authority";import { cva } from "class-variance-authority";
export const fieldVariants = cva( "group/field flex w-full gap-2 data-[invalid=true]:text-destructive-text", { variants: { orientation: { vertical: "flex-col *:w-full [&>.sr-only]:w-auto", horizontal: "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", responsive: "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", }, }, defaultVariants: { orientation: "vertical", }, },);
export type FieldVariants = VariantProps<typeof fieldVariants>;
export { default as Field } from "./Field.vue";export { default as FieldContent } from "./FieldContent.vue";export { default as FieldDescription } from "./FieldDescription.vue";export { default as FieldError } from "./FieldError.vue";export { default as FieldGroup } from "./FieldGroup.vue";export { default as FieldLabel } from "./FieldLabel.vue";export { default as FieldLegend } from "./FieldLegend.vue";export { default as FieldSeparator } from "./FieldSeparator.vue";export { default as FieldSet } from "./FieldSet.vue";export { default as FieldTitle } from "./FieldTitle.vue";<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="field-content" class={cn("group/field-content flex flex-1 flex-col gap-0.5 leading-snug", className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();</script>
<p bind:this={ref} data-slot="field-description" class={cn( "text-left text-[13px] leading-normal font-normal text-muted-foreground group-has-data-[orientation=horizontal]/field:text-balance [[data-variant=legend]+&]:-mt-1.5", "last:mt-0 nth-last-2:-mt-1", "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-foreground", className )} {...restProps}> {@render children?.()}</p><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, errors, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet; errors?: { message?: string }[]; } = $props();
const hasContent = $derived.by(() => { // has slotted error if (children) return true;
// no errors if (!errors || errors.length === 0) return false;
// has an error but no message if (errors.length === 1 && !errors[0]?.message) { return false; }
return true; });
const isMultipleErrors = $derived(errors && errors.length > 1); const singleErrorMessage = $derived(errors && errors.length === 1 && errors[0]?.message);</script>
{#if hasContent} <div bind:this={ref} role="alert" data-slot="field-error" class={cn("text-[13px] font-normal text-destructive-text", className)} {...restProps} > {#if children} {@render children()} {:else if singleErrorMessage} {singleErrorMessage} {:else if isMultipleErrors} <ul class="ml-4 flex list-disc flex-col gap-1"> {#each errors ?? [] as error, index (index)} {#if error?.message} <li>{error.message}</li> {/if} {/each} </ul> {/if} </div>{/if}<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="field-group" class={cn( "group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { Label } from "$lib/registry/ui/label/index.js"; import { cn } from "$lib/utils.js"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js"; import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, children, elevation = "auto", ...restProps }: ComponentProps<typeof Label> & { /** ✦ choice-card depth (a FieldLabel wrapping a Field): sunken -1, flat 0, raised +1, floating +2. */ elevation?: Elevation; } = $props();
const level = useElevation(() => elevation, "control");
// ✦ choice-card depth (v4); a checked card keeps its ring const ck = "has-[>[data-slot=field]]:has-data-[state=checked]:border-ring has-[>[data-slot=field]]:has-data-[state=checked]:shadow-[0_0_0_1px_var(--ring)]"; const choiceCardElevation = { sunken: `has-[>[data-slot=field]]:border-sk-bd has-[>[data-slot=field]]:bg-sk-bg has-[>[data-slot=field]]:shadow-sunken ${ck}`, flat: "", raised: `has-[>[data-slot=field]]:border-transparent has-[>[data-slot=field]]:shadow-raised ${ck}`, floating: `has-[>[data-slot=field]]:border-transparent has-[>[data-slot=field]]:shadow-floating ${ck}`, };</script>
<Label bind:ref data-slot="field-label" class={cn( "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-[>[data-slot=field]]:border-border has-[>[data-slot=field]]:bg-card has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted has-[>[data-slot=field]]:has-[:focus-visible]:border-ring has-[>[data-slot=field]]:has-[:focus-visible]:shadow-ring has-[>[data-slot=field]]:has-data-[state=checked]:border-ring has-[>[data-slot=field]]:has-data-[state=checked]:shadow-[0_0_0_1px_var(--ring)] *:data-[slot=field]:p-3.5", "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col", choiceCardElevation[level.current], className )} {...restProps}> {@render children?.()}</Label><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, variant = "legend", children, ...restProps }: WithElementRef<HTMLAttributes<HTMLLegendElement>> & { variant?: "legend" | "label"; } = $props();</script>
<legend bind:this={ref} data-slot="field-legend" data-variant={variant} class={cn("mb-1.5 font-medium text-foreground data-[variant=label]:text-sm data-[variant=legend]:text-base", className)} {...restProps}> {@render children?.()}</legend><script lang="ts"> import { Separator } from "$lib/registry/ui/separator/index.js"; import { cn, type WithElementRef } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children?: Snippet; } = $props();
const hasContent = $derived(!!children);</script>
<div bind:this={ref} data-slot="field-separator" data-content={hasContent} class={cn("relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", className)} {...restProps}> <Separator class="absolute inset-0 top-1/2" /> {#if children} <span class="relative mx-auto block w-fit bg-background px-2 text-muted-foreground" data-slot="field-separator-content" > {@render children()} </span> {/if}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLFieldsetAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLFieldsetAttributes> = $props();</script>
<fieldset bind:this={ref} data-slot="field-set" class={cn("flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", className)} {...restProps}> {@render children?.()}</fieldset><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="field-label" class={cn("flex w-fit items-center gap-2 text-sm font-medium text-foreground group-data-[disabled=true]/field:opacity-50", className)} {...restProps}> {@render children?.()}</div><script lang="ts" module> import { tv, type VariantProps } from "tailwind-variants";
export const fieldVariants = tv({ base: "group/field flex w-full gap-2 data-[invalid=true]:text-destructive-text", variants: { orientation: { vertical: "flex-col *:w-full [&>.sr-only]:w-auto", horizontal: "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", responsive: "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", }, }, defaultVariants: { orientation: "vertical", }, });
export type FieldOrientation = VariantProps<typeof fieldVariants>["orientation"];</script>
<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, orientation = "vertical", children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { orientation?: FieldOrientation; } = $props();</script>
<div bind:this={ref} role="group" data-slot="field" data-orientation={orientation} class={cn(fieldVariants({ orientation }), className)} {...restProps}> {@render children?.()}</div>import Field from "./field.svelte";import Content from "./field-content.svelte";import Description from "./field-description.svelte";import FieldErr from "./field-error.svelte";import Group from "./field-group.svelte";import Label from "./field-label.svelte";import Legend from "./field-legend.svelte";import Separator from "./field-separator.svelte";import FieldSetPart from "./field-set.svelte";import Title from "./field-title.svelte";
export { Content, Content as FieldContent, Description, Description as FieldDescription, Field, FieldErr as Error, FieldErr as FieldError, FieldSetPart as Set, // FieldSetPart as FieldSet, Group, Group as FieldGroup, Label, Label as FieldLabel, Legend, Legend as FieldLegend, Separator, Separator as FieldSeparator, Title, Title as FieldTitle,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
A FieldLabel that wraps a Field is a choice card. elevation on the FieldLabel sets its depth (sunken -1, flat, raised +1, floating +2); a checked card keeps the ring border.
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
import { Field, FieldContent, FieldDescription, FieldLabel, FieldTitle,} from "@/components/ui/field";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const options = [ { value: "weekly", title: "Weekly", description: "Rebalance every Monday." }, { value: "drift", title: "On drift", description: "Only when a weight drifts.", },];
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <RadioGroup defaultValue="weekly" className="max-w-sm"> {options.map((o) => ( <FieldLabel key={o.value} elevation={value} htmlFor={`${value}-${o.value}`} > <Field orientation="horizontal"> <FieldContent> <FieldTitle>{o.title}</FieldTitle> <FieldDescription>{o.description}</FieldDescription> </FieldContent> <RadioGroupItem id={`${value}-${o.value}`} value={o.value} /> </Field> </FieldLabel> ))} </RadioGroup> </div> ))} </div> );}<script setup lang="ts">import { Field, FieldContent, FieldDescription, FieldLabel, FieldTitle } from "@/components/ui/field";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const options = [ { value: "weekly", title: "Weekly", description: "Rebalance every Monday." }, { value: "drift", title: "On drift", description: "Only when a weight drifts." },];
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];</script>
<template> <div class="flex flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <RadioGroup default-value="weekly" class="max-w-sm"> <FieldLabel v-for="o in options" :key="o.value" :elevation="level.value" :for="`${level.value}-${o.value}`"> <Field orientation="horizontal"> <FieldContent> <FieldTitle>{{ o.title }}</FieldTitle> <FieldDescription>{{ o.description }}</FieldDescription> </FieldContent> <RadioGroupItem :id="`${level.value}-${o.value}`" :value="o.value" /> </Field> </FieldLabel> </RadioGroup> </div> </div></template><script lang="ts"> import { Field, FieldContent, FieldDescription, FieldLabel, FieldTitle } from "$lib/components/ui/field"; import { RadioGroup, RadioGroupItem } from "$lib/components/ui/radio-group";
const options = [ { value: "weekly", title: "Weekly", description: "Rebalance every Monday." }, { value: "drift", title: "On drift", description: "Only when a weight drifts." }, ];
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <RadioGroup value="weekly" class="max-w-sm"> {#each options as o (o.value)} <FieldLabel elevation={level.value} for={`${level.value}-${o.value}`}> <Field orientation="horizontal"> <FieldContent> <FieldTitle>{o.title}</FieldTitle> <FieldDescription>{o.description}</FieldDescription> </FieldContent> <RadioGroupItem id={`${level.value}-${o.value}`} value={o.value} /> </Field> </FieldLabel> {/each} </RadioGroup> </div> {/each}</div>| Prop | Type | Default |
|---|---|---|
| Field orientation | vertical | horizontal | responsive |
vertical |
| FieldLegend variant | legend | label |
legend |
| FieldError errors | Array<{ message?: string }> |
none |
| FieldLabel elevation ✦ | "auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
✦ Edmi additions
Section titled “✦ Edmi additions”- Choice-card labels (a FieldLabel wrapping a Field) highlight with the ring border when checked;
elevationadds depth.