Skip to content
kc@kumarChandrachooda.com:~$ cd /design-system/core && read --section="01 identity" 0%
KC Design System — Classic / Flow

Flow with it.
One system, every component.

The classic skin: Inter typography, ink-black 2px borders and the periwinkle accent, expressed as CSS custom properties in design-system.css. The resume and the blog are both built from these parts.

Reading in Flow — classic / flow Browse all 8 →

01

Identity details

The accent line and kicker reproduce the rotated section labels of the slideshow; the notched label chip comes from the “Hello” tab on the about page.
Section kicker
Small kicker
Hello
Featured
02

Buttons & chips

The solid button wipes to grey with a pop, like the original btn-black; the accent is the primary/brand button and hovers the same way — light wipe, ink text; the ghost outline skew-fills ink, mirroring salv-down-cv-btn.
azure
dotnet
outline
The busy button — the full-screen loader's DNA at 7px. These three are the nine banter boxes that were the page loader, kept alive at the smallest scale the system has. is-busy drops the hover wipe and the press bounce (both of them read as “your click landed”, which is the wrong thing to say while the click is still being honoured) and turns pointer events off; the label stays put, because swapping it for “Loading…” makes the button change width mid-press. The dots take currentColor, so they are white on the solid button and ink on the ghost with no variant rule to keep in sync. This is the state loadMore() applies to the load-more link while a page is in flight.
03

Tabs

