sillyspace/docs/skins.md
2026-06-03 22:59:03 -04:00

18 KiB

Profile Skins

Profile skins customize Bliish profile pages with sanitized HTML and passive CSS.

Skins are edited in the profile editor under Skin HTML and shared from Skins -> Submit skin.

The skin field is limited to 20,000 characters.

Profile skins may be empty.

Shared skins must save to non-empty sanitized skin HTML.

Profile and shared skin forms sanitize skin HTML before saving.

Profile rendering sanitizes profile skin HTML again before rendering.

Quick Start

Use one or more <style> blocks for CSS.

Use [data-skin-page] for page-level color variables and page background styles.

Use [data-skin-root] for the rendered profile layout container.

Use [data-skin-part="..."] for stable profile and shell sections.

Example:

<style>
[data-skin-page] {
  --skin-palette-backdrop: #111111;
  --skin-palette-page: #171717;
  --skin-palette-page-text: #f8fafc;
  --skin-palette-surface: #262626;
  --skin-palette-surface-text: #f8fafc;
  --skin-palette-chrome: #7c3aed;
  --skin-palette-chrome-text: #ffffff;
  --skin-palette-link: #a78bfa;
  --skin-radius-panel: 8px;
  background: #111111;
}

[data-skin-root] {
  border: 2px solid #7c3aed;
}

[data-skin-part="name"] {
  color: #ffffff;
  text-shadow: 2px 2px 0 #7c3aed;
}

[data-skin-part="bio"] a:hover {
  color: #fbbf24;
}
</style>

<p>Optional custom skin HTML appears on the profile.</p>

Rendering Model

Skin CSS is collected from sanitized <style> blocks and rendered in the page head.

Skin HTML outside <style> blocks is rendered inside the profile as the custom-html skin part.

The custom-html block appears after the profile bio and before the blog preview.

If no sanitized CSS survives, the page does not receive data-skin-page and the shell skin hooks are not activated.

The profile layout root still receives data-skin-root and data-skin-version.

Stable Hooks

These hooks are the public selector contract.

Do not depend on ordinary app class names as stable API.

Internal class names may work when scoped under a supported hook, but they may change without a skin contract update.

Page And Shell Hooks

Hook Rendered target
[data-skin-page] The <body> element when sanitized CSS exists.
[data-skin-root] The profile layout container.
[data-skin-version="2026"] The current skin-version marker on the body when sanitized CSS exists and on the profile layout root.
[data-skin-part="page"] The <body> element when sanitized CSS exists.
[data-skin-part="shell"] The site shell container around navigation, page content, and footer.
[data-skin-part="header"] The header around the site navigation.
[data-skin-part="navigation"] The navigation element.
[data-skin-part="navigation-top"] The top navigation row.
[data-skin-part="brand"] The site brand area in navigation.
[data-skin-part="search"] The navigation search form area.
[data-skin-part="account"] The navigation account/action links area.
[data-skin-part="navigation-links"] The main navigation link list.
[data-skin-part="content"] The page <main> element.
[data-skin-part="footer"] The site footer.

Shell hooks render only when the profile page has sanitized CSS.

Profile Hooks

Hook Rendered target
[data-skin-part="sidebar"] The profile sidebar column.
[data-skin-part="identity"] The username plus profile photo/details group.
[data-skin-part="name"] The profile display-name heading.
[data-skin-part="about"] The profile photo and status area.
[data-skin-part="photo"] The profile photo wrapper.
[data-skin-part="details"] The status quote, when a status is set.
[data-skin-part="theme-song"] The profile theme-song audio player.
[data-skin-part="vibe"] The current-vibe box, when a current vibe is set.
[data-skin-part="actions"] The profile actions panel.
[data-skin-part="url"] The profile URL box.
[data-skin-part="links"] The social-links panel, when social links exist.
[data-skin-part="interests"] The interests panel, when interests exist.
[data-skin-part="main"] The main profile column.
[data-skin-part="notice"] Friend or owner notices, when those notices render.
[data-skin-part="bio"] The bio panel, when the profile has bio content.
[data-skin-part="bio-content"] The rendered bio content inside the bio panel.
[data-skin-part="custom-html"] Sanitized non-style skin HTML.
[data-skin-part="blog-preview"] The latest blog entries panel.
[data-skin-part="wall"] The wall posts panel.
[data-skin-part="post"] A single post card on profile walls and when a profile skin is reused outside that author's profile.
[data-skin-part="comment"] A single author-scoped comment when a profile skin is reused outside that author's profile.
[data-skin-part="friends"] The profile front-row friends panel.

