We have all been through the same scenario: a new project is starting, and we go searching for polished UI components. Sure, we could build everything from the ground up, but the goal is to ship features, not reinvent the wheel. We need a set of tools that lets us move quickly without skimping on quality or accessibility.

This leads us into the landscape of Angular component libraries.
While these libraries offer an impressive range of components and generally include robust accessibility support, many of them carry a heavy, standardized corporate look. This default styling often clashes with the specific vision for a project. More critically, these libraries rarely provide a simple path for deep customization, making it difficult to adapt them and truly make the interface our own.

Then we cast a glance over at the React world and see the brilliant projects flourishing on top of RadixUI and shadcn. And I have to admit, every time I see what `shadcn/ui` enables, I feel a pang of envy.

shadcn - A revolutionary UI library (for React)

What makes shadcn/ui so special? It ships with an extensive catalog of components, all of which arrive with professional, ready-to-use styling. Yet, despite this out-of-the-box polish, it gives you full rein to tweak and configure every last UI primitive according to your exact requirements.

What is the secret behind this flexibility?

  1. It is engineered on top of RadixUI, which provides a collection of UI building blocks that come entirely un-styled. This foundation offers a straightforward, extensible API and strong accessibility out of the box.
  2. The visual layer relies on TailwindCSS classes and CSS variables, striking the perfect balance between adaptability and adherence to sound design systems.
  3. Rather than forcing you to install a pre-packaged theme, it lets you copy its meticulously built primitives straight into your application directory. This means you have complete ownership of the code and can modify every element to suit your specific needs.

The only snag for Angular developers is that `shadcn` is inherently tied to the React ecosystem...

So, picture this: an accessible, open-source Angular component library that is fundamentally bare-bones, offering you complete creative freedom over styling—a true `shadcn` equivalent for the Angular community.

spartan/ui - shadcn for Angular

This is where spartan/ui steps in. It is a forward-thinking suite of Angular UI primitives that are both accessible and un-styled in their default state.

brain & helm - The fundamental pieces of spartan/ui

To deliver a development flow that mirrors the `shadcn` experience, `spartan/ui` is divided into two core packages:

  1. spartan/ui/brain is dedicated to streamlining this exact process. It provides a diverse set of un-styled component building blocks. These can be effortlessly adapted to align with the specific visual identity and functional logic of your project.
  2. spartan/ui/helm offers a set of pre-configured styles that are crafted with TailwindCSS and CSS variables. Following the `shadcn` philosophy, these can be directly copied into your workspace, granting you full authority over the source code, its aesthetic, and the end-user experience.

@spartan-ng/cli - one command to rule them all

For ease of use, spartan/ui includes a specialized CLI. This tool lets you add its components to your Nx or Angular environments with zero friction. You can pull in any of the library's 30+ primitives with a simple single command.

The CLI's utility, however, doesn't end with adding components. It also enables you to import from a selection of 12 distinct themes into your Angular or Nx applications, giving you direct command over the final look of your software.

Building your initial spartan application

Now, let's walk through the process of getting spartan/ui operational in a project.

Prefer a video walkthrough? You can find one on YouTube.

Creating an Nx Angular workspace

spartan/ui adopts the same philosophy as shadcn: you maintain ownership of the code responsible for styling, extending, and composing your UI components.

While a standalone API is in development, Nx provides excellent tooling for this exact scenario. As a result, the first iteration of the spartan/ui CLI is built as an Nx plugin.

For this guide, we'll set up a fresh Angular application within an Nx workspace.

Executing create-nx-workspace

Once again, Nx simplifies this process. Execute the following command.

npx create-nx-workspace@latest
Enter fullscreen mode Exit fullscreen mode

When the setup asks for input:

  1. Provide a descriptive project name.
  2. Opt for Angular as your technology stack.
  3. Decide to create a standalone project.
  4. Critical: Ensure you select CSS for your styles.
  5. Include an optional end-to-end test runner.
  6. Choose to generate standalone components.
  7. Add routing only if it's needed for your project.

