Everything broken...
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
# Tailwind Dashed Border Plugin: Technical Description
|
||||
|
||||
This plugin provides a small set of Tailwind CSS utilities for drawing configurable dashed borders with an SVG `<rect>` overlay instead of using the native CSS `border-style: dashed`. The SVG approach makes dash length, gap, offset, stroke width, color, and rounded corners independently controllable.
|
||||
|
||||
## What the plugin does
|
||||
|
||||
The plugin defines:
|
||||
|
||||
- Base CSS custom properties for dashed-border configuration.
|
||||
- Structural utilities for the container, SVG overlay, and SVG rectangle.
|
||||
- Generated utility families for gap, dash length, offset, stroke width, stroke color, and border radius.
|
||||
- Transition helpers so dashed-border visuals can animate consistently with Tailwind transition utilities.
|
||||
|
||||
The plugin expects the dashed border to be rendered by an element structure like this:
|
||||
|
||||
```html
|
||||
<div class="dashed-border dashed-border-gap-2 dashed-border-length-4 dashed-border-width-2">
|
||||
<svg class="dashed-border-svg" aria-hidden="true">
|
||||
<rect class="dashed-border-rect" />
|
||||
</svg>
|
||||
|
||||
<!-- content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
## Core structural utilities
|
||||
|
||||
### `.dashed-border`
|
||||
|
||||
This is the root utility for any element using the plugin. It:
|
||||
|
||||
- Sets default CSS variables.
|
||||
- Applies `position: relative` so the SVG overlay can be absolutely positioned.
|
||||
|
||||
Default values:
|
||||
|
||||
- `--tw-dashed-border-gap`: `calc(var(--spacing, 0.25rem) * 2)`
|
||||
- `--tw-dashed-border-length`: `calc(var(--spacing, 0.25rem) * 3)`
|
||||
- `--tw-dashed-border-offset`: `0px`
|
||||
- `--tw-dashed-border-width`: `1px`
|
||||
- `--tw-dashed-border-color`: `currentColor`
|
||||
- `--tw-dashed-border-radius`: `0px`
|
||||
|
||||
### `.dashed-border-svg`
|
||||
|
||||
This utility is intended for the SVG overlay element. It:
|
||||
|
||||
- Makes the SVG fill the container with `position: absolute` and `inset: 0`.
|
||||
- Sets `width: 100%` and `height: 100%`.
|
||||
- Uses `overflow: hidden`.
|
||||
- Uses `pointer-events: none` so the overlay does not block interaction.
|
||||
|
||||
### `.dashed-border-rect`
|
||||
|
||||
This utility is intended for the SVG `<rect>` that draws the actual border. It:
|
||||
|
||||
- Removes fill with `fill: none`.
|
||||
- Uses `stroke` for the border color.
|
||||
- Uses `stroke-dasharray` for dash length and gap.
|
||||
- Uses `stroke-dashoffset` for dash phase shifting.
|
||||
- Uses `vector-effect: non-scaling-stroke` to keep stroke width stable.
|
||||
- Positions the rectangle inset by half the stroke width, so the stroke remains visually inside the element bounds.
|
||||
- Uses `rx` to simulate rounded corners.
|
||||
|
||||
Computed shape rules:
|
||||
|
||||
- `x` / `y`: `calc(var(--tw-dashed-border-width) / 2)`
|
||||
- `width` / `height`: `calc(100% - var(--tw-dashed-border-width))`
|
||||
- `rx`: `max(0px, calc(var(--tw-dashed-border-radius) - (var(--tw-dashed-border-width) / 2)))`
|
||||
|
||||
Dash pattern:
|
||||
|
||||
```css
|
||||
stroke-dasharray: <length> <gap>;
|
||||
```
|
||||
|
||||
## Registered custom properties
|
||||
|
||||
The plugin registers these `@property` definitions:
|
||||
|
||||
- `--tw-dashed-border-gap`
|
||||
- `--tw-dashed-border-length`
|
||||
- `--tw-dashed-border-offset`
|
||||
- `--tw-dashed-border-width`
|
||||
- `--tw-dashed-border-radius`
|
||||
|
||||
These registrations provide typed CSS custom properties so transitions and interpolation behave more predictably in supporting browsers.
|
||||
|
||||
## Generated utility families
|
||||
|
||||
### Gap utilities
|
||||
|
||||
Class shape:
|
||||
|
||||
```text
|
||||
dashed-border-gap-*
|
||||
```
|
||||
|
||||
Source values:
|
||||
|
||||
- Tailwind `theme.spacing`
|
||||
|
||||
Effect:
|
||||
|
||||
- Sets `--tw-dashed-border-gap`
|
||||
- Recomputes `stroke-dasharray`
|
||||
|
||||
Example:
|
||||
|
||||
```html
|
||||
<div class="dashed-border dashed-border-gap-4">
|
||||
```
|
||||
|
||||
### Dash length utilities
|
||||
|
||||
Class shape:
|
||||
|
||||
```text
|
||||
dashed-border-length-*
|
||||
```
|
||||
|
||||
Source values:
|
||||
|
||||
- Tailwind `theme.spacing`
|
||||
|
||||
Effect:
|
||||
|
||||
- Sets `--tw-dashed-border-length`
|
||||
- Recomputes `stroke-dasharray`
|
||||
|
||||
Example:
|
||||
|
||||
```html
|
||||
<div class="dashed-border dashed-border-length-6">
|
||||
```
|
||||
|
||||
### Dash offset utilities
|
||||
|
||||
Class shape:
|
||||
|
||||
```text
|
||||
dashed-border-offset-*
|
||||
```
|
||||
|
||||
Source values:
|
||||
|
||||
- Tailwind `theme.spacing`
|
||||
|
||||
Effect:
|
||||
|
||||
- Sets `--tw-dashed-border-offset`
|
||||
- Updates `stroke-dashoffset`
|
||||
|
||||
Example:
|
||||
|
||||
```html
|
||||
<div class="dashed-border dashed-border-offset-2">
|
||||
```
|
||||
|
||||
### Stroke width utilities
|
||||
|
||||
Class shape:
|
||||
|
||||
```text
|
||||
dashed-border-width-*
|
||||
```
|
||||
|
||||
Source values:
|
||||
|
||||
- Tailwind `theme.borderWidth`
|
||||
|
||||
Effect:
|
||||
|
||||
- Sets `--tw-dashed-border-width`
|
||||
- Updates `stroke-width`
|
||||
- Recomputes `x`, `y`, `width`, `height`, and `rx`
|
||||
|
||||
Example:
|
||||
|
||||
```html
|
||||
<div class="dashed-border dashed-border-width-2">
|
||||
```
|
||||
|
||||
### Stroke color utilities
|
||||
|
||||
Class shape:
|
||||
|
||||
```text
|
||||
dashed-border-color-*
|
||||
```
|
||||
|
||||
Source values:
|
||||
|
||||
- Flattened Tailwind `theme.colors`
|
||||
- Plus explicit aliases:
|
||||
- `dashed-border-color-current`
|
||||
- `dashed-border-color-inherit`
|
||||
- `dashed-border-color-transparent`
|
||||
|
||||
Effect:
|
||||
|
||||
- Sets `--tw-dashed-border-color`
|
||||
- Updates SVG `stroke`
|
||||
|
||||
Example:
|
||||
|
||||
```html
|
||||
<div class="dashed-border dashed-border-color-slate-500">
|
||||
```
|
||||
|
||||
Nested Tailwind colors are flattened with dash-separated keys, so a theme color like `slate.500` becomes:
|
||||
|
||||
```text
|
||||
dashed-border-color-slate-500
|
||||
```
|
||||
|
||||
### Radius utilities
|
||||
|
||||
Class shape:
|
||||
|
||||
```text
|
||||
dashed-border.rounded-*
|
||||
```
|
||||
|
||||
These are not generated as `dashed-border-radius-*`. Instead, the plugin creates compound selectors that combine `.dashed-border` with Tailwind rounded classes.
|
||||
|
||||
Examples of generated selectors:
|
||||
|
||||
- `.dashed-border.rounded`
|
||||
- `.dashed-border.rounded-sm`
|
||||
- `.dashed-border.rounded-lg`
|
||||
- `.dashed-border.rounded-full`
|
||||
|
||||
Source values:
|
||||
|
||||
- Tailwind `theme.borderRadius`
|
||||
|
||||
Effect:
|
||||
|
||||
- Sets `--tw-dashed-border-radius`
|
||||
- Lets the SVG rectangle inherit matching rounded-corner behavior through its computed `rx`
|
||||
|
||||
Example:
|
||||
|
||||
```html
|
||||
<div class="dashed-border rounded-lg">
|
||||
```
|
||||
|
||||
This means the expected way to apply radius is:
|
||||
|
||||
```html
|
||||
<div class="dashed-border rounded-xl">
|
||||
```
|
||||
|
||||
not:
|
||||
|
||||
```html
|
||||
<div class="dashed-border-radius-xl">
|
||||
```
|
||||
|
||||
## Transition behavior
|
||||
|
||||
The plugin adds transition helpers for dashed-border visuals.
|
||||
|
||||
Supported patterns:
|
||||
|
||||
- `.dashed-border-transition`
|
||||
- `.dashed-border.transition`
|
||||
- `.dashed-border.transition-all`
|
||||
- `.dashed-border.transition-colors`
|
||||
|
||||
Behavior:
|
||||
|
||||
- Inherits Tailwind-style transition duration and timing defaults.
|
||||
- Applies transitions to the inner `.dashed-border-rect`.
|
||||
- `transition` / `transition-all` animate:
|
||||
- `stroke-dasharray`
|
||||
- `stroke-dashoffset`
|
||||
- `stroke-width`
|
||||
- `stroke`
|
||||
- `rx`
|
||||
- `transition-colors` animates only `stroke`
|
||||
|
||||
Example:
|
||||
|
||||
```html
|
||||
<div class="dashed-border rounded-lg transition-all dashed-border-color-slate-400 hover:dashed-border-color-slate-900">
|
||||
```
|
||||
|
||||
## Expected visual model
|
||||
|
||||
When used correctly, the utilities should produce:
|
||||
|
||||
- A container element that establishes the positioning context.
|
||||
- A full-size SVG overlay sitting on top of the container.
|
||||
- A `<rect>` inset by half the stroke width so the line is not clipped.
|
||||
- A dashed stroke whose dash length and gap are independently tunable.
|
||||
- Rounded corners that visually track Tailwind `rounded-*` utilities.
|
||||
- Hover, or others transition triggers should be embedded from main tailwind flow. Use their duration and animation function.
|
||||
|
||||
## Implementation notes
|
||||
|
||||
- The plugin flattens nested color objects from `theme.colors` into dash-separated utility suffixes.
|
||||
- `DEFAULT` keys are handled specially so default theme values map cleanly to `rounded` and other default-style selectors.
|
||||
- Radius is adjusted by subtracting half the stroke width, which keeps the visible curve aligned with the outer element radius.
|
||||
- The plugin is built around Tailwind CSS v4 plugin APIs: `addBase`, `addUtilities`, and `matchUtilities`.
|
||||
+46
-29
@@ -11,11 +11,14 @@ const DEFAULT_BORDER_WIDTH = "1px";
|
||||
const DEFAULT_OFFSET = "0px";
|
||||
const DEFAULT_COLOR = "currentColor";
|
||||
const DEFAULT_RADIUS = "0px";
|
||||
const DEFAULT_ALIGN = "calc(var(--tw-dashed-border-width) / 2)";
|
||||
const dashedBorderAlign = "var(--tw-dashed-border-align)";
|
||||
const svgRadius = `max(0px, calc(var(--tw-dashed-border-radius) - (${dashedBorderAlign})))`;
|
||||
const svgRadius =
|
||||
"max(0px, calc(var(--tw-dashed-border-radius) - (var(--tw-dashed-border-width) / 2)))";
|
||||
const dashedBorderTransitionProperties =
|
||||
"stroke-dasharray, stroke-dashoffset, stroke-width, stroke, rx, x, y, width, height";
|
||||
"stroke-dasharray, stroke-dashoffset, stroke-width, stroke, rx";
|
||||
const strokeDasharrayValue = (
|
||||
length: string,
|
||||
gap: string,
|
||||
): string => `${length} max(${gap}, 0.001px)`;
|
||||
|
||||
const flattenThemeMap = (
|
||||
input: ThemeValueMap,
|
||||
@@ -40,11 +43,6 @@ const flattenThemeMap = (
|
||||
const roundedSelector = (name: string): string =>
|
||||
name === "DEFAULT" ? ".rounded" : `.rounded-${name}`;
|
||||
|
||||
const resolveAlignValue = (value: string): string =>
|
||||
/^-?(?:\d+|\d*\.\d+)$/.test(value)
|
||||
? `calc((0.5 - ${value}) * var(--tw-dashed-border-width))`
|
||||
: `calc((var(--tw-dashed-border-width) / 2) - (${value}))`;
|
||||
|
||||
const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
||||
({ addBase, addUtilities, matchUtilities, theme }) => {
|
||||
addBase({
|
||||
@@ -68,11 +66,6 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
||||
inherits: "true",
|
||||
"initial-value": DEFAULT_BORDER_WIDTH,
|
||||
},
|
||||
"@property --tw-dashed-border-align": {
|
||||
syntax: "<length-percentage>",
|
||||
inherits: "true",
|
||||
"initial-value": DEFAULT_ALIGN,
|
||||
},
|
||||
"@property --tw-dashed-border-radius": {
|
||||
syntax: "<length-percentage>",
|
||||
inherits: "true",
|
||||
@@ -87,7 +80,6 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
||||
"--tw-dashed-border-offset": DEFAULT_OFFSET,
|
||||
"--tw-dashed-border-width": DEFAULT_BORDER_WIDTH,
|
||||
"--tw-dashed-border-color": DEFAULT_COLOR,
|
||||
"--tw-dashed-border-align": DEFAULT_ALIGN,
|
||||
"--tw-dashed-border-radius": DEFAULT_RADIUS,
|
||||
position: "relative",
|
||||
},
|
||||
@@ -104,15 +96,17 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
||||
fill: "none",
|
||||
stroke: "var(--tw-dashed-border-color)",
|
||||
strokeWidth: "var(--tw-dashed-border-width)",
|
||||
strokeDasharray:
|
||||
"var(--tw-dashed-border-length) var(--tw-dashed-border-gap)",
|
||||
strokeDasharray: strokeDasharrayValue(
|
||||
"var(--tw-dashed-border-length)",
|
||||
"var(--tw-dashed-border-gap)",
|
||||
),
|
||||
strokeDashoffset: "var(--tw-dashed-border-offset)",
|
||||
shapeRendering: "geometricPrecision",
|
||||
vectorEffect: "non-scaling-stroke",
|
||||
x: dashedBorderAlign,
|
||||
y: dashedBorderAlign,
|
||||
width: `calc(100% - (${dashedBorderAlign} * 2))`,
|
||||
height: `calc(100% - (${dashedBorderAlign} * 2))`,
|
||||
x: "calc(var(--tw-dashed-border-width) / 2)",
|
||||
y: "calc(var(--tw-dashed-border-width) / 2)",
|
||||
width: "calc(100% - var(--tw-dashed-border-width))",
|
||||
height: "calc(100% - var(--tw-dashed-border-width))",
|
||||
rx: svgRadius,
|
||||
},
|
||||
".dashed-border-transition, .dashed-border.transition, .dashed-border.transition-all":
|
||||
@@ -139,6 +133,7 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
||||
},
|
||||
});
|
||||
|
||||
const spacing = theme("spacing", {}) as Record<string, string>;
|
||||
const borderWidths = theme("borderWidth", {
|
||||
DEFAULT: DEFAULT_BORDER_WIDTH,
|
||||
}) as Record<string, string>;
|
||||
@@ -151,26 +146,37 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
||||
{
|
||||
"dashed-border-gap": (value: string) => ({
|
||||
"--tw-dashed-border-gap": value,
|
||||
"& .dashed-border-rect": {
|
||||
strokeDasharray: strokeDasharrayValue(
|
||||
"var(--tw-dashed-border-length)",
|
||||
value,
|
||||
),
|
||||
},
|
||||
}),
|
||||
"dashed-border-length": (value: string) => ({
|
||||
"--tw-dashed-border-length": value,
|
||||
"& .dashed-border-rect": {
|
||||
strokeDasharray: strokeDasharrayValue(
|
||||
value,
|
||||
"var(--tw-dashed-border-gap)",
|
||||
),
|
||||
},
|
||||
}),
|
||||
"dashed-border-offset": (value: string) => ({
|
||||
"--tw-dashed-border-offset": value,
|
||||
}),
|
||||
"dashed-border-align": (value: string) => ({
|
||||
"--tw-dashed-border-align": resolveAlignValue(value),
|
||||
"& .dashed-border-rect": {
|
||||
strokeDashoffset: value,
|
||||
},
|
||||
}),
|
||||
"dashed-border-radius": (value: string) => ({
|
||||
"--tw-dashed-border-radius": value,
|
||||
"& .dashed-border-rect": {
|
||||
rx: `max(0px, calc(${value} - (var(--tw-dashed-border-width) / 2)))`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
values: {
|
||||
inside: DEFAULT_ALIGN,
|
||||
edge: "0px",
|
||||
outside: "calc(var(--tw-dashed-border-width) / -2)",
|
||||
},
|
||||
values: spacing,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -178,6 +184,14 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
||||
{
|
||||
"dashed-border-width": (value: string) => ({
|
||||
"--tw-dashed-border-width": value,
|
||||
"& .dashed-border-rect": {
|
||||
strokeWidth: value,
|
||||
x: `calc(${value} / 2)`,
|
||||
y: `calc(${value} / 2)`,
|
||||
width: `calc(100% - ${value})`,
|
||||
height: `calc(100% - ${value})`,
|
||||
rx: `max(0px, calc(var(--tw-dashed-border-radius) - (${value} / 2)))`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -189,6 +203,9 @@ const dashedBorderPlugin: ReturnType<typeof plugin> = plugin(
|
||||
{
|
||||
"dashed-border-color": (value: string) => ({
|
||||
"--tw-dashed-border-color": value,
|
||||
"& .dashed-border-rect": {
|
||||
stroke: value,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user