Getting started
Start a project
From an empty folder to a first screen, and two ways to shape what comes after.
Three commands, then a decision about how to organise the code. The commands are fixed; the shape is a suggestion, and the last section says why.
The first ten minutes
bunx sv create my-app # SvelteKit, TypeScript, Tailwind — say yes to Tailwindcd my-appbunx omaris init # installs omaris, wires the stylesheet and the providerbunx omaris pwa # optional: manifest, icons, offline service workerbunx omaris native # optional: an Android and an iOS project init also writes an AGENTS.md next to your code: the conventions on this site, in the flat imperative form a coding agent reads before it writes anything. Keep it; it is how a model working in your project stops guessing prop names. See Feeding this to a model.
Then set the product's look once, at the root:
<script> import '../app.css'; import { OmarisProvider } from 'omaris'; let { children } = $props();</script><OmarisProvider theme={{ preset: 'ocean' }} defaultMode="system" persist> {@render children()}</OmarisProvider> Ten presets, or a hue of your own — Theming. Everything you build after this follows that one line.
The first screen
Most omaris apps are a shell with one scrolling column. Get it in place before the first page, because the scrolling is hard to retrofit.
Dashboard frame
The layout most omaris apps end up with. Only the content column scrolls — the sidebar stays put and the app bar finds its own scroller, which is what makes sticky work without a second scrollbar.
Overview
<script lang="ts"> import { AppShell, AppShellSidebar, AppShellMain, AppShellContent, NavigationDrawer, NavigationDrawerItem, TopAppBar } from 'omaris'; let selected = $state('Overview');</script><div class="h-96 w-full overflow-hidden rounded-shape-lg border border-border"> <AppShell class="h-full"> <AppShellSidebar breakpoint="sm"> <NavigationDrawer bordered label="Sections"> <NavigationDrawerItem label="Overview" selected={selected === 'Overview'} onclick={() => (selected = 'Overview')} /> <NavigationDrawerItem label="Orders" selected={selected === 'Orders'} onclick={() => (selected = 'Orders')} /> <NavigationDrawerItem label="Customers" selected={selected === 'Customers'} onclick={() => (selected = 'Customers')} /> </NavigationDrawer> </AppShellSidebar> <AppShellMain> <TopAppBar title={selected} sticky /> <AppShellContent width="lg"> <div class="flex flex-col gap-3"> <!-- Enough rows to have something to scroll. --> {#each { length: 12 }, i (i)} <div class="h-12 rounded-shape-md bg-surface-container"></div> {/each} </div> </AppShellContent> </AppShellMain> </AppShell></div> <AppShell> <AppShellSidebar> <NavigationDrawer responsive bordered> <NavigationDrawerItem href="/" label="Overview" selected /> <NavigationDrawerItem href="/orders" label="Orders" badge={12} /> </NavigationDrawer> </AppShellSidebar> <AppShellMain> <TopAppBar title="Overview" sticky /> <AppShellContent width="xl"> {@render children()} </AppShellContent> </AppShellMain></AppShell> Only the content column scrolls; the sidebar stays put and the app bar finds its own scroller, so sticky works and there is never a second scrollbar. responsive makes the drawer a sidebar on a laptop and a modal on a phone, so you write one drawer. width="xl" sets the reading measure — leave it off and a dashboard goes full-bleed on a wide monitor.
From here, Building a screen is the next page.
Two ways to shape the code
omaris does not care how your project is organised. These are two shapes that have worked, offered so you do not have to invent one on day one. Use your own if you have one.
Routes only
For a small app — a handful of pages, no shared state to speak of.
src/routes/ +layout.svelte provider, shell, navigation +page.svelte the overview orders/+page.svelte orders/[id]/+page.sveltesrc/lib/ components/ the pieces two pages share api.ts the calls to your backend The page file holds its own state. When a page grows past a screen of code, pull a section out into src/lib/components/ and keep going. This is fine for longer than people expect.
Features, with the logic beside the markup
For a dashboard that will be worked on for a year. This is the shape omaris init writes into AGENTS.md, because it gives an agent one place to put each thing.
src/lib/ features/ orders/ table/ index.svelte markup only — calls the logic once and renders logic.svelte.ts $state, $derived, handlers row.svelte a markup-only fragment add/ index.svelte logic.svelte.ts shared/ used by two or more features; same shape api/orders/index.ts the only thing UI calls for data server/orders/logic.ts the work, framework-freesrc/routes/ (app)/orders/+page.svelte imports the feature and lays it out; no state The rules that make it hold: a folder means it has logic; index.svelte has no $state of its own; a route's name is its feature's name; shared/ at the second user, not the first. The full list is in the AGENTS.md that init writes.
What both have in common
- One `OmarisProvider`, at the root. It hosts toasts and dialogs and starts the theme before first paint. A second one is a bug.
- Import from `omaris`; charts from
omaris/chart, maps fromomaris/map. Nothing else is a public path. - Read the component's page before using it. The top of each page says when it is the right choice and, more usefully, when it is not.
What to read next
- Principles — five rules that make screens look like one product.
- Choosing a component — the look-alikes, told apart.
- Building a screen — shell, rhythm, surfaces, states.
- Making it look designed — personality, one hero, space, two greys.
- Footguns — the mistakes that compile.