Once you've made these choices, Nx will take over, installing dependencies and preparing your Angular workspace.

Cleaning up the generated code

I prefer to keep a component's template and styles in the same file as its class. Consequently, I removed the src/app/app.component.html and src/app/app.component.css files that the workspace generator produces. I also deleted the src/app/nx-welcome.component.ts file and replaced the contents of my src/app/app.component.ts with the following:

import { Component } from '@angular/core';

@Component({
  standalone: true,
  imports: [],
  selector: 'app-root',
  template: `<button>Hello from {{title}}</button>`
})
export class AppComponent {
  title = 'sparta';
}
Enter fullscreen mode Exit fullscreen mode

Before we can incorporate spartan/ui, there's one more setup step.

Integrating TailwindCSS

Since spartan/ui is built using TailwindCSS, we must have it configured in our project.

Nx handles this for us with little effort. Run the command below and pick your application when prompted:

npx nx g @nx/angular:setup-tailwind
Enter fullscreen mode Exit fullscreen mode

This command generates a tailwind.config.ts file and installs the required packages. Let's move on.

Setting up @spartan-ng/cli

Our project is now ready for spartan/ui. To simplify the process, we'll use the Nx plugin, which can be installed with this command:

npm i @spartan-ng/cli
Enter fullscreen mode Exit fullscreen mode

Adding @spartan-ng/ui-core

Next, we'll incorporate the @spartan-ng/ui-core library.

npm i @spartan-ng/ui-core
Enter fullscreen mode Exit fullscreen mode

This package provides several helpers, such as the hlm function for merging tailwind classes. Most notably, it includes the @spartan-ng/ui-core/hlm-tailwind-preset, which holds all the necessary tailwind extensions for our spartan/ui/helm directives and components to function correctly.

Configuring tailwind.config.js

Now we must integrate this spartan-specific configuration into our TailwindCSS setup. This is done by adding @spartan-ng/ui-core/hlm-tailwind-preset to the presets array within your config file:

const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
const { join } = require('path');

/** @type {import('tailwindcss').Config} */
module.exports = {
  presets: [require('@spartan-ng/ui-core/hlm-tailwind-preset')],
  content: [
    join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
    ...createGlobPatternsForDependencies(__dirname),
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Incorporating CSS variables

To finish the TailwindCSS configuration, we must also add the spartan-specific CSS variables to our main stylesheet, typically a styles.css file located in the src folder of the application.

Since we're using Nx, our plugin can handle this step automatically:

npx nx g @spartan-ng/cli:ui-theme
Enter fullscreen mode Exit fullscreen mode

When prompted:

  1. Select the sole application within the project.
  2. Pick a theme that appeals to you.
  3. Choose a preferred border-radius.
  4. Leave the path field empty (the plugin can generally determine the right location).
  5. Leave the prefix field empty, as a default theme is being added.

After this, inspect your styles.css. You should see the spartan/ui-specific variables that have been appended:

:root {
--font-sans: ''
}

:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
}

.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
Enter fullscreen mode Exit fullscreen mode

Introducing our first primitive

Excellent! Our project is now fully prepared to use spartan/ui. Let's utilize the Nx plugin once more to add the button primitive:

npx nx g @spartan-ng/cli:ui button
Enter fullscreen mode Exit fullscreen mode

When asked:

  1. Choose a suitable directory for your components, such as libs/spartan.
  2. Choose the default option (false) if you don't want to skip the installation of necessary dependencies.

The plugin will then create a new buildable library in your libs/spartan/button-helm folder.

This folder contains the source code for the HlmButtonDirective. The directive applies various styles through a HostBinding, with the style determined by its different inputs.

import { Directive, HostBinding, Input } from '@angular/core';
import { cva, VariantProps } from 'class-variance-authority';
import { hlm } from '@spartan-ng/ui-core';
import { ClassValue } from 'clsx';

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'underline-offset-4 hover:underline text-primary',
      },
      size: {
        default: 'h-10 py-2 px-4',
        sm: 'h-9 px-3 rounded-md',
        lg: 'h-11 px-8 rounded-md',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
);
type ButtonVariants = VariantProps<typeof buttonVariants>;

