styling
Esposter UnoCSS Attributify Mode styling conventions — prop-based attributes for all static styles, flex not d-flex, class only for dynamic or Vuetify-specific utilities. Apply when writing .vue or .s
SKILL.md.claude/skills/styling/SKILL.md
GitHubStyling — UnoCSS Attributify Mode
Settled — do not re-propose
- A check for an attribute on a component that generates no rule — it is as likely one of the component's props as a misspelt utility, and telling the two apart needs the component's prop list no template extraction has; a native element is
apps/web/app/templates.test.ts, and every inert family met so far is the blocklist (theunocssskill,references/blocklist.md). - A rule for theme primitive vs bespoke colour — needs the palette in mind and a judgement about intent.
- A rule for a fixed dimension on a layout region — what a region is, is the judgement.
Deep Dives
references/layout.md— when laying out a page, panel, sidebar or column split, sizing a region, drawing a border or finding one you did not ask for, or building a row that reads as one sentence.references/utility-vocabulary.md— when two spellings say the same thing: an abbreviation, a named step against a numeric one, a directional gap, or a slash value.references/images.md— when adding or sizing an image.references/theme-utilities.md— when reaching for a type size, a token or palette colour, a named opacity, or muted text.references/style-blocks.md— when a component genuinely needs a<style>block, or a scoped rule or:deep()does not apply.references/arbitrary-values.md— when a utility needs an arbitrary[...]value:calc(), a CSS variable, a transition, or!important.references/css-custom-properties.md— when a<style>block needs a shared value, and a SASS variable looks like the way to reach it.references/lengths.md— when writing an authored length, or sizing an empty element on a flex axis.references/class-attribute.md— when a style seems to needclassor a scoped rule, or a utility is switched on a condition.references/links.md— when styling a link or inline text that runs an action.references/state-variants.md— when a colour changes with state, or a shaded cell also takes a tint.references/spacing.md— when spacing siblings or reaching for a margin.
Core Rules
- Prop-based styling for ALL static styles:
<div text-red p-4>. Where UnoCSS attributes sit relative to component props is the template attribute order in.agents/skills/vue/references/ordering.md. sizeattribute (orwidth/heightprops) instead ofw-<n>/h-<n>where possible. They are authored lengths, so they takerem—size="4rem".- Prefer simple named utilities over arbitrary values. Avoid arbitrary shadows, gradients, dimensions, border widths, and z-index unless the layout needs them. Don't add z-index defensively; rely on DOM order and positioning first.
- Prefer theme primitives over bespoke styling: the library's surfaces (
ui-frame,ui-card) and type for card/panel/surface backgrounds (.agents/skills/ui-library/references/surfaces.md); token colours (bg-background,b-border,text-accent,text-muted) before custom colours. A surface is a library surface, neverbg-panelon a<div>, and no page paints its own background (references/layout.md). - Avoid arbitrary hex/RGB/RGBA, custom shadows, and one-off background/border colours in app UI. A colour a template needs is a token, and one the tokens lack is a new token rather than a raw value (
.agents/skills/ui-library/references/tokens.md). - Never hardcode a fixed dimension to lay out a region (sidebar/panel/column split) —
references/layout.md. Arbitrary dimensions are a last resort for true format constraints (aspect-video, viewport-safe containers, canvas/game surfaces, third-party embeds); first check whether the component hierarchy or flex/grid structure is wrong. - An empty element sized along a flex axis takes
shrink-0(references/lengths.md). - Always
rem, neverpx, for every authored length;pxsurvives only where the unit is not ours to choose (references/lengths.md). - A viewport height is
dvh, nevervhorh-screen— a mobile browser's toolbar comes and goes overvh.BLOCKED_SPELLINGSrefuses both, so avhvalue generates no CSS at all rather than a wrong height (.agents/skills/unocss/references/blocklist.md). field-sizing: contentis an attributify utility — putfield-sizing-contentdirectly on the<input>/<textarea>, never in a scoped class.
The look is the app's, on every page
An immersive page (apps/web/app/layouts/immersive.vue) is one with no app frame, and nothing more: it brings its own way back, not a look of its own. The look is the UI library's tokens, which every page reads whichever library draws it — a colour on any page is a token, and a page that sets its own palette on its root is a finding (.agents/skills/ui-library/references/document-chrome.md). The agent console stays in dusk through a theme scope rather than a palette of its own (openspec/specs/architecture/ui-library/design.md). Every rule on this page holds inside an immersive page as on any other.
What stays in class="..."
Only a scoped CSS ref, a :class binding (the home of a valueless utility switched on a condition), a third-party or SVG class and group — everything else is an attribute (references/class-attribute.md).
Theme utilities are attributify too — references/theme-utilities.md
presetAttributify() is active, so every token colour, named opacity and palette colour is a standalone attribute; which names generate a utility at all, and the opacity spelling, are that page.
Links use text-info (the blue), never text-accent
A link is text-info with hover:underline; inline text that runs an action is a native <button type="button"> in the link colour (references/links.md).
State variants are utilities, not &:hover blocks
A state colour is a variant utility (hover:text-text), never an &:hover block, and a hover or selected background is the library's tint, never a picked surface (references/state-variants.md).
Utility vocabulary — references/utility-vocabulary.md
Where two spellings say the same thing, one is the repo's. Choosing between an abbreviation and its long form, a named step and a numeric one, a directional gap, or a slash value against a bare one is that page.
Images Are <NuxtImg> — references/images.md
A raw <img> is a vue/no-restricted-html-elements error. Read the page when adding or sizing one: width/height are html attributes rather than styles, sizing is CSS utilities, and object-contain/object-cover is stated wherever both dimensions are constrained.
The Parent Owns Spacing
Space between siblings is the container's gap-*, space inside a boundary its padding; a reset, a negative margin or one margin repeated across siblings means the space is in the wrong place (references/spacing.md).
Absolute Positioning Within a Container
Use relative on the parent and absolute top-0 right-0 (or other corners) to pin UI elements. Prefer this over manual margin/padding tricks when an element should float independent of sibling flow.
Style Block — references/style-blocks.md
<style scoped> always (vue/enforce-style-attribute), plain CSS with native nesting and no lang, library CSS imported in script setup. A Fragment-rooted child defeats scoped and :deep() alike, silently — that page.