Conditional hooks only exist when their target content exists.

When posts or comments appear outside an author's own profile, the app may reuse the author's profile skin inside a scoped item boundary.

Scoped author skins render only sanitized CSS; custom skin HTML is not inserted into posts or comments.

Profile wall posts stay inside the profile owner's page skin. They are not re-skinned by each post author's profile skin.

Use [data-skin-part="post"] to target only the post card element.

Use [data-skin-part="comment"] to target only the comment card element.

Existing wall-post descendant rules such as [data-skin-part="wall"] .post-card can also style author-scoped posts.

Author-scoped comments also receive post-card skin rules as a low-specificity fallback, so comments match posts by default.

Add [data-skin-part="comment"] rules when comments should intentionally differ from posts.

Exact page background declarations fill the viewport-wide post-height backdrop band.

Exact shell, content, and root paint declarations, such as backgrounds, borders, radii, outlines, and shadows, fill matching author-scoped structure layers inside the app container.

Exact page, shell, navigation, footer, content, and wall rules are otherwise treated as profile-page context in author-scoped items. They can pass CSS variables and inherited text or cursor declarations through, but they do not paint page chrome, pseudo-elements, navigation, or footer decorations around feed posts or comments.

The actions panel exposes .profile-actions, .profile-actions__cell, and .profile-action classes inside [data-skin-part="actions"].

Use .profile-action when styling action links, buttons, and disabled action labels.

State classes such as .profile-action--secondary, .profile-action--danger, and .profile-action--disabled only describe tone or availability; keep the main box treatment on .profile-action so links and form buttons stay visually consistent.

Do not style every descendant span inside [data-skin-part="actions"]; action icons and button labels also use spans.

Color Variables

Put skin color variables on [data-skin-page].

The app maps skin palette variables into the active page theme for profile pages.

Missing skin palette variables inherit the current app theme.

Supported skin palette variables are:

Variable Effect
--skin-palette-backdrop Page backdrop source color.
--skin-palette-page Main page/canvas source color.
--skin-palette-page-text Text color used on the page background.
--skin-palette-surface Panel and surface source color.
--skin-palette-surface-text Text color used on panels and surfaces.
--skin-palette-chrome Navigation and panel-heading source color.
--skin-palette-chrome-text Text color used on chrome and panel headings.
--skin-palette-accent Primary accent source color.
--skin-palette-accent-text Text color used on accent-colored controls.
--skin-palette-link Link color source.
--skin-palette-link-hover Page link hover color source.
--skin-palette-surface-link-hover Surface link hover color source.
--skin-palette-muted Muted text source color.
--skin-palette-focus Focus outline source color.

The Generate color skin button writes a <style> block with editable --skin-palette-* variables.

The Generate color skin button fills the skin editor; it does not save until the profile or skin form is submitted.

The color editor reads existing --skin-palette-* values from the current skin code.

The color editor also reads these older aliases: --skin-accent, --skin-accent-text, --skin-panel-heading-background, --skin-panel-heading-text, --skin-link, --skin-backdrop, --skin-background, and --skin-panel-background.

Do not set app-owned variables such as --theme-*, --app-theme-*, or --color-* in skin CSS.

The CSS sanitizer removes custom properties whose names start with --theme-, --app-theme-, or --color-.

Radius Variables

Put radius variables on [data-skin-page].

