Skip to content
Sveltier
Mode
Core

Building components

The conventions every Sveltier component follows. Your own components can follow them too, and then behave like the built-in ones.

Shared props and defaults

A component with a variant, size, density, colour or rounding takes the shared props with their exact names and values, resolves them with useDefaults, and passes every resolved value to sharedClasses.

IDs

Labels, hints and panels are linked by ID. Svelte's $props.id() gives each instance an ID that is the same on the server and in the browser, so the links survive hydration.

Field.svelte
<script lang="ts">
  let { id: idProp, label, hint }: { id?: string; label: string; hint?: string } = $props();

  // Unique per instance, and the same on the server and in the browser.
  const uid = $props.id();
  const id = $derived(idProp ?? uid);
</script>

<label for={id}>{label}</label>
<input {id} aria-describedby={hint ? `${id}-hint` : undefined} />
{#if hint}<p id="{id}-hint">{hint}</p>{/if}
  • The main element takes the id prop when one is passed.
  • Parts derive from it: {id}-label, {id}-hint, {id}-error, {id}-panel.
  • Reference only parts that render, so screen readers never point at nothing.
  • Rendering several Svelte roots on one page? Give each its own idPrefix in render() and hydrate().

The root element

Every component renders one root element and passes through the attributes it does not use, a class and a bindable ref. ElementProps types this: the component's own props, then every attribute of the element it does not redefine, so a shared size or color never clashes with the HTML attribute of the same name.

Delta.svelte
<script lang="ts" module>
  import type { HTMLAttributes } from 'svelte/elements';
  import type { ElementProps } from 'sveltier';

  // Own props first; every other <span> attribute passes through.
  export type DeltaProps = ElementProps<
    HTMLSpanElement,
    HTMLAttributes<HTMLSpanElement>,
    { value: number }
  >;
</script>

<script lang="ts">
  import { useLocale } from 'sveltier';

  let { value, class: className, ref = $bindable(null), ...rest }: DeltaProps = $props();
  const locale = useLocale();
</script>

<!-- The spread comes first, so the attributes the component controls come after and win. -->
<span
  bind:this={ref}
  {...rest}
  class={['delta', value < 0 ? 'delta--down' : 'delta--up', className]}
>
  {locale.formatPercent(value, { minimumFractionDigits: 2, signDisplay: 'exceptZero' })}
</span>
  • Destructure the own props, class and ref; the rest is everything else.
  • Spread the rest first. Attributes the component controls (its classes, role and ARIA state) come after the spread, so a stray attribute cannot break them; callbacks such as onclick pass through.
  • Merge class last in an array: strings, arrays and objects all work, and the user's classes follow the component's.
  • ref is $bindable(null) and set with bind:this.