Create project
Start the cli and create an application that uses Tailwind as default styling. Since Tailwind is the core of the project, we do not recommend using other pre-processors.
ng new my-app --style=tailwindStart the cli and create an application that uses Tailwind as default styling. Since Tailwind is the core of the project, we do not recommend using other pre-processors.
ng new my-app --style=tailwindAdd the following dependencies to your project:
npm install @angular/cdk class-variance-authority clsx tailwind-merge @ng-icons/core @ng-icons/lucide
npm install -D tailwindcss @tailwindcss/postcss postcss tailwindcss-animatepnpm add @angular/cdk class-variance-authority clsx tailwind-merge @ng-icons/core @ng-icons/lucide
pnpm add -D tailwindcss @tailwindcss/postcss postcss tailwindcss-animateyarn add @angular/cdk class-variance-authority clsx tailwind-merge @ng-icons/core @ng-icons/lucide
yarn add -D tailwindcss @tailwindcss/postcss postcss tailwindcss-animatebun add @angular/cdk class-variance-authority clsx tailwind-merge @ng-icons/core @ng-icons/lucide
bun add -D tailwindcss @tailwindcss/postcss postcss tailwindcss-animateCreate a .postcssrc.json at the root of your project. Projects created with --style=tailwind already have it.
{
"plugins": {
"@tailwindcss/postcss": {}
}
}Add these lines inside compilerOptions on your tsconfig.json. Do not add baseUrl, it is an error from TypeScript 6 on.
{
"compilerOptions": {
"paths": {
"@/*": ["./src/app/*"]
}
}
}Add the following to src/styles.css. You can learn more about using CSS variables for theming in the theming section.
@layer ng-icon, theme, base, components, utilities;
@import 'tailwindcss';
@plugin 'tailwindcss-animate';
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background: var(--muted-foreground);
border-radius: 5px;
}
::-webkit-scrollbar-track {
border-radius: 5px;
background: var(--muted);
}Add provideZard() to the providers of your src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideZard } from '@/shared/core/provider/providezard';
export const appConfig: ApplicationConfig = {
providers: [
provideZard(),
],
};Create a core folder at src/app/shared/core
import {
Directive,
type EmbeddedViewRef,
inject,
input,
type OnDestroy,
TemplateRef,
ViewContainerRef,
effect,
type EffectRef,
} from '@angular/core';
export function isTemplateRef<C = unknown>(value: unknown): value is TemplateRef<C> {
return value instanceof TemplateRef;
}
export interface ZardStringTemplateOutletContext {
$implicit: unknown;
[key: string]: unknown;
}
@Directive({
selector: '[zStringTemplateOutlet]',
exportAs: 'zStringTemplateOutlet',
})
export class ZardStringTemplateOutletDirective<T = unknown> implements OnDestroy {
private readonly viewContainer = inject(ViewContainerRef);
private readonly templateRef = inject(TemplateRef<void>);
private embeddedViewRef: EmbeddedViewRef<ZardStringTemplateOutletContext> | null = null;
private readonly context = {} as ZardStringTemplateOutletContext;
#isFirstChange = true;
#lastOutletWasTemplate = false;
#lastTemplateRef: TemplateRef<void> | null = null;
#lastContext?: ZardStringTemplateOutletContext;
readonly zStringTemplateOutletContext = input<ZardStringTemplateOutletContext | undefined>(undefined);
readonly zStringTemplateOutlet = input.required<T | TemplateRef<void>>();
#hasContextShapeChanged(context: ZardStringTemplateOutletContext | undefined): boolean {
if (!context) {
return false;
}
const prevCtxKeys = Object.keys(this.#lastContext || {});
const currCtxKeys = Object.keys(context || {});
if (prevCtxKeys.length === currCtxKeys.length) {
for (const propName of currCtxKeys) {
if (!prevCtxKeys.includes(propName)) {
return true;
}
}
return false;
} else {
return true;
}
}
#shouldViewBeRecreated(
stringTemplateOutlet: TemplateRef<void> | T,
stringTemplateOutletContext: ZardStringTemplateOutletContext | undefined,
): boolean {
const isTemplate = isTemplateRef(stringTemplateOutlet);
const shouldOutletRecreate =
this.#isFirstChange ||
isTemplate !== this.#lastOutletWasTemplate ||
(isTemplate && stringTemplateOutlet !== this.#lastTemplateRef);
const shouldContextRecreate = this.#hasContextShapeChanged(stringTemplateOutletContext);
return shouldContextRecreate || shouldOutletRecreate;
}
#updateTrackingState(
stringTemplateOutlet: TemplateRef<void> | T,
stringTemplateOutletContext: ZardStringTemplateOutletContext | undefined,
): void {
const isTemplate = isTemplateRef(stringTemplateOutlet);
if (this.#isFirstChange && !isTemplate) {
this.#isFirstChange = false;
}
if (stringTemplateOutletContext !== undefined) {
this.#lastContext = stringTemplateOutletContext;
}
this.#lastOutletWasTemplate = isTemplate;
this.#lastTemplateRef = isTemplate ? stringTemplateOutlet : null;
}
readonly #viewEffect: EffectRef = effect(() => {
const stringTemplateOutlet = this.zStringTemplateOutlet();
const stringTemplateOutletContext = this.zStringTemplateOutletContext();
if (!this.#isFirstChange && isTemplateRef(stringTemplateOutlet)) {
this.#isFirstChange = true;
}
if (!isTemplateRef(stringTemplateOutlet)) {
this.context['$implicit'] = stringTemplateOutlet as T;
}
const recreateView = this.#shouldViewBeRecreated(stringTemplateOutlet, stringTemplateOutletContext);
this.#updateTrackingState(stringTemplateOutlet, stringTemplateOutletContext);
if (recreateView) {
this.#recreateView(
stringTemplateOutlet as TemplateRef<ZardStringTemplateOutletContext>,
stringTemplateOutletContext,
);
} else {
this.#updateContext(stringTemplateOutlet, stringTemplateOutletContext);
}
});
#recreateView(
outlet: TemplateRef<ZardStringTemplateOutletContext>,
context: ZardStringTemplateOutletContext | undefined,
): void {
this.viewContainer.clear();
if (isTemplateRef(outlet)) {
this.embeddedViewRef = this.viewContainer.createEmbeddedView(outlet, context);
} else {
this.embeddedViewRef = this.viewContainer.createEmbeddedView(this.templateRef, this.context);
}
}
#updateContext(outlet: TemplateRef<void> | T, context: ZardStringTemplateOutletContext | undefined): void {
const newCtx = isTemplateRef(outlet) ? context : this.context;
let oldCtx = this.embeddedViewRef?.context;
if (!oldCtx) {
oldCtx = newCtx;
} else if (newCtx && typeof newCtx === 'object') {
for (const propName of Object.keys(newCtx)) {
oldCtx[propName] = newCtx[propName];
}
}
this.#lastContext = oldCtx;
}
static ngTemplateContextGuard<T>(
_dir: ZardStringTemplateOutletDirective<T>,
_ctx: unknown,
): _ctx is ZardStringTemplateOutletContext {
return true;
}
ngOnDestroy(): void {
this.#viewEffect.destroy();
this.viewContainer.clear();
this.embeddedViewRef = null;
}
}import type { ListenerOptions } from '@angular/core';
import { EventManagerPlugin } from '@angular/platform-browser';
export class ZardDebounceEventManagerPlugin extends EventManagerPlugin {
override supports(eventName: string): boolean {
return /\.debounce(?:\.|$)/.test(eventName);
}
override addEventListener(
element: HTMLElement,
eventName: string,
handler: (event: Event) => void,
options?: ListenerOptions,
// eslint-disable-next-line
): Function {
// Expected format: "event.debounce.delay" (e.g., "input.debounce.150")
// If delay is omitted or invalid, defaults to 300ms
const [event, , delay] = eventName.split('.');
const parsedDelay = Number.parseInt(delay);
const resolvedDelay = Number.isNaN(parsedDelay) ? 300 : parsedDelay;
let timeoutId!: ReturnType<typeof setTimeout>;
const listener = (event: Event) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => handler(event), resolvedDelay);
};
const unsubscribe = this.manager.addEventListener(element, event, listener, options);
return () => {
clearTimeout(timeoutId);
unsubscribe();
};
}
}import type { ListenerOptions } from '@angular/core';
import { EventManagerPlugin } from '@angular/platform-browser';
/**
* Angular EventManagerPlugin that provides event modifier syntax for templates.
*
* Supports modifiers: .prevent, .stop, .stop-immediate, .prevent-with-stop
* Supports key filters: enter, escape, {enter,space}
*
* @example
* Prevent default on any click
* (click.prevent)="handler()"
*
* @example
* Prevent default only on Enter key
* (keydown.enter.prevent)="handler()"
*
* @example
* Prevent default on more keys like Enter and Space key
* (keydown.{enter,space}.prevent)="handler()"
*
* @example
* Stop propagation
* (click.stop)="handler()"
*/
export class ZardEventManagerPlugin extends EventManagerPlugin {
#keywords = ['prevent', 'stop', 'stop-immediate', 'prevent-with-stop'];
override supports(eventName: string): boolean {
return this.#keywords.some(keyword => eventName.endsWith(`.${keyword}`));
}
override addEventListener(
element: HTMLElement,
eventName: string,
handler: (event: Event) => void,
options?: ListenerOptions,
// eslint-disable-next-line
): Function {
const { event, keyword, keys } = this.#provideEventFrom(eventName, this.#keywords);
return this.manager.addEventListener(
element,
event,
(event: Event) => {
const isKeyboardEvent = event instanceof KeyboardEvent;
const isElementDisabled = element.getAttribute('aria-disabled') === 'true';
const shouldApplyModifier =
(!keys.length || (isKeyboardEvent && keys.includes(event.key.toLowerCase()))) && !isElementDisabled;
if (shouldApplyModifier) {
switch (keyword) {
case 'stop':
event.stopPropagation();
break;
case 'stop-immediate':
event.stopImmediatePropagation();
break;
case 'prevent-with-stop':
event.preventDefault();
event.stopPropagation();
break;
default:
event.preventDefault();
break;
}
}
handler(event);
},
options,
);
}
#provideEventFrom(eventName: string, keywords: string[]): { event: string; keyword: string; keys: string[] } {
const eventNameSubstrings = eventName.split('.');
let event = '';
let keys: string[] = [];
let keyword = '';
for (const substring of eventNameSubstrings) {
if (substring.startsWith('{')) {
keys = this.#extractKeys(substring);
continue;
} else if (keywords.includes(substring)) {
keyword = substring;
break;
} else if (!event) {
event = substring;
} else {
event += `.${substring}`;
}
}
return { event, keyword, keys };
}
#extractKeys(substring: string): string[] {
const stringList = substring.substring(1, substring.length - 1);
return stringList
.split(',')
.map(raw => {
const s = raw.toLowerCase().trim();
return s === 'space' ? ' ' : s;
})
.filter(Boolean);
}
}import { makeEnvironmentProviders, type EnvironmentProviders } from '@angular/core';
import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser';
import { ZardDebounceEventManagerPlugin } from './event-manager-plugins/zard-debounce-event-manager-plugin';
import { ZardEventManagerPlugin } from './event-manager-plugins/zard-event-manager-plugin';
export function provideZard(): EnvironmentProviders {
const eventManagerPlugins = [
{
provide: EVENT_MANAGER_PLUGINS,
useClass: ZardEventManagerPlugin,
multi: true,
},
{
provide: EVENT_MANAGER_PLUGINS,
useClass: ZardDebounceEventManagerPlugin,
multi: true,
},
];
return makeEnvironmentProviders([...eventManagerPlugins]);
}Create a utils folder at src/app/shared/utils
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export type { ClassValue };
export function mergeClasses(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function transform(value: boolean | string): boolean {
return typeof value === 'string' ? value === '' : value;
}
export function generateId(prefix = ''): string {
const id = crypto.randomUUID();
return prefix ? `${prefix}-${id}` : id;
}
export const noopFn = () => void 0;
export const isElementContentTruncated = (element: HTMLElement | undefined): boolean => {
if (!element) {
return false;
}
const range = document.createRange();
range.selectNodeContents(element);
const rangeWidth = range.getBoundingClientRect().width;
const elementWidth = element.getBoundingClientRect().width;
return rangeWidth > elementWidth;
};function clamp(value: number, [min, max]: [number, number]): number {
return Math.min(max, Math.max(min, value));
}
function roundToStep(value: number, min: number, step: number): number {
return Math.round((value - min) / step) * step + min;
}
function convertValueToPercentage(value: number, min: number, max: number): number {
return ((value - min) / (max - min)) * 100;
}
export { clamp, roundToStep, convertValueToPercentage };Create a components.json file in the root of your workspace.
{
"$schema": "https://zardui.com/schema.json",
"style": "css",
"icons": "lucide",
"rtl": false,
"projectType": "angular",
"appConfigFile": "src/app/app.config.ts",
"packageManager": "npm",
"tailwind": {
"css": "src/styles.css",
"baseColor": "neutral"
},
"baseUrl": "src/app",
"aliases": {
"components": "@/shared/components",
"utils": "@/shared/utils",
"core": "@/shared/core",
"services": "@/shared/services"
}
}You can now start adding components to your project.
Start the cli and create an application that uses Tailwind as default styling. Since Tailwind is the core of the project, we do not recommend using other pre-processors.
ng new my-app --style=tailwindPrepare your entire project using the zard/ui cli. Pick "Angular" on the first question:
npx zard-cli@latest initpnpm dlx zard-cli@latest inityarn zard-cli@latest initbunx zard-cli@latest initYou can now start adding components to your project. Open the components page and select what component you want to install