Your Component Doesn’t Know What Color It Is. That’s the Whole Trick.
A component should know what it is. It should never know what it looks like.
Your Component Doesn’t Know What Color It Is. That’s the Whole Trick.
A component should know what it is. It should never know what it looks like.
Here’s an uncomfortable question. Open your last Angular component. The one with the button, or the card, or the modal. Now look at the .ts file.
Does it know it’s blue?
Does it know it has 8px of padding? Does it know its border radius is 6px and not 4px?
If the answer is yes — if your component’s logic file has any opinion at all about how it looks — I want you to sit with that for a second. Because Angular Material doesn’t do that. PrimeNG doesn’t do that. And there’s a reason every component library that survives more than one design refresh follows the same rule.
A component should know what it is. It should never know what it looks like.
That’s it. That’s the whole article, really. Everything below is just me proving it to you with code.
The Sin We All Commit
Let’s not pretend this is a strawman. Here’s what 90% of “reusable” components look like in the wild:
@Component({
template: `
<button [ngStyle]="{
'background': disabled ? '#ccc' : '#3b82f6',
'padding': size === 'sm' ? '4px 8px' : '8px 16px',
'border-radius': '6px'
}">
<ng-content></ng-content>
</button>
`
})
export class ButtonComponent {
@Input() disabled = false;
@Input() size: 'sm' | 'md' = 'md';
}
It works. Ship it, right?
Six months later, design comes back and says the brand is changing from blue to a deep teal. Now you’re grepping your entire codebase for #3b82f6. You find it in fourteen components. Three of them spelled it slightly differently. One of them is actually #3b82f7 because someone fat-fingered a hex code in 2023 and nobody noticed.
This isn’t a styling bug. It’s an architecture bug. You let presentation leak into logic, and now the two are welded together so tightly that changing one means surgery on the other.
Angular Material took one look at this problem in 2017 and said: never again.
The Rule That Changes Everything
Here it is, the one sentence worth bookmarking:
Components emit state. Stylesheets interpret state. Never let the wires cross.
A button doesn’t say “I am blue.” A button says “I am primary.” Somewhere else — far away, in a file the component has never met — something decides what primary means today.
This is the difference between a component and a vocabulary. Material and PrimeNG aren’t shipping you components. They’re shipping you a vocabulary of semantic states, plus a translator that turns that vocabulary into pixels. Swap the translator, keep the vocabulary, and the entire visual identity of your app changes without a single component file being touched.
Let’s build it.
Step 1: Logic Speaks Only in Semantics
@Component({
selector: 'ui-button',
standalone: true,
templateUrl: './button.component.html',
styleUrl: './button.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'class': 'ui-button',
'[class.ui-button--disabled]': 'disabled()',
'[attr.data-variant]': 'variant()',
'[attr.data-size]': 'size()',
'[attr.aria-disabled]': 'disabled() || null',
}
})
export class ButtonComponent {
variant = input<'primary' | 'secondary' | 'danger'>('primary');
size = input<'sm' | 'md' | 'lg'>('md');
disabled = input(false);
}
Read that host block again. There is not one color, one pixel value, one hex code anywhere in this file. Every line answers the question “what is this button,” never “how does it look.” That’s not an accident — that’s the discipline.
If you ever find yourself reaching for [ngStyle] or [style.background] inside component logic, stop. You’re about to weld the wires together. Don’t.
Step 2: Tokens — The Vocabulary Itself
This is the part most people skip, and it’s the part that actually matters. Don’t go straight from “primitive color” to “component.” Put a translator in between. Three layers, every time:
// Layer 1 — Primitives. Raw values. Nobody outside this file should see these.
:root {
--blue-500: #3b82f6;
--space-2: 0.5rem;
--space-4: 1rem;
--radius-md: 6px;
}
// Layer 2 — Semantic. What the value *means* in your design system.
:root {
--color-primary: var(--blue-500);
--color-on-primary: white;
--radius-default: var(--radius-md);
}
// Layer 3 — Component. What the value means to *this specific component*.
.ui-button {
--button-bg: var(--color-primary);
--button-color: var(--color-on-primary);
--button-radius: var(--radius-default);
}
Three layers feels like overkill until the day your design team rebrands. Then it feels like the only decision you got right all year. Change Layer 1, and Layers 2 and 3 update for free — every button, every card, every component in your library, all at once, with zero component code touched.
This is precisely what Angular Material did when it moved to Material Design 3. The components didn’t change. The token layer underneath them did.
Step 3: Style Reads State, Never the Reverse
.ui-button {
background: var(--button-bg);
color: var(--button-color);
border-radius: var(--button-radius);
transition: background 0.15s ease;
&--disabled {
background: var(--gray-300);
cursor: not-allowed;
}
&[data-variant='danger'] {
--button-bg: var(--color-danger);
}
&[data-size='sm'] {
--button-bg: var(--button-bg);
padding: var(--space-2);
}
}
Notice what’s happening in [data-variant=’danger’]. It doesn’t write new CSS properties. It reassigns the token. The component still only ever reads — button-bg. It has no idea that “danger” exists as a concept. It just knows its background is whatever the token says, and the token’s value depends on an attribute the component itself exposed — without ever interpreting what that attribute means.
That separation is the entire ballgame. Logic exposes facts. Style assigns meaning to facts. Neither one does the other’s job.
Why This Is the Difference Between a Component and a Liability
Skip this pattern, and here’s your future, guaranteed:
• Rebrand request → grep-and-replace across 40 files, pray nothing breaks.
• Dark mode request → duplicate every component with a -dark variant, double your maintenance surface.
• White-label request → fork the entire library.
Follow this pattern, and here’s the same future:
• Rebrand → edit one token file.
• Dark mode → swap the semantic layer based on a media query or a class on <html>. Done.
• White-label → ship a different token file per client. Same components, zero forks.
This is not a styling preference. It’s the difference between a system that scales and a system that accumulates technical debt every single sprint until someone proposes a rewrite.
The Three Commandments
If you remember nothing else, remember these:
-
A component’s TypeScript and template never contain a color, a size, or a spacing literal. Not one. If you typed a hex code into a .ts file, you already lost.
-
A component’s stylesheet never reads @Input() values directly. It reads the classes and attributes the component already exposed, and resolves those against tokens.
-
Tokens are layered — primitive, semantic, component — so a rebrand only ever touches the top two layers. The component layer is sacred. Don’t touch it for a color change. Ever.
Angular Material didn’t get lucky. PrimeNG didn’t get lucky. They followed these three rules with religious discipline, and it’s why both libraries have survived multiple design-language rewrites without their consumers having to rewrite a single line of application code.
Your component library can do the same thing. It just has to stop knowing what color it is.
메타데이터
- post_id
- 6d43ff099c7a
- slug
- your-component-doesnt-know-what-color-it-is-that-s-the-whole-trick-6d43ff099c7a
- url
- https://medium.com/@mamdouhibr67/your-component-doesnt-know-what-color-it-is-that-s-the-whole-trick-6d43ff099c7a
- canonical_url
- https://medium.com/@mamdouhibr67/your-component-doesnt-know-what-color-it-is-that-s-the-whole-trick-6d43ff099c7a
- author_url
- https://medium.com/@mamdouhibr67
- status
- ok
- fetched_at
- 2026-09-08 19:49:32