npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nswds/ui

v8.0.0

Published

UI components for NSW Government digital products, providing reusable design elements and patterns for consistent user interfaces.

Readme

@nswds/ui

Reusable design system components for NSW Government digital products. Accessible, headless-first React components built on Base UI primitives and styled with NSW design tokens from @nswds/tokens.

Installation

npm install @nswds/ui

Requires React 19. The package is ESM-only and ships compiled JavaScript, TypeScript types, and a precompiled stylesheet — it needs no Tailwind setup and no separate @nswds/tokens install. @nswds/ui/styles.css already has the token values inlined; @nswds/tokens is a build-time dependency only.

The registry channel is different: because it copies editable source (which references the raw token CSS) rather than the precompiled stylesheet, it does install @nswds/tokens. See the registry installation guide.

Usage

Import the stylesheet once at your app's entry point, then use the components:

import '@nswds/ui/styles.css'

import { Button } from '@nswds/ui'

export default function Demo() {
  return <Button color='primary'>NSW button</Button>
}

@nswds/ui/styles.css ships the full token foundation (NSW palette, masterbrand theme, semantic tokens) plus all component styles, so components render correctly with no extra wiring.

Import order

If your app also imports the @nswds/tokens Tailwind bridges — you only need them when your own Tailwind build has to know the NSW scales, so bg-primary-800 or text-text-muted written in your code resolves — put @nswds/ui/styles.css after them, and your own Tailwind build after that:

/* Bridges first: they teach your Tailwind build the NSW scales. */
@import '@nswds/tokens/tailwind/colors/global/oklch.css';
@import '@nswds/tokens/tailwind/colors/semantic/oklch.css';

/* @nswds/ui after the bridges: it is the only one of them that carries dark mode. */
@import '@nswds/ui/styles.css';

/* Your own Tailwind build last — see the next section. */
@import 'tailwindcss';

The bridges must come first because both stylesheets emit an unlayered :root block of light-mode values, and only @nswds/ui/styles.css also ships the [data-theme='dark'], .dark block. :root and .dark have identical specificity, so the block that appears last wins. Import a bridge afterwards and its light values land last: every bg-* utility still flips to dark (those are compiled classes, not tokens) while the semantic role tokens stay light — so backgrounds go dark and text stays dark on top of them. Nothing errors; it just looks broken.

Using it with your own Tailwind build

@import 'tailwindcss' goes last for a different reason: it leaves you with two independently-sorted sets of utilities in the same utilities cascade layer, and a media query adds no specificity. Within one Tailwind build the sorter guarantees .lg\:justify-start is emitted after .justify-center; across two builds nothing does, so whichever half comes last wins any tie. Ours going first means your utilities win those ties, which is what you want: a class you wrote should beat one you didn't.

