Build a theme for FoliHub
FoliHub gives every public profile a stable set of .fh-* CSS hooks. Drop your CSS in the editor — colour, layout, motion, embedded SVG, the lot — save, see it live. This page is the full reference: every selector, every sanitizer rule, recipes that pass first try.
Getting started
Custom CSS is a Pro feature (Pro and Pro+ both unlock it). Once you’re on a paid plan:
- Open /dashboard/customize. You’ll see a CodeMirror editor on the left and a live preview iframe on the right.
- Type CSS. Save snapshots the previous version into a 5- slot revision history — revert from the right column if you break something.
- Add
?nocss=1to any URL of your public page (e.g.folihub.com/yuki?nocss=1) to view it without your CSS — useful when you’ve hidden a button into oblivion.
The DOM you target
Two pages render under the same <body data-theme="...">. Your CSS is injected after the theme variables, so you can override anything without !important.
Linkfolio (/username)
<body data-theme="amber">
<main class="fh-page fh-linkfolio">
<header class="fh-hero">
<img class="fh-avatar" src="..." />
<h1 class="fh-display-name">Yuki Tanaka</h1>
<p class="fh-tagline">Designer · Bangkok</p>
</header>
<ul class="fh-links">
<li><a class="fh-link-button fh-link-primary fh-link-donate" href="..."> ... </a></li>
<li><a class="fh-link-button fh-link-social fh-link-social-instagram" href="..."> ... </a></li>
<li><a class="fh-link-button fh-link-custom fh-link-custom-{handle}" href="..."> ... </a></li>
<li><div class="fh-text-block fh-text-block--header">DIVIDER</div></li>
</ul>
<footer class="fh-footer">© 2026 ...</footer>
</main>
</body>Portfolio (/username/portfolio)
<body data-theme="amber">
<div class="fh-page fh-portfolio">
<nav class="fh-portfolio-nav">...</nav>
<header class="fh-portfolio-hero"><h1>...</h1></header>
<section class="fh-portfolio-section fh-portfolio-about">...</section>
<section class="fh-portfolio-section fh-portfolio-work">
<article class="fh-project-card">...</article>
</section>
<section class="fh-portfolio-section fh-portfolio-skills">...</section>
<section class="fh-portfolio-section fh-portfolio-activities">...</section>
<section class="fh-portfolio-section fh-portfolio-contact">...</section>
</div>
</body>Selector cheatsheet
Every .fh-* hook the public pages expose. Don’t target classes that aren’t on this list— they’re private and may change without notice.
Page structure
.fh-pageWrapper on every public page (linkfolio + portfolio)..fh-linkfolioLinkfolio page only..fh-portfolioPortfolio page only..fh-heroLinkfolio hero section (avatar + name + tagline)..fh-avatarProfile avatar / initials circle..fh-display-nameDisplay name heading..fh-taglineTagline text under the name..fh-linksThe grid of link buttons on the linkfolio..fh-footerThe bottom credit line.
Buttons (any kind)
.fh-link-buttonEvery link button (hover, color, radius)..fh-link-primaryAny button with the star/highlight on..fh-link-socialAll social-media buttons (any platform)..fh-link-customCustom links (anything you create yourself)..fh-link-youtubeYouTube preview cards..fh-link-resumeResume download button (when enabled)..fh-link-donateDonate / Support button (when enabled).
Per-platform socials
.fh-link-social-lineLINE button..fh-link-social-facebookFacebook button..fh-link-social-instagramInstagram button..fh-link-social-tiktokTikTok button..fh-link-social-youtubeYouTube button..fh-link-social-twitterX / Twitter button..fh-link-social-threadsThreads button..fh-link-social-websiteWebsite button..fh-link-social-githubGitHub button..fh-link-social-linkedinLinkedIn button..fh-link-social-discordDiscord button..fh-link-social-behanceBehance button..fh-link-social-dribbbleDribbble button..fh-link-social-figmaFigma button..fh-link-social-mediumMedium button..fh-link-social-substackSubstack button..fh-link-social-producthuntProduct Hunt button..fh-link-social-shopShop button.
Targeting your own custom links
.fh-link-custom-{handle}Set a CSS handle on any custom link (link editor → Edit → CSS handle) to target it directly..fh-text-blockAny text block between buttons..fh-text-block--headerShort uppercase divider..fh-text-block--paragraphLonger prose text block.
Portfolio + projects
.fh-portfolio-navSticky portfolio top bar..fh-portfolio-heroPortfolio hero section (large title)..fh-portfolio-sectionEach numbered portfolio section..fh-portfolio-aboutAbout section (also -work, -skills, -activities, -contact)..fh-project-cardEach project card on the portfolio.
Sanitizer rules
CSS is validated at save time. Anything outside these rules gets rejected with a friendly error that points at the offending rule.
Allowed
- Any standard CSS property, pseudo-class, pseudo-element, media query, or
@keyframesanimation. - Units:
px rem em % vw vh svh dvh+ functionsclamp() min() max() calc(). - Custom properties (CSS variables), transforms, transitions, filters, gradients,
backdrop-filter. @importURLs limited to:fonts.googleapis.com,fonts.gstatic.com,fonts.bunny.net,cdn.jsdelivr.net.url(...)references restricted to those four hosts plus your own Supabase Storage bucket. No external image hotlinking.- Inline data URIs:
data:image/(png|jpe?g|webp|gif|svg+xml);…— see the SVG section below.
Rejected
expression(...)(legacy IE)behavior:,-moz-binding:,binding:javascript:URLs anywhere@importorurl(...)targeting any host outside the allowlist
Embed SVG
You can embed any SVG directly in CSS via a data:URI — no upload, no asset host. Waves, organic shapes, repeating patterns, hand-drawn marks, animated particles all work. Here’s the full pipeline.
Step 1 — Design the SVG
Pick whichever you’re fastest in:
- Figma — design the shape, then either: right-click → Copy as SVG, or File → Export → SVG. Single shapes work best; groups can pull in extra structure you don’t need.
- Inkscape (free, desktop) — full vector editor; File → Save as → Optimised SVG already strips a lot of metadata for you.
- Generators — speed-run common shapes: haikei.app (waves, blobs, scattered shapes), getwaves.io (waves only, dead simple), svgbackgrounds.com (repeating patterns).
- Hand-coded — for simple shapes a
<path d="M 0,40 Q 50,80 100,40" />is shorter than anything you’d export. MDN’s SVG path reference covers everything.
Step 2 — Optimise
Designer-exported SVGs carry a lot of dead weight (Figma / Illustrator metadata, comments, group wrappers). Run them through one of these:
- SVGOMG (web, free) — drop the SVG, it minifies + lets you toggle each transform. Default settings are sane; toggle Prefer viewBox on and Multipass on for an extra ~10%.
- svgo (npm) — same engine, scriptable.
Step 3 — URL-encode for the data URI
CSS url(...)wants a string — but SVGs contain characters that need escaping before they’re safe in CSS. Three rules:
- Use single quotes for SVG attributes (
fill='white') so the outer CSS string can use double quotes around the whole URI. - Replace
#with%23— important for hex colours (#fff→%23fff) and anyurl(#gradId)referencing internal SVG defs. - Replace
%with%25— only when you have CSS keyframes inside the SVG (0%→0%25).
Tools that do this for you:
- yoksel’s URL encoder for SVG — paste raw SVG, copy the CSS-ready string with the data URI prefix already wrapped.
- npm:
mini-svg-data-uri— if you’re generating CSS programmatically.
Step 4 — Use it in CSS
Drop the result into background-image or mask-image. Combine with gradients, position, repeat as you like.
/* A wave at the bottom of the linkfolio hero. */
.fh-linkfolio {
background:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 80' preserveAspectRatio='none'><path d='M0,40 C300,80 600,0 1200,40 L1200,80 L0,80 Z' fill='%23ffffff' fill-opacity='0.4'/></svg>") bottom / 100% 80px no-repeat,
linear-gradient(180deg, #e0f2fe, #38bdf8);
}Recipes
Copy these and tweak. They pass the sanitizer.
Repeating dot grid
.fh-page {
background-image:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><circle cx='12' cy='12' r='1.2' fill='%23000000' fill-opacity='0.08'/></svg>");
background-size: 24px 24px;
}Animated bubbles rising (CSS in the SVG)
.fh-linkfolio::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 800' preserveAspectRatio='xMidYMid slice'><style>@keyframes r{0%25{transform:translateY(40px);opacity:0}10%25{opacity:.6}100%25{transform:translateY(-840px);opacity:0}}circle{animation:r linear infinite}</style><circle cx='80' cy='800' r='4' fill='white' style='animation-duration:18s'/><circle cx='220' cy='800' r='6' fill='white' style='animation-duration:22s;animation-delay:-7s'/></svg>");
background-size: 400px 800px;
background-repeat: repeat;
}Note the %25 in 0%25 and 10%25 — those are CSS keyframe selectors inside the SVG, so the % needs encoding.
Hand-drawn underline on display name
.fh-display-name {
display: inline-block;
background:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 12' preserveAspectRatio='none'><path d='M0,6 Q40,2 100,6 T200,6' stroke='%23f59e0b' stroke-width='3' fill='none' stroke-linecap='round'/></svg>") bottom / 100% 8px no-repeat;
padding-bottom: 4px;
}Animation patterns
Two patterns cover most needs. Both are lighter than they look.
CSS keyframes (the boring-but-fast option)
Define @keyframes once, attach via animation wherever. Stick to GPU-friendly properties: transform, opacity, filter. Avoid animating width, height, top, left — they force layout every frame.
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.fh-avatar {
animation: float 4s ease-in-out infinite;
}Animated SVG embedded as background
SVGs used as background-image run their internal CSS animations. Drop a <style> block inside the SVG, target the elements you drew, repeat the SVG via background-repeat. Vary animation-delay + animation-duration per element so the repeat seam disappears. Recipe is in the SVG section above.
Performance
backdrop-filter: blur(...)is the most expensive everyday CSS property — every scroll frame re-blurs the layer behind it. Use it on small surfaces only, or skip entirely. A solidrgba(255,255,255,.65)reads as soft glass at zero cost when the ground is a uniform gradient.- SVG-as-background animations run on the compositor but tile-paint each cycle. Bigger tile + fewer animated elements = lighter than many small tiles with many elements.
- Always pair non-essential animations with
prefers-reduced-motionso visitors who opt out get a static fallback:
@media (prefers-reduced-motion: reduce) {
.fh-avatar { animation: none; }
.fh-linkfolio::after { display: none; }
}FAQ
▸My save is rejected — “url(...) is blocked”
▸My save is rejected — “url('%23gradId') is blocked”
# is %23 in the data URI; the encoder usually does this for you.▸Animation doesn't run inside my embedded SVG
% inside keyframe selectors (0% → 0%25), so the SVG parser sees broken syntax. (2) the SVG is used as <img> or mask-image instead of background-image — only the background context runs internal CSS animations in most browsers.▸How do I target one specific custom link?
shop), and target it as .fh-link-custom-shop.▸Can I write JavaScript?
javascript: URLs, expressions, bindings). Animations + interactivity have to be CSS-only — which is plenty in 2026 (:has(), :focus-within, scroll-driven animations etc.).Ready to build?
Open the editor and try it. Saves snapshot the previous version automatically — you can’t break anything you can’t revert.