@Directive({
  selector: '[hlmBtn]',
  standalone: true,
})
export class HlmButtonDirective {
  private _variant: ButtonVariants['variant'] = 'default';
  @Input()
  get variant(): ButtonVariants['variant'] {
    return this._variant;
  }

  set variant(value: ButtonVariants['variant']) {
    this._variant = value;
    this._class = this.generateClasses();
  }

  private _size: ButtonVariants['size'] = 'default';
  @Input()
  get size(): ButtonVariants['size'] {
    return this._size;
  }

  set size(value: ButtonVariants['size']) {
    this._size = value;
    this._class = this.generateClasses();
  }

  private _inputs: ClassValue = '';

  @Input()
  set class(inputs: ClassValue) {
    this._inputs = inputs;
    this._class = this.generateClasses();
  }

  @HostBinding('class')
  private _class = this.generateClasses();

  private generateClasses() {
    return hlm(buttonVariants({ variant: this._variant, size: this._size }), this._inputs);
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: Currently, while the plugin adds dependencies correctly, Nx doesn't install their peer dependencies. To ensure everything is set up properly, run npm i after executing @spartan-ng/cli:ui.

Utilizing the primitive

To put the new directive to use, we simply add it to our button element within src/app/app.component/ts:

import { Component } from '@angular/core';
import { HlmButtonDirective } from '@spartan-ng/button-helm';

@Component({
  standalone: true,
  imports: [HlmButtonDirective],
  selector: 'app-root',
  template: `<button hlmBtn variant="outline">Hello from {{title}}</button>`
})
export class AppComponent {
  title = 'sparta';
}
Enter fullscreen mode Exit fullscreen mode

Next, start the development server:

npm start
Enter fullscreen mode Exit fullscreen mode

You'll then see our attractively styled spartan/ui button in action:

Spartan Button in dark gray with rounded corners and white text saying Hello from sparta

To modify its appearance to a different variant, we can add a variant input to our <button hlmBtn>:

import { Component } from '@angular/core';
import { HlmButtonDirective } from '@spartan-ng/button-helm';

@Component({
  standalone: true,
  imports: [HlmButtonDirective],
  selector: 'app-root',
  template: `<button hlmBtn variant="outline">Hello from {{title}}</button>`
})
export class AppComponent {
  title = 'sparta';
}
Enter fullscreen mode Exit fullscreen mode

Our styles adapt dynamically, and we now see the outlined button variant:

Spartan Button in light gray with rounded corners and dark text and dark border saying Hello from sparta

Beyond the basics: the full component library

The initial alpha release ships with a total of 30 components ready for you to explore:

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Button
  • Card
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Dropdown Menu
  • Input
  • Icon
  • Label
  • Menubar
  • Popover
  • Progress
  • Radio Group
  • Scroll Area
  • Separator
  • Sheet
  • Skeleton
  • Switch
  • Tabs
  • Textarea (covered by hlmInput directive)
  • Toggle
  • Typography

Adding any of these to your project follows the exact same pattern we used for the button. I’m also planning more blog posts and video walkthroughs that demonstrate building complete user interfaces with spartan/ui.

Looking ahead

spartan/ui remains in alpha, which means there’s still a long road ahead — some history enthusiasts might even call it a marathon. Still, I’m genuinely thrilled to see this project finally take off, and I’m looking forward to hearing how it works for you. Your feedback is incredibly valuable at this stage. My hope is that spartan/ui becomes the shadcn go-to for the Angular community, and that together with great projects like AnalogJs, it sparks a similar wave of innovation for all of us.

Do you have any questions or ideas for future articles? What are your impressions of spartan so far — would you consider integrating it into your own work? I’m always curious to hear your perspective, so feel free to drop a comment or reach out directly.

And if this article was helpful, don’t forget to show some support by liking and sharing it. If you’d like to follow along with more of my work, you can find me on Twitter or Github.