One strip, three modes (_Tabs + .ds-tabs). The geometry is shared — the 2px rule, the accent tint and rule marking the selected tab (the rail's own current-row grammar turned horizontal), square corners because a radius meeting the rule leaves a notch, a scrolling strip with edge fades when the tabs outgrow the column — and the mode decides the element and the ARIA. --nav is anchors over real routes with aria-current (the group strip above navigates this very showcase); --panel ships as anchors and is promoted to the full tablist pattern by tabs.js once its panels are confirmed present; --display is a closed segmented radiogroup for choices about the reader's eyes rather than the content — the blog's Cards/List switch. One tab stop per strip: focus roves with the arrow keys, and selection is manual where it is expensive (a navigation) and automatic where it is free (a class toggle).
--nav: anchors over real routes, aria-current="page" on the one you are on, a count in mono beside each label. The selected tab is framed in ink on its open sides with the accent taking over the rule beneath; when the strip overflows, edge fades carry ‹ › arrows that scroll it a screenful at a time. This specimen is scenery — the live strip is at the top of this page, and each of its tabs is a document of its own.
--panel: live. The panels are siblings in this document, so this is the one mode that earns the full role="tablist" pattern — tabs.js promotes the anchors once it has confirmed every panel is present. Tab into the strip, move focus with ← → Home End, select with Enter or Space: activation is manual, because selecting swaps a panel and writes history, and arrowing across the strip should not do either seven times. The grip leading the strip is the end of its rule — drag it right to stand the strip up, left or down to lay it flat, or just press it. Standing, the accent rule moves to the left edge, the panels render next to the strip as a column, the tablist announces aria-orientation="vertical", and the choice persists for the reader in localStorage. There is no symbol for the change: a mark that stands for a change of layout is a verb, and both drawings of one (⇄ and its rotation) turn out to be somebody else's noun. The grip's strokes stay perpendicular to the rule they cap, so the mark reports the orientation without moving, and it holds the same top-left corner whichever way the strip is lying. The announcement is one fixed label with the state on aria-pressed — "Stack tabs, toggle button, pressed" — and below 992px, where there is no room for a column beside a body, the grip is greyed and inert.

Navigate selects between different documents. The state is the route, the element is an anchor, and the back button works because the browser is doing the navigating.

--display: live. A closed segmented object — 2px ink all round, dividers between segments, ink fill on the checked one — because a rendering preference is a setting, not a set of places: it is a radiogroup, a tablist here would promise a panel that does not exist, and the address bar is never touched. Arrows select immediately (the switch is one class toggle, so the APG default is free). The real one drives the blog archive's Cards/List choice and keeps it in localStorage.
View
05

Page hero & statline

Every listing page opens the same way: kicker, an oversized black title, a light lead and the mono .ds-statline counters whose pulsing dot marks the live count. Pages compose the pieces per surface — a breadcrumb above, chips or search below — so the hero is a pattern, not a partial.
The listing-page opener: kicker, title, lead and the mono statline with its pulsing live-count dot. Each page assembles the pieces directly — the series index adds a breadcrumb above, the archive adds search below — which is why this is a pattern and not a partial.
The Build Log

Blog

Notes on cloud & data architecture, .NET engineering and the practice of shipping software, written from the field, with the code to back it up.

  • 4 topics
  • 3 series
  • 18 articles
The section head is the smaller sibling: a title on the left, something quiet on the right, over a hairline. It opens a section inside a page rather than the page. The pair is the component — a heading with nothing on its right is just a heading — and what goes on the right is a fact about the list below it, never a metric about the person reading.

Everything you have touched

6 of 21

Series you are part of

yours is the one with the accent edge
06

Form fields

The layer this system did not have. Before it there was exactly one input in the 48 sections there were at the time — .ds-input, defined in a page layer for the archive's search box — and no label, select, textarea, fieldset, hint, error, disabled treatment or validation summary anywhere. The Workshop did not ignore the system; it fell through the hole and wrote nine classes of its own, which is what any app needing a form would have done. Deliberately small: a field, its label, its hint, its error, three control shapes, a grouped choice, a summary and an actions row. No date pickers, no autocompletes, no wizards — those are components with behaviour, and behaviour belongs to whichever app can name why it needs it.
The layer this system did not have. Before it there was exactly one input in the 48 sections there were at the time — .ds-input, defined in a page layer for the archive’s search box — and no label, select, textarea, fieldset, hint, error, disabled treatment or validation summary anywhere. The Workshop did not ignore the system; it fell through the hole and wrote nine classes of its own. The control room is the named second consumer.

It is the card, the meta description and the OG tag. 61/220

Chosen from what exists — a new one is a request, not a text box.

What the system owns, and the distinction that took two components rather than one. A disabled input says “you could edit this if you had the right”. A derived value has no right to grant — nobody edits a slug the system computes — so it is not an input at all. Disabled is greyed rather than hidden, because hiding a control teaches that the rule does not exist and greying it teaches that it does.

slug — derived ⚠ outbox-without-a-scheduler frozen at first submission

That category does not exist.

A grouped choice and a summary. The system had neither. A fieldset because that is what a set of related controls is — the legend names the question, and a screen reader announces it with every option rather than once at the top.
when would you like this out?

A suggestion — the owner still sets the date. Nothing here writes scheduled or published; those two words appear on this control and nowhere else in an author’s half of the app.

Not ready to submit:

  • It needs a category.
  • The body is under the floor of 200 words.

Greying the button out is a courtesy. The endpoint re-checks every one of these before it does anything.

08

Badges & pills

The status vocabulary in one place: notched sheet labels, the “coming soon” pills, the draft-preview chip, date pills, the colour-coded role pills from the project cards and the tech chips. Grey consistently means “not yet”; periwinkle means live, featured or in focus.
Sheet labels: .ds-label with its notched-tab fold. Base ink for identity, accent for a live series, grey for one still in drafts — grey consistently means “not yet”.
Hello
Series · 4 parts
Coming soon · 2 parts
Status pills: the roadmap vocabulary. Two different components say “not yet” in two places, and they are kept together here so the difference between them stays a decision rather than an accident. The third used to be .ds-draft-badge; it is now a member of the lifecycle scale below.
Coming soon
Coming soon
Lifecycle states: where a piece of content is in its life. .ds-state plus one tone, and the tones are the scale — --draft is the only one with no signal colour, the two that are waiting on a person are dashed as well as coloured, and --published is the only filled one because it is the only terminal one. Read as a column at the right-hand end of list rows, which is why the type is mono: a shape the eye matches down a list rather than a word it reads five times.
Draft
In review
Changes requested
Scheduled
Published
Preview
Date pills: one on the employment rail, one on certification cards, and a muted variant for a certification still being prepared.
July 2021 – Aug 2022
July 2021
In preparation
Role pills: .ds-project__role--* colour-codes the project cards, so the filter row doubles as a legend and the reader never has to learn the mapping twice.
Architect
Lead
Developer
R&D
Tech chips: the key stack on a project card. Uncoloured on purpose — the role pill is the one carrying meaning in that row.
C#
Azure Functions
Terraform
09

Loading & empty states

Three answers to one question — what is on screen while there is nothing to show. The cell wave (.ds-cellwave) for a fetch whose shape is not known; the skeleton (.ds-skeleton) for one whose shape is known exactly, the card emptied and wearing its own class list so it cannot drift from it; and the quiet placeholder (_EmptyState) for when the answer turns out to be none. Seventeen skeleton shapes ship, each documented beside the card it empties.
A skeleton is the card, emptied. It wears the component's own class list plus .ds-skeleton, keeps every structural wrapper, and swaps only the leaves that held text or an image for .ds-skel blocks. Nothing about the box is restated — border, radius, padding, gaps and the media ratio all come from the one definition that already exists, which is why a skeleton cannot drift from its card. That guarantees the part that matters: the grid does not reflow when content lands, because the column tracks and the media ratios are identical before and after. Height is an estimate, not a promise — the line counts stand in for a typical card, and each bar plus its stack gap is sized to equal the line-height it replaces, so a title that runs long still grows the card by a line. That is the honest limit of the technique. .ds-skeleton also switches off every transition and animation the borrowed classes bring with them: a card that lifts on hover or draws its border in has nothing to respond to yet. Widths are the exception and are set inline — the width is the content, and content is the one thing a skeleton cannot inherit.
Roots are never <a>, <button> or <details>, even where the real component's is: pointer events are off but the tab order is not, and a focusable aria-hidden element is a WCAG 4.1.2 failure. Call one with <partial name="_Skeleton" model="card" />.
ScaleHookUse it when
Page.ds-loader--counterThe site is booting. Once per session, never under reduced motion — see Page entrance, in Chrome.
Page.ds-loader--stampSame, where the monogram says it better than a number.
Region.ds-skeletonContent is coming and you already know its shape. Each of the seventeen is documented beside the card it empties.
Inline.ds-cellwaveContent is coming and you do not.
Control.ds-btn.is-busyA press is being honoured.
Below the page scale, all of them are for waits the reader started, so they appear at once rather than after a beat — the beat belongs to the caller, which should hold off ~300ms before showing anything so a fast response never flashes a placeholder. Every looping animation resolves to a legible static frame under prefers-reduced-motion rather than freezing mid-cycle, and the page loader is skipped entirely.
The cell wave — the segmented meter again, this time with nowhere to get to: six bordered cells passing a periwinkle pulse down the line. It is the loader for a fetch whose shape is not known in advance, where a skeleton would be a promise you cannot keep — a carousel page, a drawer list, a filter re-query. Where the shape is known, use the skeleton shelf instead; it says more.
Loading more
Seventeen shapes ship. Every card in the system has one, and each is documented in the section for the component it empties — the claim "every card has one" is the only thing that needs saying in one place. Render any of them by name with <partial name="_Skeleton" model="card" />.
flagshipcardpost-rowtopicseries-cardseries-itemseries-mappagenavauthorrelatedcontinueprojectcertfeatureedunotecardskill-group
And when nothing arrives. The empty state is the same decision one step later, which is why it is documented here rather than in a section of its own: a placeholder that never resolves into content is a wait that lied, and the two have to be designed against each other. The real _EmptyState partial in both its forms, over mocked models.
Message only: how a topic page reads when everything under it is still in drafts.

Nothing published under this topic yet.

Message, hint & link: the archive's no-results form, with a way back out of the dead end.

Nothing matched “owl-carousel”.

Try a different search or filter.

Clear search and filters
10

Drawer

An edge-anchored overlay panel (.ds-drawer) that slides in over a scrim, right by default and --left on request, in three widths. Square corners — it is a surface, and the 2px edge frames it. The markup is one <aside> plus a trigger; the dialog role, focus trap, Escape, scrim and scroll lock come from the behaviour layer. Its rows are generic on purpose: the navigator behind the tab on the right of this page is the same component that will hold saved-for-later articles.
Anatomy. A head (title, optional sub, close), an optional tools strip, a scrolling body and an optional foot that stays put. The trigger points at the panel by id — data-ds-drawer-open="…" — and everything else (dialog role, scrim, focus trap, Escape, scroll lock) is applied by the behaviour layer.
Rows are deliberately generic. .ds-drawer-item is an index, a label and a meta column, grouped under .ds-drawer__group. That row reads as a section link in the navigator on this page, and as a saved article when the drawer becomes the read-later shelf — same component, no fork.
12

Toast

The third overlay, and the only one nobody opens (.ds-toast): it arrives on its own and leaves on its own, so there is no panel on the page and no id to point at — the host is built on first use and a caller supplies only the message. The primary skin is the fuse: a bordered card at the bottom right whose accent underline drains left-to-right as the dwell runs out, the signature wipe run in reverse as a countdown, paused by hover or focus along with the timer behind it. The secondary is the dock chip (--dock) — the dock's position and skin, a mono kind-tag and an action word, springing up from the bottom centre with the one overshoot the system allows. Cards carry a payload; chips carry a verb. One at a time per host, always dismissible, role="alert" for failures and role="status" for everything else, entrance and exit frozen under reduced motion — the fuse is not, because it is a readout rather than decoration.
The fuse — the primary. A white bordered card at the bottom right whose bottom edge carries the accent underline, draining left-to-right as the dwell runs out: the signature wipe, run in reverse as a countdown. You can see how long the message has left, and hovering or tabbing into it pauses the drain — the bar and the timer behind it are held together, so the underline never lies about the time left. It is the workhorse because it carries a payload: a title, and under it the filename, slug or size the message is about.
A dwell of zero changes what the card has to carry. There is no countdown to draw, so the fuse goes — and with it both the thing that was colouring the kind and the only thing on the card that moved. So the border takes the kind, and spin gives the tag a pulsing dot for a message that is waiting on something rather than stuck. Fire the last two above and watch the edge, not the underline.
The dock chip — the secondary. A dark chip springing up from the bottom centre with a mono kind-tag and an action word in violet: the dock's position and the dock's skin, and the one moment of overshoot-and-settle the system allows. It is for one-line state that comes with a verb rather than a payload — turn off, undo, view. It carries no fuse: a chip you are meant to act on should not visibly be running out, so it dwells longer and leans on the verb instead.
The two hosts are independent, so a chip stating a mode and a card confirming an action can sit on screen together — they are saying different kinds of thing. Within a host it is one at a time: a second message supersedes the first rather than stacking under it, so a burst never turns the corner into a feed. Fire both above and watch it happen.
Anatomy, held still. The same markup the behaviour layer builds, lifted out of its fixed host with the fuse stopped three-quarters through.
download

CV download started

KUMAR_CHANDRACHOODA_Resume.pdf · 412 KB

draft

Preview mode is on — drafts are visible

Raising one. There is no panel on the page and no id to point at: the host is built on first use, so a caller only supplies the message. Either window.dsToast(…) from script, or the declarative trigger every button on this page uses — which is what the CV download and the copy-link buttons will carry.
<button data-ds-toast="CV download started"
        data-ds-toast-msg="KUMAR_CHANDRACHOODA_Resume.pdf &middot; 412 KB"
        data-ds-toast-kind="success">Download</button>

dsToast({ title: 'Filter cleared', variant: 'dock', tag: 'archive',
          action: 'undo', onAction: restore });
OptionDefaultWhat it does
variantfusedock for the chip.
kindinfosuccess, warning, danger — recolours the border, the fuse and the tag, and switches the live region from polite to assertive.
dwell3500 / 6000ms on screen; 0 stays until dismissed, and drops the fuse with it.
spinfalsePulses the tag. For a sticky message that is waiting on something — a poll, a write in flight.
action—The verb. onAction runs, then it dismisses unless the handler returns false.
tagthe kind, on a chipThe mono kind-tag. Set uppercase by the skin, so pass it lowercase.
message—A string, or a list of parts for mixed type: ['Reading ', { code: '/deck/state' }, ' until…']. Built out of nodes, never parsed as markup.
Accessibility and motion are not the caller's problem: a failure gets role="alert" and interrupts, everything else gets role="status" and waits its turn, every toast is dismissible, and under prefers-reduced-motion the slide and the spring are frozen to a fade. The fuse is not frozen with them — it is the dwell readout rather than decoration, and hiding how long a message has left serves a reader who asked for less motion worse, not better.

Markup

As rendered on the page behind this — captured, not transcribed.