18 KiB
Plan: CSS migration — Tailwind + DaisyUI to modern hand-written CSS
Status: Complete (all phases 0-8)
Overview
Replace Tailwind CSS v4 and DaisyUI with a modern, layered CSS system using @layer, native nesting, container queries, oklch(), @property, and other 2024+ CSS features. Zero framework dependencies. Smaller output. Full cascade control.
Final state (post Phase 7)
| Bundle | Minified | Gzipped | Contents |
|---|---|---|---|
shop.css |
54.1 KB | 9.8 KB | Hand-written component CSS + three-layer theme |
admin.css |
90.8 KB | 17.8 KB | Utility CSS + themes + heroicon data URIs |
app.js |
139.2 KB | 42.9 KB | LiveView + hooks |
Zero framework dependencies. No Tailwind, no DaisyUI, no PostCSS plugins.
Three esbuild profiles handle all asset builds. Three root layouts isolate CSS:
shop_root.html.heexloadsshop.cssroot.html.heexloadsadmin.css(auth pages)admin_root.html.heexloads bothadmin.css+shop.css
Pre-migration state (for reference)
| Bundle | Compiled | Contents |
|---|---|---|
app-shop.css |
53 KB | Tailwind v4 (no DaisyUI) + three-layer theme |
app.css |
212 KB | Tailwind v4 + DaisyUI + three-layer theme |
281 style= attributes across 10 files. All removed in Phases 2-4.
Three-layer theme system (1,130 lines) preserved throughout:
- Primitives (94 lines):
:roottokens - Attributes (717 lines):
.themed+data-*selectors - Semantic (319 lines): aliases, accessibility, component styles
CSSGenerator (Elixir): generates inline <style> from DB settings. Unchanged.
Architecture: CSS layer order
@layer reset, primitives, tokens, components, layout, utilities, overrides;
| Layer | Purpose | Source |
|---|---|---|
reset |
Box-sizing, margin reset, img/list defaults | New ~40 lines |
primitives |
:root design tokens (spacing, radii, fonts, type scale) |
Existing theme-primitives.css |
tokens |
Theme token switching via data-* attrs on .themed |
Existing theme-layer2-attributes.css |
components |
All component styles (product cards, cart, gallery, etc.) | Existing semantic.css + extracted inline styles |
layout |
Layout primitives (stack, cluster, row, auto-grid, etc.) | New ~200 lines |
utilities |
Tiny utility set (sr-only, truncate, text-balance) | New ~60 lines |
overrides |
Theme editor .preview-frame rules |
Existing from app.css |
@layer eliminates all !important hacks — later layers beat earlier ones.
Layout primitives (replace Tailwind flex/grid utilities)
Seven primitives cover ~80% of Tailwind layout usage:
.stack— vertical flex with gap (replacesflex flex-col gap-*).cluster— horizontal flex-wrap with gap (replacesflex flex-wrap gap-*).row— horizontal flex, no wrap (replacesflex items-center gap-*).auto-grid— intrinsic responsive grid usingauto-fill+minmax()(replacesgrid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4).container-page— centered max-width (replacesmax-w-7xl mx-auto px-*).with-sidebar— main + aside flex layout (replaces PDP two-column grid).center— flex center alignment
Modifiers via custom properties: --stack-gap, --grid-gap, --auto-grid-min.
Responsive strategy
Intrinsic design over breakpoints:
auto-fill/auto-fitgrids eliminate most responsive classes- Container queries (
@container) for component-level responsiveness - Two retained media queries for genuinely viewport-dependent layout:
@media (width >= 40em)— mobile to tablet@media (width >= 64em)— tablet to desktop
@media (hover: hover)for desktop hover effects (already in use)- Fluid typography via
clamp()(already in use)
Admin component replacements (DaisyUI)
~300 lines of admin-components.css replacing DaisyUI's 15 used components:
| DaisyUI | Replacement |
|---|---|
drawer |
CSS grid layout + checkbox toggle |
btn variants |
.admin-btn + modifiers |
modal |
Native <dialog> + ::backdrop |
table |
Styled <table> with .admin-table |
input/select/textarea |
.admin-input with :focus-visible |
checkbox/toggle |
appearance: none + custom styling |
range |
accent-color on native <input type=range> |
alert/toast |
Left-border accent box + fixed-position container |
badge |
Inline pill with border-radius: 9999px |
Build pipeline changes
During migration (Phases 1-5): Tailwind runs alongside new CSS for shop.
After shop migration (Phase 5): Remove tailwind_shop watcher.
After full migration (Phase 7): Remove tailwind dep entirely.
Production CSS: esbuild handles @import resolution + minification (already available). Lightning CSS added optionally in Phase 8 for vendor prefixing.
Dev: No CSS build step needed — Phoenix live_reload watches .css files directly. Modern CSS features (nesting, @layer, @container) work in all current browsers natively.
Visual regression testing
Tool: Mix task wrapping Playwright for automated screenshots.
Breakpoints: 375px (mobile), 768px (tablet), 1024px (laptop), 1440px (desktop)
Pages (15 states):
- Home (
/) - Collection (
/collections/all) - Collection with active filters
- PDP single image (
/products/:id) - PDP multi-image gallery
- Cart with items (
/cart) - Cart empty
- Cart drawer open
- Checkout success
- About (
/about) - Contact (
/contact) - Delivery (
/delivery) - Content page (privacy/terms)
- Search modal open
- 404 error page
= 60 screenshots per phase (15 pages x 4 breakpoints)
Process per phase:
- Capture "before" golden screenshots
- Complete phase work
- Capture "after" screenshots
- Visual diff (pixelmatch or manual comparison)
- Fix any regressions
- Run
mix test(757 tests) - Commit
Phase 0: Foundation and testing infrastructure
Estimate: 1 session (~1.5h) | Shippable: yes (no visual changes)
Tasks:
- Create Playwright screenshot Mix task (
mix screenshots) - Capture golden screenshots for all 15 pages at 4 breakpoints
- Create
assets/css/shop/directory structure:reset.css,primitives.css,tokens.css,components.css,layout.css,utilities.css,overrides.css
- Create new entry file
assets/css/shop.csswith@layerdeclaration - Wire nothing yet — just file structure
Files created:
assets/css/shop.css(entry, not wired)assets/css/shop/reset.css(skeleton)assets/css/shop/layout.css(skeleton)assets/css/shop/components.css(skeleton)assets/css/shop/utilities.css(skeleton)assets/css/shop/overrides.css(skeleton)lib/mix/tasks/screenshots.ex
Acceptance: golden screenshots captured, mix test passes, no visual changes.
Phase 1: Layout primitives and reset
Estimate: 1 session (~1.5h) | Shippable: yes
Tasks:
- Write CSS reset in
shop/reset.css(~40 lines) - Write layout primitives in
shop/layout.css(~200 lines) - Copy existing theme CSS into
@layerwrappers in new structure - Add LiveView variants and
[data-phx-session] { display: contents } - Wire new
shop.cssalongside existingapp-shop.css(both loaded) - Visual regression — should be no changes (Tailwind still present)
Files modified:
assets/css/shop/*.css(populate reset + layout)assets/css/app-shop.css(add import of new shop.css)
Acceptance: layout primitives available, no visual changes, all tests pass.
Phase 2: Inline style extraction — product components
Estimate: 2 sessions (~3h) | Shippable: after each session
2a — Product cards, grid, badges, hero, categories (~1.5h):
- Extract 40+ inline styles from
product.exintocomponents.css - Classes:
.product-card-body,.product-card-title,.product-card-price,.hero-section,.hero-content,.category-card,.category-card-overlay - Visual regression: home page, collection page
2b — PDP, variant selector, gallery, accordion (~1.5h):
- Extract remaining ~40 inline styles from
product.ex - Classes:
.pdp-layout,.variant-selector,.color-swatch,.size-button,.quantity-selector,.add-to-cart-btn,.accordion-item - Visual regression: PDP page
Files modified:
lib/berrypod_web/components/shop_components/product.ex(83 style= -> 0)assets/css/shop/components.css
Acceptance: product.ex has zero inline styles, visual regression clean.
Phase 3: Inline style extraction — layout + cart
Estimate: 2 sessions (~3h) | Shippable: after each session
3a — Layout components (~1.5h):
- Extract 59 inline styles from
layout.ex - Classes:
.announcement-bar,.mobile-bottom-nav,.mobile-nav-item,.search-modal-overlay,.search-modal-panel,.search-result-item,.shop-footer-section - Visual regression: all pages (header/footer/nav are global)
3b — Cart components (~1.5h):
- Extract 51 inline styles from
cart.ex - Classes:
.cart-drawer-header,.cart-drawer-body,.cart-drawer-footer,.cart-item-details,.cart-remove-btn,.cart-empty-state,.checkout-btn,.order-summary - Visual regression: cart page, cart drawer
Files modified:
lib/berrypod_web/components/shop_components/layout.ex(59 -> 0)lib/berrypod_web/components/shop_components/cart.ex(51 -> 0)assets/css/shop/components.css
Acceptance: both files zero inline styles, visual regression clean.
Phase 4: Inline style extraction — content + page templates
Estimate: 1.5 sessions (~2.5h) | Shippable: yes
4a — Content components (~1.5h):
- Extract 57 inline styles from
content.ex - Classes:
.content-body,.contact-form,.info-card,.trust-badge,.review-card,.star-rating,.newsletter-card,.page-title
4b — Page templates + remaining (~1h):
- Extract 29 inline styles from
.heexpage templates checkout_success.html.heex(23),content.html.heex(3), others (3)- Also
base.ex(2)
Files modified:
lib/berrypod_web/components/shop_components/content.ex(57 -> 0)lib/berrypod_web/components/shop_components/base.ex(2 -> 0)lib/berrypod_web/components/page_templates/*.html.heex(29 -> 0)assets/css/shop/components.css
Acceptance: zero inline styles remain (0/281), full visual regression clean.
Phase 5: Remove Tailwind from shop pages
Estimate: 2 sessions (~3h) | Shippable: yes
5a — Replace Tailwind utility classes in components (~1.5h):
- Replace layout utilities with
.stack,.cluster,.row,.auto-grid, etc. - Replace typography utilities with
.t-caption,.t-small,.t-heading-* - Replace colour utilities with theme CSS variables
- Replace responsive prefixes with container queries / media queries
5b — Remove Tailwind shop build (~1.5h):
- Replace remaining Tailwind classes in
.heexpage templates - Remove
@import "tailwindcss"fromapp-shop.css - Remove
berrypod_shopTailwind profile fromconfig/config.exs - Remove
tailwind_shopwatcher fromconfig/dev.exs - Update
assets.buildandassets.deployMix aliases - Full visual regression
Files modified:
- All shop component
.exfiles and page template.heexfiles assets/css/app-shop.css-> becomesassets/css/shop.css(pure CSS entry)config/config.exs,config/dev.exs,mix.exs
Acceptance: no Tailwind classes in shop code, Tailwind shop build removed, admin still on Tailwind + DaisyUI, all tests pass, visual regression clean.
Phase 6: Replace DaisyUI (admin pages)
Estimate: 2 sessions (~3h) | Shippable: yes
6a — Admin layout and navigation (~1.5h):
- Create
assets/css/admin-components.css(~300 lines) - Admin drawer (CSS grid), navbar, sidebar menu, button styles
- Update
admin.html.heex,admin_root.html.heex
6b — Admin components + auth pages (~1.5h):
- Admin modal (
<dialog>), alert/toast, badge, divider, toggle, range - Update
core_components.ex - Update admin LiveViews and auth pages
- Remove
@plugin "../vendor/daisyui"fromapp.css - Delete
assets/vendor/daisyui.jsandassets/vendor/daisyui-theme.js
Files modified:
assets/css/admin-components.css(new)assets/css/app.css(remove DaisyUI)lib/berrypod_web/components/core_components.ex- All admin LiveView files
- Auth LiveView files
Acceptance: zero DaisyUI classes, vendor files deleted, admin looks identical.
Phase 7: Remove Tailwind entirely
Estimate: 1 session (~1.5h) | Shippable: yes
Tasks:
- Replace remaining Tailwind utilities in admin/auth templates
- Remove
@import "tailwindcss"fromapp.css - Replace heroicons Tailwind plugin with plain CSS icon sizing
- Remove
tailwinddep frommix.exs - Remove all Tailwind config from
config/config.exsandconfig/dev.exs - Update
assets.buildandassets.deployaliases - Delete
assets/vendor/heroicons.js - Full visual regression across shop + admin
- All tests pass
Files modified:
mix.exs(remove:tailwinddep)config/config.exs(remove tailwind config)config/dev.exs(remove tailwind watchers)assets/css/app.css(pure CSS entry)- All remaining admin/auth template files with Tailwind classes
Acceptance: tailwind gone from deps, zero Tailwind classes anywhere, CSS builds correctly, all tests pass, visual regression clean.
Phase 8: Optimisation and modern CSS enhancements
Estimate: 1-2 sessions (~2-3h) | Shippable: yes (each item independent)
- Lightning CSS — production minification + vendor prefixing
- Remove all
!important—@layerhandles cascade - Container queries — product grid, PDP layout respond to container width
content-visibility: auto— skip paint for offscreen sections- CSS containment —
contain: layout style painton product cards - oklch colours — upgrade CSSGenerator from HSL to oklch
- Better perceptual uniformity across hues
color-mix(in oklch, var(--accent) 80%, black)replaces HSL lightness math- Relative colour syntax:
oklch(from var(--accent) calc(l - 0.1) c h)
@property— register typed custom properties for animated transitions- Font optimisation —
font-display: swap,size-adjust, latin subsetting - Critical CSS inlining — inline above-fold CSS in
shop_root.html.heex - Performance audit — measure file sizes, Lighthouse scores
Post-migration efficiencies
Tree-shaking and dead code
With Tailwind, tree-shaking is essential — it generates thousands of utility classes and purges unused ones. Hand-written CSS doesn't need this because you only write what you use. The CSS is "pre-shaken" by definition.
Efficiencies that do apply:
- Dead rule elimination: Lightning CSS strips empty rules and unreachable selectors during minification
@layerzero-cost empties: Unused@layerblocks add zero bytes and zero specificity impact- CSSGenerator already optimised: Only generates CSS for the active theme variant (not all 4 moods, 7 typographies, etc.)
- Scope-based pruning: All shop CSS scoped under
.themed/.shop-container— unmatched selectors have no performance cost
Actual sizes (post migration)
| Bundle | Before | After (minified) | After (gzipped) | Reduction |
|---|---|---|---|---|
| Shop CSS | 53 KB | 54.1 KB | 9.8 KB | similar size but zero framework |
| Admin CSS | 212 KB | 90.8 KB | 17.8 KB | ~57% minified |
| Total | 265 KB | 144.9 KB | 27.6 KB | ~45% minified |
Shop CSS grew slightly vs original Tailwind because all component styles are now explicit (Tailwind tree-shook aggressively). Admin CSS dropped substantially because DaisyUI's 212 KB base is gone. The gzipped sizes are what matters for users: 9.8 KB shop, 17.8 KB admin.
Other gains
- No build step in dev: CSS changes are instant (no Tailwind compilation)
- No framework version chasing: Pure CSS doesn't break on updates
- Better cacheability: Semantic class names change less often than utilities
- Composable cascade:
@layermeans styles compose without fighting - Animatable tokens:
@propertyenables transitions on custom properties (smooth accent colour changes in theme editor) content-visibility: Browsers skip layout/paint for offscreen product grid items
Risk mitigation
- Theme editor preview:
.preview-framerules inoverrideslayer always win. Test theme editor after every phase. - CSSGenerator: Generates inline
<style>setting CSS custom properties. Completely unaffected by the migration. - JS hooks:
SearchModal,CartDrawer,ProductImageScroll,Lightbox,CollectionFiltersall reference DOM classes/data-attrs. Verify after each phase that referenced class names match. - Rollback: Each phase is an atomic commit. Tailwind runs alongside new CSS during Phases 1-4, so old styles remain as a safety net.
Summary
| Phase | What | Sessions | Hours |
|---|---|---|---|
| 0 | Foundation + screenshot tooling | 1 | 1.5 |
| 1 | Layout primitives + reset | 1 | 1.5 |
| 2 | Extract product inline styles | 2 | 3 |
| 3 | Extract layout + cart inline styles | 2 | 3 |
| 4 | Extract content + template inline styles | 1.5 | 2.5 |
| 5 | Remove Tailwind from shop | 2 | 3 |
| 6 | Replace DaisyUI (admin) | 2 | 3 |
| 7 | Remove Tailwind entirely | 1 | 1.5 |
| 8 | Optimisation + modern enhancements | 1.5 | 2.5 |
| Total | 14 | ~22h |
Shop fully migrated after Phase 5. Admin after Phase 7. Phase 8 is polish. Each phase is independently shippable with visual regression verification.