
After waiting so long for an Angular UI library that actually met my needs, I decided to stop waiting...
After waiting so long for an Angular UI library that actually met my needs, I decided to stop waiting and build my own. The result is Semantic Components — an open-source Angular UI library built on Tailwind CSS, Angular CDK, and Angular Aria, heavily inspired by shadcn/ui.
GitHub: https://github.com/gridatek/semantic-components
Package: @semantic-components/ui
Website: https://semantic-components.com
The Angular ecosystem has always had fewer off-the-shelf UI options compared to React. Libraries like shadcn/ui, Radix, and Headless UI have raised the bar for what a component library can be — and Angular deserves the same quality.
Semantic Components is my attempt to bring that standard to Angular, while leaning fully into what makes Angular great.
Every directive or component is named to describe its role in the interface, not just the feature it belongs to. Take the tooltip as an example. Angular Material gives you a single matTooltip directive:
<!-- Angular Material -->
<button matTooltip="Save changes">Save</button>
In Semantic Components, it's scTooltipTrigger:
<!-- Semantic Components -->
<button scTooltipTrigger="Save changes">Save</button>
scTooltipTrigger — because ScTooltip is already the component that renders the actual tooltip bubble. The directive on the button is not the tooltip — it's what triggers it. These are two different things, and the names reflect that. ScDrawerTrigger, ScSelectValue, ScSelectTrigger, ScSidebarBody — you know exactly what each piece does before reading a single line of docs.
This principle extends to the HTML elements themselves. When possible, components/directives are applied to the right native element rather than a generic <div>.
The entire UI is described in the template — no imperative open(), close(), or DialogService.create() calls. Take the dialog as an example:
<div scDialogProvider [(open)]="isOpen">
<button scDialogTrigger scButton variant="outline">Open Dialog</button>
<ng-template scDialogPortal>
<div scDialog>
<button scDialogClose>
<svg siXIcon></svg>
<span class="sr-only">Close</span>
</button>
<div scDialogHeader>
<h2 scDialogTitle>Edit profile</h2>
<p scDialogDescription>Make changes to your profile here.</p>
</div>
<!-- content -->
<div scDialogFooter>
<button scButton variant="outline" (click)="isOpen.set(false)">Cancel</button>
<button scButton type="submit">Save changes</button>
</div>
</div>
</ng-template>
</div>
readonly isOpen = signal(false);
The open state is a signal. The trigger, the portal, the close button — all declared in the template. No service injection, no imperative show/hide, no ViewContainerRef gymnastics. You read the template and immediately understand the full structure of the dialog.
The naming reinforces this. ScDialog is not a service — it's the <div role="dialog"> element itself. In Angular Material, MatDialog is a service you inject and call .open() on. Here, scDialog is the thing rendered in the DOM. Same naming principle: the name describes exactly what the piece is, not what it does behind the scenes.
There is a tradeoff: scDialog requires an extra wrapper element in the DOM scDialogProvider. It acts as the coordination point between the trigger, the portal, and the close button — sharing state through Angular's DI tree. It's a conscious choice in favor of keeping everything in the template, at the cost of one extra <div> that you may need to style or account for in your layout.
Each component is a set of small, focused pieces that you assemble yourself. There are no magic [content] inputs or hidden <ng-content> slots — you write the structure, and the pieces plug into it.
The Select is a good example of how far this goes:
<div scSelect #select="scSelect" placeholder="Select a label">
<div scSelectTrigger aria-label="Label dropdown">
<span scSelectValue>
@if (displayIcon(); as icon) {
<svg scSelectItemIcon siTagIcon></svg>
}
<span class="truncate">{{ select.displayValue() }}</span>
</span>
</div>
<ng-template scSelectPortal>
<div scSelectPopup>
<div scSelectList>
@for (item of items; track item.value) {
<div scSelectItem [value]="item.value" [label]="item.label">
<svg scSelectItemIcon siTagIcon></svg>
<span>{{ item.label }}</span>
</div>
}
</div>
</div>
</ng-template>
</div>
This also composes across components. A button can be a drawer trigger, a tooltip trigger, and an icon button all at once:
<button scButton size="icon" scDrawerTrigger scTooltipTrigger="Open menu">
<svg siMenuIcon></svg>
</button>
One element. Three responsibilities. No wrappers.
The tradeoff is verbosity. Because you own the structure, you write more template code than you would with a batteries-included component that hides everything behind inputs. That's a deliberate choice — explicit over implicit. You always know what's in the DOM because you put it there.
The library follows the shadcn/ui design system — same CSS variables, same color tokens (bg-primary, text-muted-foreground, border-input…), same default styles. If you're already familiar with shadcn, the visual language is instantly recognizable.
Styles are written in Tailwind CSS and managed with class-variance-authority. This means:
default, outline, ghost, destructive, link) across all componentsexport const buttonVariants = cva('inline-flex items-center justify-center rounded-lg border ...', {
variants: {
variant: {
default: 'bg-primary text-primary-foreground',
outline: 'border-border bg-background hover:bg-muted',
ghost: 'hover:bg-muted hover:text-foreground',
destructive: 'bg-destructive/10 text-destructive',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-8 px-2.5',
sm: 'h-7 px-2.5 text-[0.8rem]',
lg: 'h-9 px-2.5',
icon: 'size-8',
},
},
});
The rest of the library's design is guided by a few core principles:
Attribute selectors over element selectors. Instead of custom elements like <sc-button>, the library uses attribute selectors on native HTML. No extra wrapper elements, native accessibility roles preserved, and multiple components/directives can stack on the same element:
<button scButton variant="outline" scDrawerTrigger>Open</button>
Modern Angular, all the way down. Signals (input(), output(), computed()), standalone components, native control flow (@if, @for), inject(), and OnPush everywhere. Overlays and positioning are built on @angular/cdk. Accessible patterns like focus trapping and live regions use @angular/cdk/a11y and @angular/aria. Forms are signal-based. The library is also zoneless-compatible — no zone.js required. No legacy APIs, no NgModules.
@Directive({ selector: 'button[scButton]' })
export class ScButton {
readonly variant = input<ScButtonVariants['variant']>('default');
readonly size = input<ScButtonVariants['size']>('default');
readonly disabled = input<boolean, unknown>(false, { transform: booleanAttribute });
}
Accessible by default. Every component is built to pass WCAG AA minimums — proper ARIA attributes, full keyboard navigation, focus management on dialogs and drawers, and screen reader support. Where possible, this is powered by Angular CDK's accessibility primitives (@angular/cdk/a11y) and @angular/aria.
This library makes deliberate choices that prioritize the future of Angular over backwards compatibility. That means it is not for every project — and that's intentional.
ChangeDetectionStrategy.OnPush.NgModel or reactive forms.forRoot(), no compatibility shims for module-based apps.@semantic-components/ui — Core Library50+ components:
| Category | Components |
|---|---|
| Actions | Button, Button Group, Link, Toggle, Toggle Group |
| Layout | Card, Separator, Aspect Ratio, Toolbar, Scroll Area, Typography |
| Forms | Input, Textarea, Checkbox, Radio Group, Switch, Select, Native Select, Label, Field, Input Group, Slider, Range Slider |
| Overlays | Dialog, Alert Dialog, Drawer, Sheet, Popover, Hover Card, Tooltip, Toast, Backdrop |
| Navigation | Breadcrumb, Pagination, Tabs, Menu, Menu Bar, Navigation Menu |
| Display | Alert, Badge, Avatar, Skeleton, Spinner, Progress, Kbd, Empty, Item |
| Data | Table, Accordion, Collapsible, Calendar, Date Picker, Time Picker |
| File | File Upload |
@semantic-icons/lucide-iconsIcons are distributed as Angular components from @semantic-icons/lucide-icons. Every icon is a standalone component you apply to an <svg> element:
<svg siStarIcon></svg>
<svg siUserIcon></svg>
<svg siArrowRightIcon></svg>
This approach is fully tree-shakable — only the icons you import end up in your bundle. No icon fonts, no sprite sheets.
npm install @semantic-components/ui
Add the styles to your global stylesheet:
@import '@semantic-components/ui/styles';
@source "../node_modules/@semantic-components/ui";
The @import brings in the CDK overlay styles and the shadcn-compatible CSS variables (colors, radius, spacing tokens). The @source tells Tailwind v4 to scan the library's files so its utility classes are included in your build.
Then import what you need directly in your standalone component:
import { ScButton, ScDialog, ScDialogBody, ScDialogTitle } from '@semantic-components/ui';
@Component({
imports: [ScButton, ScDialog, ScDialogBody, ScDialogTitle],
template: `
<button scButton>Open Dialog</button>
`,
})
export class MyComponent {}
No module registration. No forRoot(). Just import and use.
Feedback, stars, and contributions are very welcome. If you're building Angular apps and tired of fighting your UI library, give Semantic Components a try.
gemmaI ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...
communityHey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...
ai(yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...
aiMy laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...
githubactionsI Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...
aiI've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...
Workflows from the Neura Market marketplace related to this DeepSeek resource