| name | nebari-ui |
| description | Add and use components from the Nebari design system (the @nebari shadcn registry) in this app. Use when asked to "add a nebari component", "use the nebari button/badge/alert/spinner", "install the nebari theme", "animate, add a transition, make it feel polished, or add an entrance animation with nebari", or "build <something> with nebari components". Covers registry setup (the @nebari namespace in components.json + the shadcn add command), the component catalog (variants, sizes, props), the Base UI render-prop composition convention, theming (the @nebari/theme tokens, CSS variables, and light/dark), and motion (duration/easing tokens, entrance animations, overlay transitions, accessibility guardrails). |
Using the Nebari design system
Nebari design is a
shadcn component registry styled with the
Nebari brand. You install its components into this app with the shadcn CLI —
they're copied into your codebase, but Nebari treats them as upstream-managed
source: you extend them at the call site rather than editing the installed
files (see Treat installed components as managed).
This skill covers setup, the catalog, the composition convention, and theming.
Step 1 — register the @nebari namespace (once)
Add the @nebari registry to the consumer's components.json so
shadcn add @nebari/<name> resolves. The items are served as JSON from the
project's GitHub Pages site:
{
"$schema": "https://ui.shadcn.com/schema.json",
"registries": {
"@nebari": "https://nebari-dev.github.io/nebari-design/r/{name}.json"
}
}
{name} is the placeholder shadcn substitutes per item. This block sits
alongside the app's existing style / tailwind / aliases config — it does
not replace them. A standard shadcn-initialized project (run npx shadcn init
first if components.json doesn't exist yet) is the only prerequisite.
Step 2 — install components
npx shadcn add @nebari/<name>
Most components depend on the shared cn() helper (the utils item) and the
theme tokens, and shadcn pulls those registryDependencies in automatically —
you don't list them yourself. Install the theme explicitly the first time (see
Theming):
npx shadcn add @nebari/theme
npx shadcn add @nebari/button
Installed files land under the app's configured aliases (@/ui, @/lib), so
imports look like import { Button } from '@/components/ui/button' — match the
host app's existing alias resolution.
Treat installed components as managed
shadcn add copies the source into your repo, but treat the installed ui/*
and lib/* files as upstream-managed, not app-owned. Don't edit them.
- Why — these files are regenerated by
shadcn add on every upgrade, so any
local edit is silently overwritten and lost. Editing also forks you away from
the shared design system: the next consumer of the same component gets
different behavior than yours, which is exactly what a design system exists to
prevent.
- To change look or behavior, do it at the call site, never in the file:
- Pass extra classes via
className — they're merged with cn(), so your
classes win without touching the source.
- Swap the rendered element with the Base UI
render prop (link button, etc.)
instead of rewrapping or editing.
- Build a thin wrapper component in your own app that composes the Nebari
component when you need app-specific defaults or behavior.
- Put custom styling and motion in your own
globals.css or a CSS module — the
only files you edit are ones you own.
- If a component genuinely can't express what you need through the above,
don't fork it locally — request the change upstream in
nebari-design so every app gets
it and stays consistent. Until then, wrap rather than edit.
Discovering what's available
The registry is the source of truth for the catalog — don't rely on a
hard-coded list here, which would drift as components are added. To see what
exists and learn a component's exact API:
-
List every item — fetch the registry index, which names and describes each
installable item (components, the utils helper, the theme, this skill):
curl -s https://nebari-dev.github.io/nebari-design/r/registry.json
-
Inspect one before installing — shadcn view prints an item's
description, dependencies, and its full source:
npx shadcn view @nebari/button
-
After installing, read the source — components are copied into your repo
(treat them as managed — see
Treat installed components as managed).
The exact variant/size names and props live in the
component's cva block and its props type; open the installed .tsx (e.g.
@/components/ui/button.tsx) — that file, not any doc, is authoritative.
How Nebari components are built
Every component follows the same shape, so once you've seen one you can use any:
- Styled with semantic theme tokens (
bg-primary, text-muted-foreground,
…), so it follows light/dark automatically — never restyle with raw hex or
dark: variants.
- Sets stable
data-slot / data-variant / data-size attributes you can
target in CSS or tests.
- Exports its
cva class function (buttonVariants, badgeVariants, …)
alongside the component for reuse.
variant / size (where present) select the look; read the source for the
exact set a given component offers.
Button is a representative example — variants, sizes, a loading state, and
render-prop composition:
import { Button } from '@/components/ui/button';
<Button>Save</Button>
<Button variant="outline" size="sm">Cancel</Button>
<Button variant="destructive" loading loadingText="Deleting…">Delete</Button>
<Button size="icon" aria-label="Settings"><Settings /></Button>
Composed components (e.g. Alert with AlertTitle / AlertDescription /
AlertAction) export their parts as named exports from the same module — the
installed source and shadcn view show how the pieces fit together.
Composition (Base UI render prop)
Polymorphic components (Button, Badge) use Base UI's render prop to
change the rendered element while keeping their styling — this is Nebari's
equivalent of Radix's asChild. Pass an element and the component merges its
classes, data-* attributes, and props onto it:
<Button render={<a href="/docs" />}>Docs</Button>
<Badge variant="outline" render={<a href="/tag/new" />}>new</Badge>
The component's data-slot and styling are preserved on the swapped element, so
a <Button render={<a />}> is still a fully-styled button that's semantically a
link.
Theming
Install the theme once; it writes the Nebari brand color tokens and radius into
the app's global stylesheet as CSS variables for both light and dark modes:
npx shadcn add @nebari/theme
-
Tokens are semantic (--primary, --muted-foreground, --destructive,
--info, --success, --warning, --border, --ring, chart + sidebar
tokens, …) and consumed in components via Tailwind utilities like
bg-primary / text-muted-foreground. Never hard-code brand hex values or
add dark: variants — the tokens flip automatically.
-
Light/dark: the theme defines a light set on :root and a dark set;
toggle by adding/removing the .dark class on an ancestor (typically <html>).
Every Nebari component re-themes from the active token set with no per-component
changes.
-
Fonts: the tokens reference Geist (sans) and IBM Plex Mono (mono) but
don't ship the webfonts. To actually load them, install and import them at the
app entry point (otherwise they fall back to the system stacks):
npm i @fontsource-variable/geist @fontsource/ibm-plex-mono
import '@fontsource-variable/geist';
import '@fontsource/ibm-plex-mono/400.css';
import '@fontsource/ibm-plex-mono/500.css';
Motion
The @nebari/theme item ships motion tokens alongside the color and radius
tokens. Use them to add consistent, accessible, on-brand animation at the
call site — never by editing the installed ui/* component source (which
shadcn add would overwrite on the next upgrade).
Available tokens
| Token | Value | Use |
|---|
--duration-fast | 100ms | Micro-interactions (icon swap, badge count) |
--duration-base | 200ms | Standard enter/exit (tooltip, dropdown) |
--duration-slow | 350ms | Page-level overlays (dialog, sheet, drawer) |
--ease-standard | cubic-bezier(0.4, 0, 0.2, 1) | Most transitions |
--ease-emphasized | cubic-bezier(0.2, 0, 0, 1) | Overlays sliding into view |
And four ready-made Tailwind animate-* utilities (backed by @keyframes
that the theme installs):
| Utility | Effect |
|---|
animate-fade-in | opacity: 0 → 1 |
animate-fade-out | opacity: 1 → 0 |
animate-slide-up-fade | fade + translateY(6px → 0) |
animate-slide-down-fade | fade + translateY(0 → 6px) |
Hard rules
- Always gate on
motion-safe: — Nebari targets WCAG 2.1 SC 2.3.3.
Every animation class must be prefixed: motion-safe:animate-fade-in.
- Use tokens, never hardcode durations — write
var(--duration-base) in
custom CSS, or use the pre-built animate-* utilities. Never 0.2s.
- Animate
opacity and transform only — layout properties (height,
width, padding, top) force reflows; don't animate them.
- CSS + Base UI first — reach for JS animation libraries only when the
CSS primitives are insufficient (see the escape hatch below).
Recipes
Entrance — fade / slide into view
<Card className="motion-safe:animate-fade-in" />
<li className="motion-safe:animate-slide-up-fade">…</li>
Press feedback — scale on active
<Button className="motion-safe:active:scale-95 transition-transform">
Submit
</Button>
Overlay enter/exit — Base UI data-starting-style / data-ending-style
Base UI popups (Dialog, Select, Tooltip, …) apply data-starting-style and
data-ending-style attributes during CSS transitions so you can define enter
and exit animations purely in CSS. Use a plain <style> block or a CSS
module — do not add inline styles to the component source:
[data-starting-style] {
opacity: 0;
transform: translateY(4px);
}
[data-starting-style],
[data-ending-style] {
transition-property: opacity, transform;
transition-duration: var(--duration-base);
transition-timing-function: var(--ease-emphasized);
}
Loading / skeleton shimmer
Skeletons use a repeating animation; stay within the token vocabulary:
<div
className={cn(
'rounded bg-muted',
'motion-safe:animate-[shimmer_1.5s_var(--ease-standard)_infinite]',
)}
/>
Add the @keyframes shimmer rule to globals.css only if you adopt this
pattern in your project — it is not shipped by the theme.
cn() / tailwind-merge gotcha
When you add transition-* classes to a Nebari component via className,
tailwind-merge (inside cn()) will deduplicate them against any transition
classes already applied by the component's cva block. Re-enumerate every
transition property you need so no property silently disappears:
<Button
className={cn(
'motion-safe:transition-[color,background-color,transform]',
'motion-safe:duration-[--duration-base]',
'motion-safe:active:scale-95',
)}
/>
<Button className="motion-safe:transition motion-safe:active:scale-95" />
Call-site guardrail
Never edit files under ui/ to add animation. Those files are managed by
shadcn add and will be overwritten on upgrade. All motion belongs in your
app's globals.css, a CSS module, or a wrapper component that uses the Base
UI render prop.
JS escape hatch — Motion via Base UI render
For sequences that CSS alone can't express (exit animations that depend on JS
state, staggered lists, spring physics), use the
Motion library through Base UI's render prop. This
keeps the original component's classes and data-* attributes intact:
import { motion } from 'motion/react';
import { Button } from '@/components/ui/button';
<Button render={<motion.button animate={{ scale: [1, 0.96, 1] }} />}>
Save
</Button>
Install Motion only when needed — it's not a default dependency of the
registry.
Conventions when building with Nebari components
- Prefer composing the existing components and variants over restyling them; pass
extra classes via
className (it's merged with cn(), so your classes win).
- Reach for
render when you need a different element (link button, etc.) rather
than rewrapping.
- Use the semantic tokens for any custom styling so the result stays
light/dark-correct and on-brand.
- Need a component that isn't in the catalog yet? Fall back to the upstream
shadcn component, then style it with the same semantic tokens.