# Styling reference

The stylesheet is the source of truth for which classes and custom properties
exist. **Never hardcode a class name or token value from memory** — verify it
against the shipped CSS first.

## Where the stylesheet lives

```
node_modules/@neo4j-ndl/base/lib/neo4j-ds-styles.css
```

In the Needle repo source it is generated from `packages/base/src/` (Tailwind
preset + token JSON), but the built file above is what a consuming app ships.
When checking whether a class or custom property exists, grep the built file —
not the source `.pcss` — because Tailwind tree-shakes classes that no source
file uses.

### What the prebuilt stylesheet contains

Besides the `ndl-*` component classes and the CSS custom properties, the
prebuilt stylesheet ships only two groups of `n-` classes:

| Group | Classes | Why they ship |
|-------|---------|---------------|
| Typography | `n-display`, `n-title-1` … `n-title-4`, `n-subheading-large/medium/small`, `n-body-large/medium/small`, `n-label`, `n-code` | Defined directly in the base typography stylesheet |
| Semantic colors | `n-text-*`, `n-bg-*`, `n-border-*` for the color families below, plus `hover:` variants (and `active:` for `n-bg-*`) | Safelisted in the Tailwind preset |

**No other Tailwind utility is guaranteed to be in the prebuilt stylesheet.**
Layout, spacing, sizing, radius and shadow utilities — `n-flex`,
`n-items-center`, `n-gap-token-8`, `n-p-4`, `n-w-full`, `n-rounded-lg`,
`n-shadow-raised` and the rest — are all absent. A utility only ends up in the
file if Needle's own component source uses it, and component source no longer
uses Tailwind utilities at all. To use them, the consuming app needs its own
Tailwind build; see
[whether a utility class works](#whether-a-utility-class-works-is-a-property-of-the-app-not-the-class).

### Quick verification

```sh
# Does a class exist?
CSS=node_modules/@neo4j-ndl/base/lib/neo4j-ds-styles.css
grep -qE '\.n-body-medium\b' "$CSS" && echo "exists" || echo "MISSING"

# Does a custom property exist?
grep -qE -- '--space-8:' "$CSS" && echo "exists" || echo "MISSING"
```

## Tokens only — never raw color values

Generated or hand-written code must **never** contain hardcoded `hex`, `rgb()`,
`oklch()`, or arbitrary Tailwind color values. All colors come from semantic
theme tokens that automatically resolve to the correct light/dark value:

```css
/* ✅ Token — adapts to theme */
color: var(--theme-color-neutral-text-default);
background: var(--theme-color-primary-bg-weak);

/* ❌ Hardcoded — breaks dark mode, drifts from the system */
color: #1a1b1d;
background: rgb(10 97 144);
```

### Semantic color families

Each family has the same suffix set (`text`, `icon`, `bg-weak`, `bg-strong`,
`bg-status`, `border-strong`, `border-weak`, and for primary/danger also
`hover-*`, `pressed-*`):

| Family | CSS custom property prefix | Utility class prefix |
|--------|---------------------------|---------------------|
| Neutral | `--theme-color-neutral-*` | `n-text-neutral-*`, `n-bg-neutral-*`, `n-border-neutral-*` |
| Primary | `--theme-color-primary-*` | `n-text-primary-*`, `n-bg-primary-*`, `n-border-primary-*` |
| Danger | `--theme-color-danger-*` | `n-text-danger-*`, `n-bg-danger-*`, `n-border-danger-*` |
| Warning | `--theme-color-warning-*` | `n-text-warning-*`, `n-bg-warning-*`, `n-border-warning-*` |
| Success | `--theme-color-success-*` | `n-text-success-*`, `n-bg-success-*`, `n-border-success-*` |
| Discovery | `--theme-color-discovery-*` | `n-text-discovery-*`, `n-bg-discovery-*`, `n-border-discovery-*` |

**Do not use `n-bg-dark-*` / `n-bg-light-*` prefixed classes.** They pin a
specific theme and break when the user switches. The non-prefixed classes
above resolve to the active theme automatically.

### Space tokens

The custom properties are always available. The utility classes exist only in
an app with its own Tailwind build — none of them are in the prebuilt CSS.

| Token | Value | Utility class (gap; also `p-`, `m-`, `w-`, …) |
|-------|-------|-----|
| `--space-2` | 2px | `n-gap-token-2` |
| `--space-4` | 4px | `n-gap-token-4` |
| `--space-6` | 6px | `n-gap-token-6` |
| `--space-8` | 8px | `n-gap-token-8` |
| `--space-12` | 12px | `n-gap-token-12` |
| `--space-16` | 16px | `n-gap-token-16` |
| `--space-20` | 20px | `n-gap-token-20` |
| `--space-24` | 24px | `n-gap-token-24` |
| `--space-32` | 32px | `n-gap-token-32` |
| `--space-48` | 48px | `n-gap-token-48` |
| `--space-64` | 64px | `n-gap-token-64` |

### The same number means different things in the two systems

This is the easiest way to get spacing silently wrong, and it is invisible to
`tsc`, to the build and to review:

| What you write | Renders |
|----------------|---------|
| `<Flex gap="16">` — component prop | **16px** |
| `className="gap-token-16"` — token-named utility | **16px** |
| `className="gap-16"` — Tailwind numeric utility | **64px** |

A Needle spacing prop takes a **token name that is its pixel value**. A Tailwind
numeric utility takes a **step on Tailwind's 4px scale**. So `gap="8"` is 8px
while `gap-8` is 32px, and the two scales never coincide at any step — mixing
them multiplies the spacing you meant by four.

Two gap scales are **defined in the preset**: the token-named one
(`n-gap-token-2` … `n-gap-token-64`) and Tailwind's numeric one (`n-gap-1`,
`n-gap-1.5`, `n-gap-2`, `n-gap-3` …). Both are valid wherever Tailwind is
generating classes, and neither is in the prebuilt stylesheet — see
[which classes work in your app](#whether-a-utility-class-works-is-a-property-of-the-app-not-the-class).
`Flex` and `Box` accept token values directly (`gap="8"`, `padding="12"`) and
sidestep the question entirely, which is why they are preferred.

### Border-radius tokens

| Token | Value |
|-------|-------|
| `--border-radius-none` | 0 |
| `--border-radius-sm` | 4px |
| `--border-radius-md` | 6px |
| `--border-radius-lg` | 8px |
| `--border-radius-xl` | 12px |
| `--border-radius-2xl` | 16px |
| `--border-radius-3xl` | 24px |
| `--border-radius-full` | 9999px |

`Box` and `Flex` accept `borderRadius="xl"` directly. The preset also defines
`n-rounded-sm/md/lg/xl/2xl`, but only an app with its own Tailwind build can use
them — they are not in the prebuilt CSS.

### Shadow tokens

| Token | Utility class (Tailwind build only) |
|-------|---------------|
| `--theme-shadow-raised` | `n-shadow-raised` |
| `--theme-shadow-overlay` | `n-shadow-overlay` |

### JS token imports

For inline styles or JS logic, import the token object:

```tsx
import { tokens } from '@neo4j-ndl/base';

tokens.space['8']        // '8px'
tokens.borderRadius.xl   // '12px'
tokens.colors.primary['500']  // raw palette value
```

Prefer CSS custom properties in `style` over JS token values where possible —
`var(--theme-color-*)` adapts to theme, a JS palette value does not.

## Whether a utility class works is a property of the app, not the class

Two independent questions decide it, and the app's `tailwind.config` answers
both. Read it before writing a single class.

**1. Does the app run its own Tailwind build?**

| | What works |
|---|-----------|
| **Yes** — it consumes the Needle preset | Any standard Tailwind utility, generated on demand: `sr-only`, `p-token-6`, `gap-3` and the rest |
| **No** — it only imports `@neo4j-ndl/base/lib/neo4j-ds-styles.css` | Only the [typography and safelisted color classes](#what-the-prebuilt-stylesheet-contains). No layout or spacing utilities — everything else silently does nothing |

**If you need layout or spacing utilities, the app must run its own Tailwind
build.** That is the only setup in which a given utility is guaranteed to
exist.

**2. What prefix does it use?**

The Needle preset sets `prefix: 'n-'`, but an app can override it and the app's
config wins. The Aura console monorepo sets `prefix: ''`, so utilities there are
written unprefixed — `flex`, `gap-4`, not `n-flex`. An app with no Tailwind build
at all gets only the prebuilt stylesheet's classes, and those are `n-` prefixed.

Getting this wrong is invisible: a class at the wrong prefix compiles, passes
type-checking and renders nothing. And it can look fine by accident — in a
`prefix: ''` app that also imports the prebuilt CSS, `n-text-neutral-text-weak`
still renders because it is safelisted, while `n-flex` does not.

### Why the prebuilt stylesheet has no layout utilities

The prebuilt stylesheet is **not a utility library.** Tailwind only emits a
utility when a scanned source file uses it, and the production build scans
only Needle's own component source (`packages/react/src`, excluding stories and
Playwright specs). Component source does not use Tailwind utilities — components
style themselves through `ndl-*` classes, with any Tailwind applied inside the
component stylesheets via `@apply`. `@apply` **inlines the declarations** into
the component's rule rather than emitting a reusable selector, so `@apply
n-flex` inside a component stylesheet does not make `.n-flex` available to you.

The result is that only the typography classes and the safelisted colors are
guaranteed. Do not rely on any other utility being in the prebuilt CSS, even if
you find it in some version of the file.

This is also why a class can work in Needle's Storybook and fail in a consuming
app: the non-production build widens the scan to include stories and
`packages/storybook/src`, so a class written in a story is generated there.
`n-sr-only` is a typical example — it works in Storybook and is absent from the
published CSS.

If there is no `tailwind.config` in the app, treat the typography and color
classes as the complete set of utilities and use component props or inline
styles with tokens for everything else.

An app that wants the full utility set opts into the first setup by consuming the
preset, which `@neo4j-ndl/base` exports from its main entry:

```js
// tailwind.config.js
import { tailwindConfig } from '@neo4j-ndl/base';

export default {
  presets: [tailwindConfig],
  content: ['./src/**/*.{ts,tsx}'],
};
```

### Grep the installed copy

When checking the stylesheet, read the version from
`node_modules/@neo4j-ndl/base/package.json` and grep *that* copy; never a copy
from elsewhere on disk (a Needle checkout's `lib/` may be a non-production build
with Storybook's extra classes in it), and never this document alone.

### Two things that are fictional in both setups

Everything above is a real utility somewhere. These are not — they are invented
namespaces that resolve to nothing no matter how the app is built, and both have
been written by generated code more than once:

- **`--theme-palette-*` is not a namespace.** The semantic one is
  `--theme-color-*`. There are zero occurrences of `--theme-palette` in the
  package.
- **Radius is not under `--theme-*`.** The tokens are `--border-radius-sm|md|lg|
  xl|2xl|3xl|full|none`, so `--theme-radius-lg` is nothing.

A `var()` with no fallback renders nothing when the property is undefined, so
both fail silently rather than loudly.

## Layout without a Tailwind build

In an app that only imports the prebuilt stylesheet, **every layout utility —
`n-flex`, `n-items-center`, `n-justify-between`, `n-gap-*`, `n-p-*`, `n-w-*`,
`n-text-right` and the rest — compiles and renders nothing.** It is a silent
layout bug, invisible to `tsc` and to review. Use these instead:

| Need | Use |
|------|-----|
| Flex layout, alignment, gap | `<Flex flexDirection="row" alignItems="center" gap="8">` |
| Padding, radius | `<Box padding="12" borderRadius="xl">` |
| Text style | `<Typography variant="body-medium">`, or the `n-body-medium`-style classes |
| Text, background, border color | The safelisted color classes, e.g. `n-bg-neutral-bg-weak` |
| Anything else (margin, width, …) | An inline `style` using a token: `style={{ margin: 'var(--space-8)' }}` |

Check each component's doc for its exact prop names. If the app runs its own
Tailwind build with the Needle preset, all of these utilities are available
there and this section does not constrain you.

## How to verify before writing

1. **Check component props first.** `Flex`, `Box`, `Typography` accept typed
   token values that are always correct — prefer them over utility classes or
   `htmlAttributes`. See the customization hierarchy below.
2. **Check whether the app has its own Tailwind build** and which `prefix` it
   uses. Without one, only typography and color utilities exist.
3. **Check the stylesheet**: grep `neo4j-ds-styles.css` for the exact class
   or property.
4. **Never copy a class from another library** (MUI, Radix, Chakra, plain
   Tailwind). They won't exist in the Needle stylesheet.

## Customization hierarchy

When adjusting a Needle component, work through these in order. Each level is
more error-prone than the one before it:

1. **Component props** — typed, token-constrained, and type-checked. Always the
   first choice. Examples: `gap="8"`, `padding="12"`, `borderRadius="xl"`,
   `variant="danger"`, `size="small"`, `isFluid`, `ariaLabel`.

2. **`className`** — for utility classes the component doesn't cover (e.g.
   `n-text-neutral-text-weak` or `n-body-small` on a plain element, or when
   composing). These are unchecked strings: verify every class against
   `node_modules/@neo4j-ndl/base/lib/neo4j-ds-styles.css`, or against the app's
   own Tailwind build. Layout utilities such as `n-flex` work only with an app
   Tailwind build. A wrong class compiles and silently renders nothing.

3. **`htmlAttributes`** — escape hatch for native HTML attributes, `aria-*`, and
   `data-*` the component doesn't expose as props. Valid reasons, drawn from the
   component docs:

   - `id` — for `aria-controls` references, portal targets, or form label wiring
   - `aria-*` — when no dedicated prop exists (e.g. `aria-haspopup` or
     `aria-expanded` on a trigger button, `aria-label` on a `TextInput` without
     a label)
   - `data-*` — custom data attributes for testing or integration
   - Native HTML attributes not surfaced as props (e.g. `type="email"`,
     `type="password"`, `max`/`min`/`step` on a `TextInput`, `href` on
     `Typography` with `as="a"`, `title` on an icon button)

   Do **not** use `htmlAttributes` to override behavior the component already
   provides — e.g. setting `role="alertdialog"` on a `Dialog` (use `variant`
   instead) — or for styling (use component props or `className`).
