Seven Lines of CSS for Twenty Components
Trill's Blazor WebAssembly client has twenty Razor components, thirty-seven inline style attributes, two authored class names and one stylesheet containing a single rule - and the thinness is not an oversight, it is the most precise measurement of what a teaching repository is.
You can tell how a front end was built by opening its stylesheet before its components. A design system leaves custom properties and a type ramp. A framework-first app leaves overrides. A prototype leaves almost nothing, and the “almost” is where the real decisions hid. Part 13 followed a paid placement down to a boolean rendered as a border colour. This part opens the file that draws the border, and finds it is one of two.
The stylesheet, in full
Trill.Web/src/Trill.Web.UI/wwwroot/css/app.css — the complete file, not an excerpt:
body {
background-color: #ffffff;
}
wwwroot/ contains three items in total: css/app.css, favicon.ico and index.html. There is no appsettings.json, no image directory, no font.
The second and last piece of authored CSS is inlined at the bottom of Shared/MainLayout.razor, after the @code block:
<style>
.site-layout-background {
background: #fff;
}
</style>
Two rules. Six lines of CSS, seven if you count the <style> tag that carries the second one. Against that:
- 20
.razorcomponents —App, five components (Ad,AdFull,Story,StoryFull,User), twelve routable pages,MainLayoutandNavMenu. - 0
.razor.cssfiles. The project targets .NET 5, where scoped CSS isolation is a first-class, zero-configuration feature. It is not used once. - 0 Sass or Less files, 0 custom properties, 0 theme file, 0 palette, 0 spacing scale.
- 0
@mediaqueries, anywhere in the project. - 2 authored class names in the whole application:
nav-textandsite-layout-background.
A naming convention is not violated here; it is inapplicable. With two class names there is no BEM to break, no utility system to be inconsistent with, and no specificity war to lose. Bloat is zero, and so is reuse.
Where the styling actually lives
index.html answers it in two lines:
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
Ant Design Blazor 0.3.0 — not Bootstrap, despite what a dotnet new blazorwasm project usually ships. Every visual decision is delegated to antd's defaults, and the widget vocabulary the app draws on is genuinely broad: Card, Tag, Button, Title/Paragraph/Text, Icon, Menu, Layout/Sider/Content, Row/Col, Form/FormItem, Input/InputPassword/TextArea, Select, DatePicker, Switch, Tooltip, Popconfirm, Modal, Tabs, Steps, Descriptions, Search, MessageService. For a teaching repository that wants to demonstrate architecture rather than typography, that is the right trade and I would make it too.
The deviations from antd's defaults go inline. Thirty-seven Style= / style= attributes across eleven .razor files:
| File | Inline styles |
|---|---|
Components/StoryFull.razor |
8 |
Pages/Index.razor |
7 |
Components/Ad.razor, Components/Story.razor, Pages/CreateAd.razor, Pages/Register.razor, Shared/MainLayout.razor |
3 each |
Components/AdFull.razor, Components/User.razor, Pages/Login.razor |
2 each |
Pages/Trending.razor |
1 |
And they repeat. Style="width: 95%" appears in all five components. <div style="background:#fff; padding-top:30px"> wraps four of the five identically — Ad.razor:9, Story.razor:8, StoryFull.razor:11, User.razor:10. Style="font-size: 14px; padding: 10px; margin: 10px" appears three times across two files.
That is duplication, but it is worth being precise about what kind. It is duplication of inline strings, not of CSS rules — there is no stylesheet for it to bloat. The cost is not bytes; it is that changing the card width is a five-file edit with no compiler help and no grep-safe token, and that none of the thirty-seven values can respond to a media query, a theme, or a prefers-color-scheme toggle, because inline styles are not selectors.
What the absence actually costs
Three consequences follow directly, and they are the reason this is a finding rather than a shrug.
The layout is fixed-width and taller than a laptop. MainLayout.razor:19 hardcodes <Sider Width="300"> and :54 sets min-height: 960px on the content pane. With no @media query anywhere, there is no breakpoint at which either changes.
Responsiveness is switched off at the document level. index.html:6:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
maximum-scale=1.0, user-scalable=no disables pinch zoom. That fails WCAG 1.4.4 (Resize Text) outright, and it compounds the fixed 300-pixel sidebar on a phone: the user cannot zoom out to see the content pane, and there is no breakpoint to collapse the sidebar for them.
Accessibility is effectively absent. Grepping every .razor and .html in the project for aria-, alt=, role= and lang= returns three hits — all aria-hidden on decorative icons, all inside NavMenu.razor, which is dead (see below). index.html:2 is a bare <html> with no lang attribute, so no screen reader knows what language to pronounce. And the like/dislike controls in StoryFull.razor are AntDesign <Icon OnClick=…> elements — icons with click handlers, no role="button", no tabindex, no accessible name — which makes rating a story keyboard-inaccessible. Navigation is raw <a href> nested inside <Button> in three components, which is an interactive element inside an interactive element.
None of those is a styling opinion. They are the things a stylesheet's absence takes away the place to fix.
Dead template residue
Shared/NavMenu.razor is the unmodified dotnet new blazorwasm scaffold, still in the repository:
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">Trill.Web.UI</a>
...
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
Thirty-seven lines linking to /counter and /fetchdata — routes that do not exist in this application — styled with Bootstrap classes (navbar navbar-dark, nav-link, pl-4) and Open Iconic glyphs (oi oi-home). Neither Bootstrap nor Open Iconic is referenced in index.html, so even if the component rendered it would be entirely unstyled. It is not referenced by MainLayout or by any page, so it never renders. It is also the only file in the project containing an aria- attribute, which is a small joke the repository is not in on.
The residue is not confined to markup. _Imports.razor:6 imports Microsoft.AspNetCore.Components.Web.Virtualization while Users.razor, Ads.razor, Index.razor and Trending.razor all render unbounded foreach loops and <Virtualize> appears nowhere. _Imports.razor:2 imports System.Net.Http.Json, which is also a package reference, and is unused because CustomHttpClient hand-rolls its serialisation with StringContent and JsonSerializer. Trill.Web.Core.csproj references Google.Protobuf, Grpc.Net.Client, Grpc.Net.Client.Web and Grpc.Tools — in a class library containing no .proto file and no gRPC type; all four are duplicated in Trill.Web.UI.csproj, where they are genuinely used. And AdFull.razor and User.razor both open with @using Google.Protobuf.WellKnownTypes in components that touch no protobuf type at all.
The one UX cost with the least code behind it
index.html:15 is the whole loading experience of a .NET 5 WebAssembly application:
<div id="app">Loading...</div>
No spinner, no progress bar, no skeleton, no <progress> element — all of which the .NET 5 Blazor WASM template supplies by default and which were deleted here. Behind that word, the browser is downloading the full Mono runtime and every assembly: there is no <PublishTrimmed>, no AOT, no lazy-loaded assemblies, no globalisation-data trimming, plus AntDesign 0.3.0's complete CSS and JS. The single heaviest perceived-performance cost in the application is the one with the least code behind it, and it is a deletion rather than an omission.
Two smaller formatting problems round it out, both from the same root cause of having no shared presentation layer. @Model.Amount.ToString("C") renders currency in the browser's culture for a domain with no currency at all. @Model.CreatedAt.ToString("g") renders UTC timestamps — every service writes DateTime.UtcNow — with no conversion and no suffix, so every time in the UI is wrong by the reader's offset and does not say so.
What the thinness measures
I want to resist the easy version of this article, which is “the client has no design system, therefore it is bad”. A teaching estate's client exists to prove the services are reachable, and this one does that: five typed HTTP clients mapping one-to-one onto the gateway's routes, a gRPC-Web streaming subscription, a result-type discipline that converts every failure into an ApiResponse<T> rather than throwing. The architecture is complete. What the seven lines measure is exactly which half of a production application a reference implementation is allowed to skip — and the answer, stated by the file sizes, is the entire presentation layer.
That is worth knowing in both directions. If you are reading Trill to learn microservices, none of this matters. If you are reading it as a template for something you intend to ship, the stylesheet is telling you that the design system, the responsive strategy, the accessibility pass, the loading states and the internationalisation are all still yours to write — and none of them will be visible as a gap in a code review, because there is no file where their absence shows.
Next, the retrospective, and the most honest number in the estate: seven test projects, zero tests.