Create project
Start the cli and create an application that uses CSS 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 CSS 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 tailwindcss-animate @ng-icons/core @ng-icons/lucidepnpm add @angular/cdk class-variance-authority clsx tailwind-merge tailwindcss-animate @ng-icons/core @ng-icons/lucideyarn add @angular/cdk class-variance-authority clsx tailwind-merge tailwindcss-animate @ng-icons/core @ng-icons/lucidebun add @angular/cdk class-variance-authority clsx tailwind-merge tailwindcss-animate @ng-icons/core @ng-icons/lucideadd these lines inside your compilerOptions on the tsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/app/*"]
}
}
}Add the following to your styles.css file. 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);
}Create a folder called core inside your app folder
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 folder called lib inside your app folder
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 project.
{
"$schema": "https://zardui.com/schema.json",
"style": "css",
"appConfigFile": "src/app/app.config.ts",
"packageManager": "npm", // npm, pnpm, yarn or bun
"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:
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