Variable Effect
--skin-radius Shared fallback radius for profile panels and media.
--skin-radius-panel Panels, cards, composers, URL boxes, and notice boxes.
--skin-radius-photo Profile photos, placeholders, friend images, and post media.
--skin-radius-control Form controls and buttons.

Examples:

<style>[data-skin-page]{--skin-radius:8px;--skin-radius-control:6px;}</style>
<style>[data-skin-page]{--skin-radius-panel:18px;--skin-radius-photo:50%;--skin-radius-control:999px;}</style>

CSS Selector Rules

Every skin CSS selector must include at least one supported hook.

Supported hooks are [data-skin-page], [data-skin-root], [data-skin-version="2026"], and supported [data-skin-part="..."] values.

Bare page selectors are limited to [data-skin-page] or [data-skin-version="2026"] by themselves.

Use [data-skin-root] or [data-skin-part="..."] before descendant selectors.

This is allowed:

[data-skin-part="bio"] a:hover {
  color: red;
}

This is dropped because it is a descendant of the body hook rather than a profile part:

[data-skin-page] a {
  color: red;
}

This is dropped because it has no skin hook:

body {
  color: red;
}

Selectors with unsupported attribute selectors are dropped.

Selectors using :has(), :is(), or :where() are dropped.

Simple :not(...) selectors can survive when the :not(...) contents only use simple class, id, tag, or spacing characters.

If any selector in a comma-separated selector list is unsafe, the whole rule is dropped.

Selectors longer than 500 characters are dropped.

The sanitizer removes < and > from CSS before parsing, so do not use child combinators.

Use descendant selectors instead of >.

CSS Declaration Rules

Most ordinary CSS property names are accepted.

The properties behavior and -moz-binding are blocked.

position:absolute, position:fixed, and position:sticky are blocked.

position:relative is not blocked by the skin sanitizer.

CSS custom properties are accepted when the name starts with -- and does not use a blocked app-owned prefix.

CSS values are dropped if they contain CSS escapes, expression(...), javascript:, vbscript:, behavior:, -moz-binding, attr(...), <, or >.

A declaration containing an unsafe url(...) is dropped.

Other safe declarations in the same rule can still survive.

CSS comments are removed.

Malformed CSS rule blocks may be dropped.

CSS At-Rules

@import is only kept for https://fonts.googleapis.com/... stylesheets.

Other @import rules are removed.

@media rules can survive when the media prelude uses the sanitizer's supported simple characters.

@keyframes rules can survive when the animation name is alphanumeric, underscore, or hyphen.

Keyframe selectors can be from, to, or percentages.

Other CSS at-rules are removed.

URLs And Media

Links and resources use different URL rules.

Link href values can be local paths, hash links, http:, https:, or mailto:.

Link rel is rewritten to nofollow noopener noreferrer.

Link target is kept only when it is _blank.

Image src, table background, and CSS url(...) resources can be local app paths or https: URLs.

Resource URLs using http:, data:, file:, javascript:, or vbscript: are removed.

Invalid image URLs are replaced with an empty <span>.

CSS declarations with invalid url(...) values are removed.

Embeds

Skins can include sanitized <iframe> embeds from supported media providers.

Supported embed providers are YouTube, SoundCloud, Vimeo, Spotify, Bandcamp, TikTok, and Dailymotion.

YouTube watch, shorts, embed, youtube-nocookie, and youtu.be URLs are normalized to https://www.youtube-nocookie.com/embed/....

Spotify URLs are normalized to https://open.spotify.com/embed/....

Vimeo URLs are normalized to https://player.vimeo.com/video/....

TikTok URLs are normalized to https://www.tiktok.com/player/v1/....

Dailymotion URLs are normalized to https://www.dailymotion.com/embed/video/....

Unsupported iframe sources are replaced with an empty <span>.

Iframe loading is rewritten to lazy.

Iframe referrerpolicy is rewritten to strict-origin-when-cross-origin.

Iframe sandbox is rewritten to allow-scripts allow-same-origin allow-presentation allow-popups.

Iframe allow is rewritten from the normalized provider URL.

Iframe allowfullscreen is added.

