berrypod/docs/plans/css-migration.md
jamey c65e777832 update progress and css migration plan status after phase 7
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 21:12:41 +00:00

18 KiB

Plan: CSS migration — Tailwind + DaisyUI to modern hand-written CSS

Status: Phases 0-7 complete. Phase 8 (optimisation) not started.

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.heex loads shop.css
  • root.html.heex loads admin.css (auth pages)
  • admin_root.html.heex loads both admin.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): :root tokens
  • 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 (replaces flex flex-col gap-*)
  • .cluster — horizontal flex-wrap with gap (replaces flex flex-wrap gap-*)
  • .row — horizontal flex, no wrap (replaces flex items-center gap-*)
  • .auto-grid — intrinsic responsive grid using auto-fill + minmax() (replaces grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4)
  • .container-page — centered max-width (replaces max-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:

  1. auto-fill/auto-fit grids eliminate most responsive classes
  2. Container queries (@container) for component-level responsiveness
  3. Two retained media queries for genuinely viewport-dependent layout:
    • @media (width >= 40em) — mobile to tablet
    • @media (width >= 64em) — tablet to desktop
  4. @media (hover: hover) for desktop hover effects (already in use)
  5. 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):

  1. Home (/)
  2. Collection (/collections/all)
  3. Collection with active filters
  4. PDP single image (/products/:id)
  5. PDP multi-image gallery
  6. Cart with items (/cart)
  7. Cart empty
  8. Cart drawer open
  9. Checkout success
  10. About (/about)
  11. Contact (/contact)
  12. Delivery (/delivery)
  13. Content page (privacy/terms)
  14. Search modal open
  15. 404 error page

= 60 screenshots per phase (15 pages x 4 breakpoints)

Process per phase:

  1. Capture "before" golden screenshots
  2. Complete phase work
  3. Capture "after" screenshots
  4. Visual diff (pixelmatch or manual comparison)
  5. Fix any regressions
  6. Run mix test (757 tests)
  7. Commit

Phase 0: Foundation and testing infrastructure

Estimate: 1 session (~1.5h) | Shippable: yes (no visual changes)

Tasks:

  1. Create Playwright screenshot Mix task (mix screenshots)
  2. Capture golden screenshots for all 15 pages at 4 breakpoints
  3. Create assets/css/shop/ directory structure:
    • reset.css, primitives.css, tokens.css, components.css, layout.css, utilities.css, overrides.css
  4. Create new entry file assets/css/shop.css with @layer declaration
  5. 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:

  1. Write CSS reset in shop/reset.css (~40 lines)
  2. Write layout primitives in shop/layout.css (~200 lines)
  3. Copy existing theme CSS into @layer wrappers in new structure
  4. Add LiveView variants and [data-phx-session] { display: contents }
  5. Wire new shop.css alongside existing app-shop.css (both loaded)
  6. 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.ex into components.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/simpleshop_theme_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/simpleshop_theme_web/components/shop_components/layout.ex (59 -> 0)
  • lib/simpleshop_theme_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 .heex page templates
  • checkout_success.html.heex (23), content.html.heex (3), others (3)
  • Also base.ex (2)

Files modified:

  • lib/simpleshop_theme_web/components/shop_components/content.ex (57 -> 0)
  • lib/simpleshop_theme_web/components/shop_components/base.ex (2 -> 0)
  • lib/simpleshop_theme_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 .heex page templates
  • Remove @import "tailwindcss" from app-shop.css
  • Remove simpleshop_theme_shop Tailwind profile from config/config.exs
  • Remove tailwind_shop watcher from config/dev.exs
  • Update assets.build and assets.deploy Mix aliases
  • Full visual regression

Files modified:

  • All shop component .ex files and page template .heex files
  • assets/css/app-shop.css -> becomes assets/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" from app.css
  • Delete assets/vendor/daisyui.js and assets/vendor/daisyui-theme.js

Files modified:

  • assets/css/admin-components.css (new)
  • assets/css/app.css (remove DaisyUI)
  • lib/simpleshop_theme_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:

  1. Replace remaining Tailwind utilities in admin/auth templates
  2. Remove @import "tailwindcss" from app.css
  3. Replace heroicons Tailwind plugin with plain CSS icon sizing
  4. Remove tailwind dep from mix.exs
  5. Remove all Tailwind config from config/config.exs and config/dev.exs
  6. Update assets.build and assets.deploy aliases
  7. Delete assets/vendor/heroicons.js
  8. Full visual regression across shop + admin
  9. All tests pass

Files modified:

  • mix.exs (remove :tailwind dep)
  • 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)

  1. Lightning CSS — production minification + vendor prefixing
  2. Remove all !important@layer handles cascade
  3. Container queries — product grid, PDP layout respond to container width
  4. content-visibility: auto — skip paint for offscreen sections
  5. CSS containmentcontain: layout style paint on product cards
  6. 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)
  7. @property — register typed custom properties for animated transitions
  8. Font optimisationfont-display: swap, size-adjust, latin subsetting
  9. Critical CSS inlining — inline above-fold CSS in shop_root.html.heex
  10. 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:

  1. Dead rule elimination: Lightning CSS strips empty rules and unreachable selectors during minification
  2. @layer zero-cost empties: Unused @layer blocks add zero bytes and zero specificity impact
  3. CSSGenerator already optimised: Only generates CSS for the active theme variant (not all 4 moods, 7 typographies, etc.)
  4. 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: @layer means styles compose without fighting
  • Animatable tokens: @property enables transitions on custom properties (smooth accent colour changes in theme editor)
  • content-visibility: Browsers skip layout/paint for offscreen product grid items

Risk mitigation

  1. Theme editor preview: .preview-frame rules in overrides layer always win. Test theme editor after every phase.
  2. CSSGenerator: Generates inline <style> setting CSS custom properties. Completely unaffected by the migration.
  3. JS hooks: SearchModal, CartDrawer, ProductImageScroll, Lightbox, CollectionFilters all reference DOM classes/data-attrs. Verify after each phase that referenced class names match.
  4. 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.