Design Choices
Functional, local chaos
Combining utility classes in html markup results in utter chaos. It makes code look ugly. But...there is a functional appeal to it that's growing on me. Perhaps its strongest suit is what I understand to be grug-compatible locality. Think "lady in the red dress" in the first Matrix: ostentibly unpleasant yet quite readable.
tailwind.config.js
: dawn / dusk
I use dawn
to signify the light theme and dusk
to signify the dark theme. The words are uncommon enough that it makes it easy to replace them all in a wholesale find-all-and-replace approach via the IDE, or simply to reuse them by modifying the values found in the Tailwind config file:
module.exports = {
darkMode: "class", // (1)
theme: {
extend: {
colors: {
dawn: { // (2)
darker: "#15803d", // green-700
DEFAULT: "#16a34a", // green-600
muted: "#22c55e", // green-500
lighter: "#86efac", // green-300
},
dusk: {
darker: "#7e22ce", // purple-700
DEFAULT: "#9333ea", // purple-600
muted: "#a855f7", // purple-500
lighter: "#d8b4fe", // purple-300
},
grayed: {
darker: "#334155", // slate-700
DEFAULT: "#475569", // slate-600
muted: "#94a3b8", // slate-400
lighter: "#cbd5e1", // slate-300
},
},
},
},
}
- Enables theme switching by modifying
<html>
tag - The color-number convention, e.g. green-500 refers to the designation found in TailwindCSS colors.
Note that the named themes can utilize suffixes and prefixes that TailwindCSS is known for, e.g. with bg-dawn
, dawn
is a variable I made up, whereas bg
as a prefix will always mean apply a background color of CSS variable dawn
.
It also becomes easier to implement a custom partial template, particularly with dashed notation that specifies custom accents like muted
, lighter
, and darker
... as well as generic tailwind prefixes like hover
, focus-visible
, etc.
input.css
definition for buttons, forms
Most of the declarations of utility classes generally happen in common html files.
The definitions of these declarations are automatically made in a single output.css
file, assuming the build step is running.
However, the input.css
makes initial adjustments to commonly styled elements:
@layer base {
[type="text"],
[type="email"],
[type="url"],
/* etc.; see override source (1) */
{
@apply mt-2.5 block w-full rounded-md border-0 px-3.5 py-2 text-sm/6 text-grayed-darker dark:text-grayed-lighter
/* other tailwind classes */
}
button.btn[data-btn="primary"], a.btn[type="button"][data-btn="primary"] {
@apply text-green-100 dark:text-purple-100 bg-dawn dark:bg-dusk
/* other tailwind classes */
}
}
- Overrides selectors foudn in
/node_modules/@tailwindcss/forms/src/index.js
The controversial @apply
The @apply directive is considered by TailwindCSS maker to be ill-conceived but I find it rather useful to style base elements: specifically buttons and form fields described above. And typing these all out in using the theme directive seems overly verbose. I felt the same about the Tailwind utility classes when they first came out so maybe I'll grow to like the theme()
convention in time.
django-fragments
for skeletal partials
The layout of DOM nodes of html partials is handled by django-fragments, especially for icons. The idea is to make this library handle the construction of a building... so that it's ready for a paint job afterwards. See existing partials for:
-
{% icon %}
- idiomatic<svg>
combiner with neighboring / parent tagsInvocation via Django Template Language name='x_mark_mini'
refers to a heroicon (default) svg copy/pasted into a file named 'heroicon_x.html'- The
aria_hidden
attribute is converted toaria-hidden
,pre_text
andpre_class
means add a<span class='sr-only'>Close menu<span>
before (pre)_ the svg icon.
Output HTML after the Template is populated with the Context.<span class="sr-only">Close menu</span> <svg aria-hidden="true" class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="1.5" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"> </path> </svg>
-
{% themer %}
-<button onclick=toggleTheme()>
enclosing two{% icon %}s
./tailwind.config.jsmodule.exports = { darkMode: "class", // theme switching via <html> class. // (1) }
{% themer %}
makes use of js functionstoggleTheme()
from django-fragments to change the class.
Overridding defaults{# re: css of sun and moon (1), re: css icon (2) #} {% themer icon1_name="sun" icon1_css="dark:hidden icon" icon2_name="moon" icon2_css="hidden dark:block icon" btn_kls="desktop flex justify-center items-center rounded-md transition" %}
- sun has
dark:hidden
+ moon hashidden dark:block
. TailwindCSS translation: if<html class='dark'>
: (a) hide sun icon via:hidden
; (b) make moon icon visible via:block
. icon
as a css class defined ininput.css
-
{% hput %}
- A limited, simple<input>
-basedBoundField
+ related<label>
, tags forhelp_css
,label_css
(complementsdjango-widget-tweaks
).Invocation via Django Template Language - Must include a classname so that this can be detected by TailwindCSS. Enables future styling to related css targets, e.g. using
.fx
:.fx > ul.errorlist
.fx > p.help
.fx > label
Output HTML after the Template is populated with the Context. v5.0
The
BoundField.as_field_group()
seems like a viable alternative to use in the future.TailwindCSS Forms Integration
With this setup, I can now use TailwindCSS forms plugin and override defaults in
input.css
:input.css refers to the Tailwind input css filediv.fx > ul.errorlist { /* handles errors to be displayed post validation */ @apply flex flex-col mt-1 ml-1 text-xs sm:text-sm tracking-wide text-pink-500 font-thin } div.fx > p.help { /* handles the help text */ @apply flex mt-1 ml-1 text-xs tracking-wide font-thin text-grayed dark:text-grayed-muted } @layer base { /* see forms plugin override */ [type="text"], [type="email"], [type="url"], [type="password"], [type="number"], [type="date"], [type="datetime-local"], [type="month"], [type="search"], [type="tel"], [type="time"], [type="week"], [multiple], textarea, select, .faux-select /* works in tandem with sel.html */ { @apply mt-2.5 block w-full rounded-md border-0 px-3.5 py-2 text-sm/6 text-grayed-darker dark:text-grayed-lighter bg-white dark:bg-grayed-darker shadow-sm ring-1 ring-inset focus:outline-none focus:ring-1 ring-gray-300 dark:ring-grayed-darker focus:ring-dawn dark:focus:ring-dusk } }
The generate route for forms is to have render the entire form based on model declaration. For more granular controls, i.e. styling the individual components of a field, it's up to the user to reconstruct the form manually.
django-widget-tweaks
help adjust the field itself but the neighboring tags like<label>
, the wrapping<div>
, the help text and possible error messages, I think, still need to be managed individually.Since the field template from django-fragments is intentionally devoid of style, the only sources that need to be considered in the styling of the
{% hput %}
field are:- any applicable tweaks done by django-widget-tweaks when
{% hput ... %}
is first invoked; and - the styled input.css, specifically overriding TailwindCSS forms plugin.
- Must include a classname so that this can be detected by TailwindCSS. Enables future styling to related css targets, e.g. using
Sample template tags for inseparable partials
There are some fragments however that cannot be easily separated from the css and javascript involved. They're defined in the page
's app and the base.html
rather than in a third-party library like django-fragments
. Consider:
-
{% sel %}
- aria-* and hyperscripted<select>
Invocation - Custom template tag from the "pages" app. See
src/pages/templatetags
. - It's different from
input
since the template creates a faux select field with a<div>
rather than using the native<select>
. Since there can be many select fields in a given parent template, I introduce an identifieridx
to explicitly segregate fields.
- Custom template tag from the "pages" app. See
-
{% include '_msg.html' ... %}
- messages as alertsThis adopts the architecure for messages in django-fragments'
msg.html
to create a global alerts center so that hyperscripted-notifications can be added after an htmx swap.Invocation - Custom template tag from the "pages" app. See
src/pages/templatetags
. - Assuming template fragment of
HttpResponse
contains aform
, I can isolatenon_field_errors
to render them as messages.
- Custom template tag from the "pages" app. See