Reverse it and the same mechanism works against you — our plain .inline-flex (Button's base) would outrank your hidden sm:inline-flex, showing a button you meant to hide on mobile.

Our components no longer depend on that tiebreak. Where a component needs a responsive override it uses mutually exclusive variants (max-lg:justify-center lg:justify-start) rather than a bare utility plus an override of it, so no rule you emit can displace one of ours on an element you never referenced. This is enforced on every build — see scripts/check-cascade-safety.mjs.

If you do hit a conflict, a call-site override outranks both halves:

<Footer className='[&_[data-slot=footer-legal-links]]:lg:justify-start' />

Don't reach for a named cascade layer to pin the order. @import '@nswds/ui/styles.css' layer(nswds) looks tidy, but it also layers the :root and [data-theme='dark'], .dark token blocks the file carries, and layered custom properties lose to any unlayered :root — including the ones the bridges above ship. That is the dark-mode failure described in the previous section, with no import order that recovers it.

Dark mode

Toggle class="dark" or data-theme="dark" on a root element (for example with next-themes). Semantic tokens and dark: utilities both key off either marker, and both match the element carrying it as well as its descendants.

<html lang='en' className='dark'>

A scoped <div class="dark"> gives you only a partial dark mode. The semantic tokens are declared on :root in terms of mode-aware role tokens (--primary: var(--action-default)), and a var() is substituted where it is declared — so on a nested element the role token changes too late. The dark: utilities flip (those are compiled classes) while the semantic colours stay light. Put the marker on the root element.

Icons

Icons are tree-shakeable from the barrel, or importable individually:

import { IconSearch } from '@nswds/ui/icons'
import { IconAdd } from '@nswds/ui/icons/add'

export function Toolbar() {
  return (
    <>
      <Button leadingVisual={IconSearch}>Search</Button>
      <Button leadingVisual={IconAdd}>Add</Button>
    </>
  )
}

Icons in a React Server Component

Icon slots — leadingVisual, trailingVisual, trailingAction, and Footer's socialLinks[].icon — take either the component or an element. In a server component, pass the element:

// app/page.tsx — a server component, no 'use client'
import { ButtonLink } from '@nswds/ui'
import { IconDownload } from '@nswds/ui/icons'

export default function Page() {
  return (
    <ButtonLink href='/report.pdf' leadingVisual={<IconDownload />}>
      Download
    </ButtonLink>
  )
}

Passing the component itself (leadingVisual={IconDownload}) throws there:

Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".

Button, ButtonLink and FooterSocialLink are client components; the icon modules deliberately are not, so they stay server-renderable and a <IconDownload /> on a server page ships no JavaScript. That means a bare icon function is just a function value, and functions do not serialise across the RSC boundary — an element does. Client components ("use client") can use either form.

When you genuinely need the component form on a server page — an icon chosen from a lookup table, a prop typed as ElementType, a config object shared between server and client code — import from @nswds/ui/icons/client, which re-exports the same set as client references:

// app/page.tsx — a server component, no 'use client'
import { ButtonLink } from '@nswds/ui'
import { IconDownload } from '@nswds/ui/icons/client'

export default function Page() {
  return (
    <ButtonLink href='/report.pdf' leadingVisual={IconDownload}>
      Download
    </ButtonLink>
  )
}

That gives up the zero-JavaScript property above — the icon module is sent to the browser — so prefer the element form where it fits.

Brand icons

The icon set is Material Symbols and contains no third-party brand marks, but Footer's socialLinks[].icon needs one per channel. The six the NSW Government uses ship under their own subpath, already client-safe, so either form works from a server component:

import {
  IconFacebook,
  IconGitHub,
  IconInstagram,
  IconLinkedIn,
  IconX,
  IconYouTube,
} from '@nswds/ui/icons/brands'

Components

62 components, plus 12 copy-and-adapt blocks on the registry channel. The complete catalogue — with every export and a description per item — is in the component reference.

NSW-original (no upstream equivalent — the reference is their only documentation): Callout, Container, DescriptionList, ExpandableSearch, Footer, Header, LabeledSeparator, Link, LinkCard, Logo, MainNav, Masthead, OnThisPage, PushMenu, Section, SideNav, SiteSearch, SkipLink, StepIndicator, TabNav, ThemeSwitcher.

Shadcn shapes on Base UI primitives, NSW-styled: Accordion, AspectRatio, Avatar, Badge, Breadcrumb, Button, ButtonGroup, Card, Carousel, Checkbox, Collapsible, Combobox, DirectionProvider, Drawer, Field, HoverCard, Input, InputGroup, InputOTP, Kbd, Label, NativeSelect, Pagination, Popover, Progress, RadioGroup, ResizablePanelGroup, ScrollArea, Select, Separator, Sheet, Slider, Spinner, Switch, Table, Tabs, Textarea, Toaster, Toggle, ToggleGroup, Tooltip.

Plus the icon set under @nswds/ui/icons and the brand marks under @nswds/ui/icons/brands.

Hooks: useChromeHeight, which measures a sticky header and publishes its height as a CSS custom property, for scroll-padding-top, MainNav's --main-nav-top, and OnThisPage's scroll-spy offset.

Every interactive component wraps a Base UI primitive, which provides focus management, keyboard navigation, and ARIA semantics. Each component also exports its cva variants function (for example buttonVariants) so you can extend styling.

Theming

Every visual property traces back to a CSS custom property token, in four layers: Tailwind utilities → shadcn semantic tokens (--primary, --background, --border) → NSW role tokens (--action-default, --text-default) → the NSW primitive palette.

To re-brand, override the masterbrand ramps at :root, after importing the stylesheet:

:root {
  --primary-800: oklch(0.29 0.12 150);
  /* …the remaining steps of the ramp… */
}

Supply the whole ramp — components draw on different steps for fills, borders and hover states.

Overriding tokens in a scoped selector mostly does not work, because a var() is substituted where it is declared and every layer is declared at :root. In particular Button's primary colour resolves to the masterbrand ramp (--color-primary-800), not to --primary. See Theme and re-brand for the rule and the working escape hatch.

Prefer the source in your repo?

The same components are distributed through a shadcn registry at https://ui.digital.nsw.gov.au/registry, which copies the component source directly into your project for teams that want to adapt it:

npx shadcn@latest add https://ui.digital.nsw.gov.au/registry/r/button.json

The npm package gives you versioned, upgradeable components; the registry gives you editable source. See the registry installation guide for the full setup, including the one-time @nswds namespace configuration.

License

MIT