Allowed HTML

Allowed skin tags are:

a b big blockquote br caption center code div em font h1 h2 h3 h4 hr i iframe img li marquee ol p s small span strike strong style sub sup table tbody td tfoot th thead tr u ul

Allowed attributes by tag are:

Tag Attributes
a href, title, target, rel, class, id, style
font face, color, size, class, id, style
iframe src, title, width, height, class, id, style, loading, allow, allowfullscreen, frameborder, referrerpolicy, sandbox
img src, alt, title, width, height, class, id, style, loading, align, border
marquee behavior, direction, scrollamount, scrolldelay, loop, width, height, bgcolor, align, class, id, style
table width, height, border, cellpadding, cellspacing, align, valign, bgcolor, background, class, id, style
td width, height, colspan, rowspan, align, valign, bgcolor, background, class, id, style
th width, height, colspan, rowspan, align, valign, bgcolor, background, class, id, style
tr align, valign, bgcolor, background, class, id, style
all allowed tags class, id, style, align, title

Event handler attributes are not allowed.

<script> is not allowed.

Inline style attributes are sanitized with the same declaration sanitizer used for CSS blocks.

Classes and ids on custom HTML can be useful for your own markup.

CSS selectors should still start from a supported skin hook.

Shared Skins

Shared skins have a title, description, and skin HTML.

The shared skin description is ordinary user text, not skin HTML.

Shared skin HTML is sanitized before it is stored.

Users can preview a shared skin on their own profile before applying it.

Applying a shared skin copies that skin's saved HTML into the user's profile skin field.

User-authored shared skins can be edited by the owner or an admin.

Built-in shared skins can be edited by admins.

User-authored shared skins can be deleted by the owner, an admin, or a permitted moderator.

Built-in shared skins can be deleted by admins.

Examples

Background Image

<style>
[data-skin-page] {
  background: #000000 url("https://example.com/background.gif") repeat;
}
</style>

Style A Profile Part

<style>
[data-skin-part="friends"] {
  border: 2px solid #ff99cc;
}
</style>

Add Custom HTML

<p>
  <a href="https://example.com" target="_blank">My links</a>
</p>
<img src="https://example.com/banner.gif" alt="Decorative banner" loading="lazy">

Add A YouTube Embed

<iframe
  src="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  title="YouTube video"
  width="350"
  height="215">
</iframe>

The saved iframe source is normalized to https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ.

Common Reasons Code Disappears

The selector did not include a supported skin hook.

The selector used body, .profile, nav, table, or another global selector by itself.

The selector used an unsupported attribute selector.

The rule used a blocked at-rule.

The declaration used an unsafe URL.

The declaration used a blocked CSS value.

The HTML tag or attribute is not in the allowed lists.

The image or iframe URL did not match the sanitizer's URL rules.

Developer Verification

The skin hook list comes from src/skins/contract.ts.

Skin rendering behavior comes from src/skins/rendering.tsx, src/views/profile/page.tsx, and src/views/profile/main.tsx.

Shell hook behavior comes from src/shell/layout.tsx, src/shell/nav.tsx, and the layout primitives in src/shell/page.tsx.

Profile part placement comes from src/views/profile/layout.tsx, src/views/profile/sidebar.tsx, src/views/profile/main.tsx, src/views/profile/details.tsx, src/views/profile/actions.tsx, src/views/posts/panels.tsx, and the split layout helpers in src/shell/page.tsx.

Color and radius variables come from src/skins/colorPalette.ts, src/skins/colorPaletteEditor.tsx, and public/static/css/features/profile.css.

HTML, CSS, URL, and iframe sanitizer behavior comes from src/server/security/skinHtml.ts, src/server/security/css.ts, src/server/security/urls.ts, and src/server/security/embeds.ts.

Skin create, edit, preview, and apply behavior comes from src/routes/skins/index.tsx.

Profile skin save and generated color-skin behavior comes from src/routes/profile/index.tsx and src/routes/profile/edit/actions.ts.

When changing skin behavior, update this document in the same change.