Getting started — Svelte

Add Justin to a Svelte 5 / SvelteKit app. Six steps: authenticate to the package feed, install, import the styles, wire the icon plugin, render a component, verify.

1. Authenticate to the feed

@davidhorn/justin is published to the private shared Azure DevOps Artifacts npm feed — a plain bun add fails until your project points at it. Add this to the .npmrc in your project root:

@davidhorn:registry=https://pkgs.dev.azure.com/davidhorn/Packages/_packaging/shared/npm/registry/
always-auth=true

Feed access and credentials are documented on the internal Justin Package Feeds page.

2. Install

bun add @davidhorn/justin

The only peer dependency is svelte ^5 — the library uses runes and does not support Svelte 4.

3. Import the styles

Once, at the root of your app — usually in src/routes/+layout.svelte:

<script>
  import '@davidhorn/justin/styles.css';
</script>

This registers all design tokens as CSS custom properties (--color-primary, --spacing-4, --font-size-headline-md, …) and applies the ITCSS layers (reset, generic, elements, components, utilities). It also loads the bundled Inter variable font — the @font-face sources resolve to woff2 files shipped inside the package, so there’s nothing extra to install or link.

4. Wire the icon plugin

DhIcon ships the full Lucide glyph set and serves each SVG as a per-file static asset on your own origin. Register the Vite plugin so the icons end up under /icons/:

// vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import { justinIcons } from '@davidhorn/justin/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [sveltekit(), justinIcons()]
});

5. Use a component

<script>
  import { DhButton, DhTextInput } from '@davidhorn/justin';
  let name = $state('');
</script>

<DhTextInput bind:value={name} label="Your name" />
<DhButton variant="primary" text="Submit" onclick={() => alert(name)} />

6. Verify

Run your dev server and load the page:

  • The button renders with Justin’s primary color and rounded corners. An unstyled browser button means the stylesheet import from step 3 is missing.
  • Text renders in Inter — the network tab shows a request for inter-latin-wght-normal.woff2. A system font instead means the stylesheet import from step 3 is missing or overridden.
  • Add <DhIcon name="search" /> to the page. A blank spot with a /icons/search.svg 404 in the network tab means the Vite plugin from step 4 isn’t registered.

That’s the full setup. Everything below is optional.

Going further

Granular imports

The package exposes per-component and per-layer subpaths so you can cherry-pick when you don’t want the whole library. When importing a component’s CSS directly, also import @davidhorn/justin/styles/settings.css (or a per-group file under styles/settings/) so the design-token custom properties used by that CSS are defined.

Icon-only recipe:

<script>
  // Just the icon component
  import DhIcon from '@davidhorn/justin/components/Icon.svelte';
  // Tokens (so the icon picks up your app's theme)
  import '@davidhorn/justin/styles/settings.css';
  // The icon's component CSS only
  import '@davidhorn/justin/styles/components/icon.css';
</script>

<DhIcon name="search" />

Tokens-only (build your own components on top):

<script>
  import '@davidhorn/justin/styles/settings.css';
  // or one group at a time:
  // import '@davidhorn/justin/styles/settings/colors.css';
</script>

Available CSS subpaths

SubpathShips
@davidhorn/justin/styles.cssEverything (tokens + reset + elements + components + utilities)
@davidhorn/justin/styles/settings.cssTokens only
@davidhorn/justin/styles/settings/<group>.cssOne token group (colors, radius, spacing, typography, motion)
@davidhorn/justin/styles/generic.cssReset + box-sizing
@davidhorn/justin/styles/elements.cssBase HTML element styles
@davidhorn/justin/styles/components.cssAll component CSS, no tokens
@davidhorn/justin/styles/components/<name>.cssOne component’s CSS
@davidhorn/justin/styles/utilities.cssType-scale classes (dh-display-lgdh-label-sm) — import last
@davidhorn/justin/styles/fonts.cssInter @font-face only — the per-layer bundles above carry none

Light and dark mode

By default, components follow the user’s prefers-color-scheme. To force a theme, set data-theme="light" or data-theme="dark" on <html>. The docs site’s theme picker uses the same key (localStorage.dh-demo-theme) — share that mechanism in your own app if you want a manual override.

Overriding tokens

Justin ships its CSS in named cascade layers, declared in this order:

@layer justin-base, justin-overrides, generic, elements, components, utilities;

Design tokens live in justin-base. To change one, put your declaration in the justin-overrides layer — declared after the base, it wins regardless of selector specificity, with no !important and no need to out-specify the [data-theme] rules:

/* your app's CSS, loaded after '@davidhorn/justin/styles.css' */
@layer justin-overrides {
  :root {
    --color-primary: #0066cc;
    --radius-md: 0.25rem;
  }
}

Import order matters: bring in @davidhorn/justin/styles.css first so the layer order is established, then your overrides.

What’s next