Skip to content
omaris

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:

src/routes/+layout.svelte Svelte
<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>
src/routes/+layout.svelte (inside the provider) Svelte
<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 from omaris/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.