Getting started
Install
Habitus is published to the internal Gitea registry, so the @livereader scope needs pointing at it. Add to your project's .npmrc — the mapping only, never a token:
# @livereader/habitus-vue comes from LiveReader's Gitea registry, not npmjs.com.
# Reads need a token: set yours once per machine (it goes to ~/.npmrc):
#
# npm config set -- '//git.livereader.com/api/packages/LiveReader/npm/:_authToken' '<gitea token with read:package>'
#
# `LiveReader` is capitalised on purpose, here and in the token key.
@livereader:registry=https://git.livereader.com/api/packages/LiveReader/npm/Then install:
npm install @livereader/habitus-vueReads need a token
The registry refuses anonymous package requests with a 401. Create a Gitea token with the read:package scope and register it once per machine with the npm config set line above. npm merges ~/.npmrc with the project's file, so the token never enters the repository.
In CI, append the same line from the LR_GIT_TOKEN repository secret (scope read:package) before installing:
- name: Authenticate the Habitus registry
run: echo "//git.livereader.com/api/packages/LiveReader/npm/:_authToken=${{ secrets.LR_GIT_TOKEN }}" >> .npmrcSpell the org LiveReader
WARNING
Use LiveReader, capitalised, in both the registry URL and the token key.
Gitea builds tarball URLs from the org's stored name, whatever case the registry was addressed in, and npm matches a token to a URL case-sensitively. With a lowercase livereader registry, the metadata request carries the token and the tarball request does not — so installs fail with a 401 on the tarball. That is what happened between 17 and 28 September 2026.
Set up
// main.ts
import { createApp } from "vue";
import { createHabitus } from "@livereader/habitus-vue";
import "@livereader/habitus-vue/styles";
import App from "./App.vue";
createApp(App)
.use(createHabitus({ registerComponents: false }))
.mount("#app");Then mark your root element so Habitus base styles apply:
<div
id="app"
data-habitus
></div>Habitus scopes its base styles to [data-habitus] rather than applying them to html/body. A library should not restyle a host document that did not ask for it, and during a migration you will want parts of an app opted out.
Import order matters
WARNING
Import @livereader/habitus-vue/styles before your application CSS.
When you deliberately override a Habitus utility, the stylesheet that comes later wins. Import Habitus first and your overrides behave as expected.
Letting Habitus own the whole page
/styles applies the page's ink, ground and type to [data-habitus] only, so modals, popovers, menus and toasts — which reka portals to the end of <body>, outside your root — do not inherit them. If your whole app is Habitus, import /page after /styles to set them on body instead:
import "@livereader/habitus-vue/styles";
import "@livereader/habitus-vue/page";Leave it out while parts of the app still belong to another framework: it restyles the whole document.
This is the setup for an app that does not run Tailwind itself. An app with its own Tailwind build does not import /styles at all — see If your app runs its own Tailwind.
Using components
Import each component where you use it:
<script setup lang="ts">
import { HabButton } from "@livereader/habitus-vue";
</script>
<template>
<HabButton>Save</HabButton>
</template>That is why the setup above passes registerComponents: false. By default createHabitus() registers every component globally, so templates could use them without importing — but out of the box vue-tsc and your editor know nothing about a globally registered component. Its props, slots and events go unchecked. An explicit import is type-checked like any other component.
It does not yet let your bundler drop the components you do not use. createHabitus() imports every component in order to be able to register them, and that import stays in the bundle even with registerComponents: false, when the plugin does nothing at runtime. One app measured 559 kB of JavaScript with the call and 234 kB without it. Until that changes, an app that registers nothing can leave createHabitus() out entirely.
The narrowest import goes to the component's own entry:
import { HabButton } from "@livereader/habitus-vue/components/Button";If you register components globally
Global registration remains the default. An app that keeps it should opt in to Habitus's GlobalComponents types, from any declaration file in its program (env.d.ts, say):
/// <reference types="@livereader/habitus-vue/global-components" />That makes every Hab* component in a template type-checked like an imported one. It is opt-in rather than automatic because the declaration is program-wide: with it, every Hab* name type-checks in every template whether or not the component was registered. An app on the setup above would then pass type-checking with a forgotten import and fail at runtime — so only add it if you register globally.
If your app runs its own Tailwind
Do not import @livereader/habitus-vue/styles. That is a finished Tailwind build carrying only the utilities Habitus's own templates use; your app's Tailwind build generates them instead, alongside your own. Your CSS entry pulls in Habitus's tokens (/theme), its global rules (/base) and the page's ink and type (/page), and scans Habitus's compiled components for their classes:
/* src/styles/tailwind.css */
@layer properties, theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
/* `source("../")` is `src/`, relative to this file: the app's own markup. */
@import "tailwindcss/utilities.css" layer(utilities) source("../");
/* Habitus's tokens, its global rules (focus indicators, links, keyframes), and
* the page's ink, ground and type on `body`. */
@import "@livereader/habitus-vue/theme";
@import "@livereader/habitus-vue/base";
@import "@livereader/habitus-vue/page";
/* Habitus's components carry their utilities in their compiled templates.
* `dist` only: the package's sources and docs name classes that ship nowhere. */
@source "../../node_modules/@livereader/habitus-vue/dist";The paths are relative to the file. Move it and adjust source("../") and the @source path so they still reach your src/ and node_modules/@livereader/habitus-vue/dist.
WARNING
This file must be the first stylesheet your app loads — import it before any other CSS in main.ts:
// main.ts
import "./styles/tailwind.css"; // first, before any other stylesheet
import { createApp } from "vue";Cascade layer order is fixed by the order in which layers first appear. The @layer properties, theme, base, components, utilities; statement only sets that order if no stylesheet has declared a layer before it; an earlier stylesheet that declares one decides the order instead, and utilities can end up beneath the reset. Unlayered CSS outranks every layer regardless, so keep your own resets layered — see When components look unstyled.
Two things to know about this setup:
/baseis not optional. It holds the focus indicators for inputs and toggles; without it they never fire./pageis what gives the page its ink and type. Without/styles, nothing else sets them, and text falls back to the browser's default. It sets them onbodyrather than on[data-habitus], so content reka portals to<body>— modals, popovers, menus, toasts — inherits them too. Keepdata-habituson your root anyway:/basescopes its link and table-row rules to it.
See Theming for what the tokens are and how to override them.
Dark mode
Habitus keys dark mode off a dark class, not prefers-color-scheme, so apps can offer an explicit toggle:
document.documentElement.classList.toggle("dark", isDark);