1. Principles

Jellyfy Design System

1. Principles

Jellyfy is a soft control material. A control looks poured, not painted: it is translucent, it carries a fine grain, and its edge is thick enough to catch light.

Depth has three separate jobs.

  • Thickness is the inset depth of the material. It says what kind of thing the object is.
  • Elevation is the drop shadow. It says how far the object sits off the page.
  • Press light is the pair of radial washes that appear while a control is pressed, or while a field is focused. It says the object is being acted on.

Color is quiet on surfaces and fields, and stronger on controls. That difference is the hierarchy. A custom theme changes the solids. It does not flatten the hierarchy.

The typeface belongs to the app. Jellyfy inherits it.

Small marks use a shallower inset than buttons, because the button inset collapses at checkbox and handle size. The radio bullet is jelly with the grain. Its color is at the material opacity. It has no blur and no thickness.

Implementation notes

Jellyfy is a CSS component library. It is inspired by Tailwind and daisyUI. It does not require Tailwind. It stays compatible with Tailwind by reading Tailwind color and spacing variables and supplying hex and length fallbacks when those variables are absent.

2. Roles

Every object uses one role. Custom components use the same roles. Size and role are different axes: a large input is still a field.

RoleWhat it isThicknessColor strengthRadius
SurfaceA container: card, alert, modal panel, badgeThinSurface tone. Wash with a slot.Surface
FieldA place to type or choose: input, textarea, selectMidSurface tone. Wash with a slot.Field
ControlA press that takes an action: button, icon buttonThickSurface tone. Control fill with a slot.Control
MarkA small indicator: checkbox box, radio ring, toggle track, toggle thumb, range track, range thumbMarkMark color. Control fill when selected or filled.Control. The checkbox box uses the mark radius
OverlayModal panel, select menuThinModal panel uses the surface fill. Select menu uses the menu tone.Surface

A modal panel follows the surface fill rules. A select menu uses the menu tone for its panel. A hovered option and a selected option mix the menu mix into that menu tone. A slot's menu mix is its solid. A select with no slot uses black in light and white in dark.

The bump button, used to close a modal, is not a fifth thickness. It is a transparent control with the bump light, no fill, and no drop shadow.

A custom component picks a role the way it picks the squish behavior. The thickness classes are thickness-surface, thickness-field, thickness-control, and thickness-mark. Squish is one utility, because it is one behavior. noise adds the grain alone, the same way.

Implementation notes

The thickness classes are thickness-surface, thickness-field, thickness-control, and thickness-mark. The squish class is squish. The noise class is noise. Size classes are sm, md, and lg. Medium is also the size with no size class.

Light

Surface
Field
Control
Mark

Dark

Surface
Field
Control
Mark

3. Color

Slots

Eight slots. Four are the theme, four are status.

SlotDefault solidFallback
PrimaryTW Indigo 500 in both themes#6366f1
SecondaryTW Pink 600#db2777
TertiaryTW Teal 600#0d9488
NeutralNeutral 500 in both themes#737373
SuccessTW Green 600#16a34a
WarningTW Amber 600#d97706
ErrorTW Red 600#dc2626
InfoTW Sky 600#0284c7

The neutral ramp is Jellyfy's, from 50 through 950, even where a component uses only some steps. It reads Tailwind's neutral variables when they exist, and uses these hex fallbacks when they do not.

StepFallback
50#fafafa
100#f5f5f5
200#e5e5e5
300#d4d4d4
400#a3a3a3
500#737373
600#525252
700#404040
800#262626
900#171717
950#0a0a0a

An object with no slot uses the base tones. A slot replaces the solid. The user applies a slot, including Error, with their own logic. Error does not add a second invalid style.

Every slot, including Neutral, uses the same wash, control fill, content color, press light, and menu mixes. A component with no slot uses those same mixes, with the surface tone as its solid. Its menu mix is black in light and white in dark. No slot has a mix of its own.

Base tones

ToneStep
Surface fillNeutral 50
MenuNeutral 100
Plain contentNeutral 800
Disabled fillNeutral 500 at 60%
Disabled textNeutral 800 at 70%
Shadow colorNeutral 950

Black and white are shared values, stored once and reused by every mix that needs them. They are not color slots.

Content color

Each slot has a content color, stored explicitly. Replacing a solid does not recalculate the content color. A custom theme sets both.

The content color is the text on a control. Primary is Indigo 500 in both themes. The other solids stay the same in both themes. Light text is the 950 step, because the page behind the button is light and the fill turns pale. Dark text is the 100 step, because the page behind the button is dark and the fill turns dark. The hex fallback is that same step.

SlotLightDark
PrimaryTW Indigo 950TW Indigo 100
SecondaryTW Pink 950TW Pink 100
TertiaryTW Teal 950TW Teal 100
NeutralNeutral 950Neutral 100
SuccessTW Green 950TW Green 100
WarningTW Amber 950TW Amber 100
ErrorTW Red 950TW Red 100
InfoTW Sky 950TW Sky 100

Text on a control is the content color. Text on a slotted surface or field is the surface content color. With no slot, that text is the plain content color. Plain content is Neutral 800 in light and Neutral 100 in dark.

Surface content color

SlotLightDark
PrimaryTW Indigo 800TW Indigo 200
SecondaryTW Pink 800TW Pink 200
TertiaryTW Teal 800TW Teal 200
NeutralNeutral 800Neutral 200
SuccessTW Green 800TW Green 200
WarningTW Amber 800TW Amber 200
ErrorTW Red 800TW Red 200
InfoTW Sky 800TW Sky 200

Icons use the text color of the component they sit in. An icon on a slotted surface uses the surface content color. An icon on a field uses the field text color. An icon on a control uses the control content color.

Text on a control meets WCAG AA, 4.5:1, against the painted fill. Surface content text meets that ratio against the wash. The focus outline meets 3:1 against the ground next to it. A content color that cannot reach 4.5:1 is set explicitly on that slot.

Color strength

Strength is separate from the solid. The same solid is quiet on a surface or field and firm on a control.

ApplicationMix
Surface toneNeutral 50 in light, Neutral 800 in dark. A plain surface runs this through the resting wash.
Mark colorPlain mixed with 28% white. The same mix in both themes.
Wash, restingFields: 86% plain and 14% solid in light, 76% and 24% in dark. Surfaces: 94% plain and 6% solid in light, 88% and 12% in dark.
Wash, focusedFields: 78% plain and 22% solid in light, 68% and 32% in dark.
Control fillThe solid, with no plain mixed in.

Every jelly fill is painted at the material opacity, with the shared blur and the shared grain. Surfaces and fields use the wash. A focused field uses the focused wash, which mixes in more of the solid. With no slot, the solid is plain, so the wash stays plain. An empty checkbox box, an empty radio ring, an off toggle track, and a range track use the mark color. Buttons, icon buttons, badges, selected checkbox boxes, selected toggle tracks, range fills, and colored range thumbs use the control fill.

Menu mixes

These mixes are internal. They are not part of the customization contract.

StateMix into the menu tone
Hovered or focused option14% of the menu mix
Selected option28% of the menu mix

A slot's menu mix is its solid. A select with no slot uses black in light and white in dark.

Option text stays the plain content color.

Implementation notes

Slot classes are primary, secondary, tertiary, neutral, success, warning, error, and info. They set the solid, the content color, and the surface content color. Content colors are paired variables, named with the daisyUI suffix content (--color-primary-content, and the same for the other slots). Surface content colors use the suffix surface-content (--color-primary-surface-content, and the same for the other slots).

With Tailwind, configure all three color tokens for each slot in @theme, or @theme inline when referencing other variables. Configure non-color options on :root. Without Tailwind, configure both colors and non-color options on :root. Public color and material defaults belong to the theme layer with zero selector specificity so consumer settings take priority.

Plain content is --color-plain-content. The menu fill is --color-menu. The mark color is --color-mark. It mixes --color-white into --color-plain at --percent-mark-lift.

Defaults read Tailwind color variables first: --color-primary, --color-indigo-500, --color-indigo-400, --color-indigo-100, --color-neutral-50, and the rest of the ramp. The fallbacks above apply when Tailwind is absent.

Black and white are --color-black and --color-white. Wash, press light, bump light, and loading light read those variables.

Wash percentages, wash opacity, and the menu mixes are variables.

Primary
Secondary
Tertiary
Neutral
Success
Warning
Error
Info
Plain
Wash
Control
Selected
50
100
200
300
400
500
600
700
800
900
950

4. Material

Every part of a checkbox, radio, switch, and range slider is jelly. The radio bullet keeps the grain. Its color is at the material opacity. It has no blur and no thickness. Every other part has the grain, the blur, and its thickness.

Grain

The grain is fractal noise, tiled. The tile is 22.5rem. The light theme uses a dark grain. The dark theme uses a lighter, fainter grain so the same texture stays visible on a dark ground. The turbulence is the same in both.

.noise paints that grain on any other element. It sets the image and the tile size. It does not add a fill, a blur, or a thickness.

Opacity and blur

Material opacity is 60% for every object. Blur is 4px for every object. Both are single values. Components do not invent their own.

The wash percentages are color mixes. They are not a second opacity.

Thickness

Thickness is two inset shadows. The first falls upward, so the bottom edge reads as the thick part of the pour. The second is a light lip in the other direction. Its offset is smaller, and its blur matches the thick edge. Those distances are the same in both themes.

The thick edge is Neutral 950. Dark uses a stronger opacity. The button's dark thick edge is Neutral 900 at 65%. The lip is Neutral 50.

ThicknessThick edgeLipLightDarkUsed by
Surfaceinset 0 -6px 8pxinset 0 4px 8pxEdge 12%, lip 10%Edge 32%, lip 8%Card, alert, modal panel, badge, select menu
Field, restinginset 0 -10px 16pxinset 0 5px 16pxEdge 20%, lip 10%Edge 50%, lip 10%Input, textarea, select
Field, focusedinset 0 -6px 10pxinset 0 3px 10pxEdge 15%, lip 10%Edge 45%, lip 10%Input, textarea, select while focused
Controlinset 0 -16px 16pxinset 0 8px 16pxEdge 50%, lip 10%Edge 65% in Neutral 900, lip 12%Button, icon button
Markinset 0 -2px 5pxinset 0 1px 5pxEdge 35%, lip 10%Edge 65%, lip 10%Checkbox box, radio ring, toggle track, toggle thumb, range track, range fill, range thumb

The track and the fill of a range slider are both jelly. Only the color differs. The same is true of a switch track and thumb, and of a radio ring.

Press light

Press light is a pair of soft ellipses. The lower ellipse is a translucent white highlight; the upper ellipse is a translucent black shade. They blend with the face fill rather than deriving their colors from the slot solid. Their strengths are tuned separately for light and dark themes. They are not the loading sheen, which still derives its tones from the solid.

Control light is hidden at rest and fully visible while the control is pressed. The transition is 300ms. Both ellipses are centered and fade out at 60% of the gradient radius.

EllipseLightDarkPlace
HighlightWhite at 22% opacityWhite at 12% opacity90% of the width, 60% tall, starts at 60% of the height, shifted up by 40% of its own height
ShadeBlack at 30% opacityBlack at 30% opacity90% of the width, 50% tall, starts at 42% of the height, shifted up by 75% of its own height

Field light uses a different strength, because a field is a recess. It is hidden at rest and fully visible while the field is focused, including when focus arrives by click. The transition is 300ms.

EllipseLightDarkPlace
HighlightWhite at 28% opacityWhite at 4% opacity70% of the width, 1.75rem tall, 1rem below the bottom edge, shifted up by 40% of its own height
ShadeBlack at 14% opacityBlack at 16% opacity90% of the width, 2.1rem tall, 1.4rem from the top, shifted up by 75% of its own height

Enabled controls use the same neutral lighting with or without a color slot. Changing only background-color changes the fill; the highlight and shade remain neutral overlays.

Bump

The bump button is always lit. It uses its own highlight and shade strengths, not the control press light. Each ellipse is 87.5% of the button width and 50% of its height, centered horizontally, and fades out at 70% of the gradient radius. While pressed, the two ellipses exchange places. The exchange takes 200ms.

EllipseLightDarkPlace
HighlightWhite at 42% opacityWhite at 12% opacity7.5% from the top
ShadeBlack at 25% opacityBlack at 25% opacity42.5% from the top

Implementation notes

Light grain: an SVG turbulence filter, baseFrequency 1.34, 4 octaves, stitched, drawn at 20% opacity. Dark grain: the same filter, white, drawn at 6% opacity. Tile size is --noise-size: 22.5rem.

Material opacity is one variable. Blur is backdrop-filter: blur(4px).

Thickness, control light, field light, and bump light are variables. The dark theme has its own inset-shadow opacity. The inset distances stay the same. Press-light and bump strengths are not customization tokens.

Light

Surface
Field
Field focused
Control
Mark
Plain pressed
Plain focused
Plain bump
Primary pressed
Primary focused
Primary bump

Dark

Surface
Field
Field focused
Control
Mark
Plain pressed
Plain focused
Plain bump
Primary pressed
Primary focused
Primary bump

5. Elevation

Elevation is the drop shadow. It is independent from thickness. The shadow color is Neutral 950. The default theme has one multiplier, 1, which scales every shadow together. Rest stays quieter than hover. The multiplier does not let those two cross.

LevelShadowWhere it is used
Rest0 12px 16px -14px, Neutral 950 at 55%Buttons, fields, cards, alerts
Hover0 16px 36px -14px, Neutral 950 at 55%Buttons and icon buttons while hovered
Mark0 4px 6px -3px, Neutral 950 at 45%Marks
OverlayThe hover shadowModal panel, select menu
Disabled0 12px 12px -12px, Neutral 950 at 25%Disabled buttons and fields

Overlay is its own level. In the default theme it uses the same shadow as hover.

A pressed control keeps the shadow it already has. The press is the press light and the squish.

Buttons and icon buttons also rise 2px on hover. The rise takes 200ms, eased out. A disabled control does not rise.

Cards, alerts, and fields do not rise on hover.

Focus adds the focus gap and keeps the elevation the object already has.

Implementation notes

Rest, hover, mark, disabled, and overlay are separate variables. Overlay's default value is the hover shadow.

Light

Rest
Hover
Mark
Overlay
Disabled

Dark

Rest
Hover
Mark
Overlay
Disabled

6. Shape

Four radius tokens. The names are roles. A product can change the values.

TokenDefaultUsed by
Surface1remCard, alert, modal panel, select menu
Field1remInput, textarea, select
Control999pxButton, icon button, toggle track, toggle thumb, range track, range thumb, radio ring, radio bullet
Mark0.5remCheckbox box

Field and surface both default to 1rem. They stay separate so a field inside a card can be tightened without changing the card. A nested corner should be smaller than the corner that contains it.

The control value is a capsule. Lowering it makes buttons, switches, tracks, and handles more rectangular together. There is no square class and no circle class. An icon button becomes a circle when the control radius is at least half its height, and a rounded rectangle when it is less.

The checkbox box uses the mark radius at every size.

Select options use a 0.65rem corner. That is inside the menu, so it is tighter than the 1rem surface radius.

Implementation notes

The variables are --radius-surface, --radius-field, --radius-control, and --radius-mark.

Light

Surface
Field
Control
Mark

Dark

Surface
Field
Control
Mark

7. Typography

Jellyfy does not ship a typeface and does not set one. Text, icons, and form controls inherit the app font.

Weight, case, and size are still specified, because they are part of the material.

TextSize, mediumWeightCase and tracking
Button label0.875rem600Uppercase, 0.04em
Icon button icon1.5rem——
Field value1remInherited—
Field label, resting in an empty field1rem400Current color at --percent-label-resting
Field label, floated10px5000.04em
Checkbox, radio, and toggle label0.875rem500—
Range label0.875rem500—
Message0.75remInherited—
Alert and card1remInheritedLine height 1.5
Badge10px600Uppercase, 0.04em

Line height is 1 on buttons and floated labels, and 1.5 on field values, resting labels, alerts, and cards.

Implementation notes

Do not set font-family on the library root. Docs may set a font on the docs page. Components use font: inherit.

There is no font token in the customization contract.

Icon button icon

Button label

Save

Field value

Jane Cooper

Field label, resting

Name

Field label, floated

Name

Checkbox, radio, and toggle label

Remember me

Range label

Volume

Message

This field is required

Alert and card

The pour stays quiet until you press it.

Badge

New

8. Spacing

Interior spacing uses one unit. The default unit is 0.25rem. When Tailwind is present, the unit is Tailwind's spacing variable, so the library follows the app's spacing scale.

Component padding, gaps, and icon insets in the component section are multiples of that unit. The medium button is 12 units tall. The medium field label sits 3 units from the top when floated.

Jellyfy does not define a page grid.

Implementation notes

Read --spacing and fall back to 0.25rem. Author component lengths as multiples of that variable so an app that changes the spacing scale changes Jellyfy with it.

1 unit tall
12 units tall
Name
3 units from the top

9. Motion

Motion is feedback. It is short, it eases out of the gesture, and it turns off when the person prefers reduced motion. Reduced motion removes squish, loading lights, and state transitions.

Squish

Squish is the press deformation. It is measured in pixels so a radio and a wide button move by the same physical amount.

The pointer triggers it. Keyboard focus does not. A disabled control does not squish.

The press lasts 480ms. It has three shapes. Release lasts 300ms and returns from the scale already on screen to the normal size.

PoseWidthHeight
NormalThe control's own sizeThe control's own size
Peak, at 42% of the press8px wider2/7 of 8px shorter, 16/7px
Settle, held while the pointer is down1/7 of 8px wider, 8/7px2/7 of 8px taller, 16/7px

The 8px is the total change on that axis, stored as one value. Scale is centered, so the peak adds 4px on the left and 4px on the right. The height uses a smaller distance because a control is shorter than it is wide: an equal pixel cut on the height would collapse it. That height ratio is 2/7 of the 8px. The settle width is 1/7 of the 8px. The settle height uses the same 2/7, outward. These three numbers are named constants.

The way to the peak eases out. The way from the peak to the settle eases in and out. Release eases out.

Squish applies to the button, the icon button, the bump button, the field, the checkbox box, the radio ring, the toggle track, and the range thumb. Any other element can opt in.

Hover rise

Buttons and icon buttons rise 2px on hover over 200ms, eased out, and return on press release or when disabled.

Label

The floating label moves and changes size over 300ms, eased out.

Loading lights

Loading light is derived from the solid. It is not the press light.

  • The light sheen is a light variation of the solid, shown at 40% opacity.
  • The shade sheen is a dark variation of the solid, shown at 40% opacity.

A button carries three sheens that sweep across it. Two are lights and one is a shade. Each sheen is a capsule, 30% of the button width and 40% of its height, blurred by 12px. It travels from fully off the left side to three widths past the right, vertically centered. The sweep is linear and repeats.

SheenDurationDelay
Light1.35s0
Shade1.65s0.36s
Light1.5s0.78s

Opacity along that sweep is 0 at the start, 0.55 at 8%, 0.30 at 55%, 0.42 at 78%, and 0 at the end.

An icon button keeps the icon the product placed inside it and shows three orbiting dots. The library does not supply a loading icon. The dots fade in over 400ms, eased out. Each dot is 0.875rem, blurred by 4px, and sits 0.125rem from the top edge of the button.

DotToneStart angleDurationOpacity
1Light0°1.35s0.15
2Shade130°2.2s0.10
3Light240°1.6s0.15

The orbit is linear and repeats. The light and shade tones use the loading mix above. The opacities stay as listed, so the orbit stays quieter than the button sweep.

Other transitions

Color, shadow, and outline changes take 300ms, eased out. The select chevron turns 180° over 200ms, eased out, when the list is open. The checkbox glyph and the radio bullet scale over 300ms, eased out.

Implementation notes

Squish runs on the painted face of the control, not on the label text and not on the invisible input. A press on the label of a checkbox, radio, switch, or range slider triggers that face. The opt-in class is squish. The named constants are the total width change 8, the height ratio 2/7, the settle-width ratio 1/7, the press 480ms, the release 300ms, and the peak at 0.42. Scale on an axis is 1 + pixels / size on that axis.

Loading light and loading shade are variables that read --color-white and --color-black.

Reduced motion is prefers-reduced-motion: reduce.

Light

Press and hold
Hover
Rises 2px
Loading sweep
Loading orbit

Dark

Press and hold
Hover
Rises 2px
Loading sweep
Loading orbit

10. Interaction states

States stack. A selected, focused control shows both treatments. Disabled wins over hover, press, and squish.

Rest

The role's fill, thickness, elevation, and content color. Press light is hidden. The bump button's lights stay visible.

Hover

Buttons and icon buttons use the hover elevation and rise 2px. Select options use the 14% menu mix. Other objects do not change on hover.

Press

Buttons and icon buttons show the control press light and squish. A field squishes and shows the field light: the radial highlight along the bottom and the shade along the top, at the field strengths. It does not use the control press light. The field light stays while the field is focused, including after a click. The bump button exchanges its highlight and shade. Keyboard activation does not squish.

Focus

The focus mark is a 1px outline in the object's solid, 2px outside the object, with a 2px gap. In the light theme the gap is white at 50%. In the dark theme the gap is Neutral 950 at 50%. The gap separates the outline from the jelly edge.

An object with no slot uses the plain content color for the outline.

Buttons, icon buttons, the bump button, marks, and the range thumb show this on keyboard focus only. Fields show it whenever they contain focus, including after a click, and they also show the field light and the focused thickness. The modal shell itself has no focus outline.

Selected

Selected means the value is on. It is not the press.

  • A checked checkbox draws its glyph and, when a slot is applied, fills the box with the control fill.
  • A checked radio shows its bullet.
  • An on toggle moves the thumb to the end of the track. A slotted track uses the control fill. The thumb becomes white.
  • A selected select option uses the 28% menu mix.

A normal button has no selected style.

Disabled

A disabled control replaces its fill with the disabled fill and its text with the disabled text. It does not keep its slot color. Buttons, icon buttons, and fields keep their own thickness and use the disabled elevation. Marks and the range keep the mark thickness and the mark elevation. The range track keeps its own color. The bump button has no fill. It fades to 60%, its icon uses the plain content color, and its lights use the plain tone. A disabled control loses the hover rise, and it does not squish or show press light. The cursor shows that it cannot be used.

Error

Error is the Error slot, applied by the product. It uses the same wash, content recipe, and field light as any other slot. The field and its message choose slots independently. A message may use Error while the field has no slot. A field may use Error while the message uses another slot.

Loading

A button or icon button with loading shows the loading lights from the motion section. The button keeps its fill, thickness, and label. The icon button keeps the icon the product placed inside it. The library does not add an icon.

Implementation notes

Keyboard focus is :focus-visible. Field focus is :focus-within. Disabled buttons are :disabled. Disabled fields are a disabled control inside the field. Selected checks are :checked. The select option selected style is :checked on the option.

The focus gap is a 2px spread shadow drawn before the thickness and the drop shadow. The outline is a 1px transparent outline that takes the solid color when focused, with a 2px offset.

Light

Rest
Rest
Hover
Hover
Press
Press
Focus
Focus
On
Selected
Off
Disabled
Error
Error
Load
Loading

Dark

Rest
Rest
Hover
Hover
Press
Press
Focus
Focus
On
Selected
Off
Disabled
Error
Error
Load
Loading

11. Themes

Supported browser minimums are Chrome and Edge 123, Firefox 120, and Safari / iOS Safari 17.5. Native light-dark() is required. Foreground defaults are declared once inside @supports (color: light-dark(white, black)) so CSS compilers preserve local theme selection. Compiler settings do not need to change.

The dark theme uses the same mixes, the same thickness distances, the same elevations, the same motion, and the same opacity and blur. Every slot, including Neutral, keeps those mixes. A plain element, one with no slot, keeps them too, and uses the surface tone as its solid.

Dark changes only the neutral step fed into a recipe, the content color step, the focus-gap tone, and the grain.

The Neutral slot is one of the eight colors. Both themes use Neutral 500 as its solid. It then uses the same wash, control fill, and content mix as primary.

The plain element uses plain as its solid, plus the menu tone and the plain content color. Plain is not a second color and not the Neutral slot.

Disabled is a state. The fade stays 60% for the fill and 70% for the text. Dark changes the ramp step that is faded.

The focus gap and the grain apply to every component, with a slot and without one.

TokenWhat it isLightDark
Neutral slotOne of the eight colorsNeutral 500Neutral 500
Surface tonePlain solid, run through the washNeutral 50Neutral 800
Menu tonePlain menu fillNeutral 100Neutral 800
Plain contentText on a surface or fieldNeutral 800Neutral 100
Disabled fillDisabled state, 60% fadeNeutral 500Neutral 700
Disabled textDisabled state, 70% fadeNeutral 800Neutral 400
Focus gapShared by every focus ringWhite at 50%Neutral 950 at 50%
GrainShared by every jelly surfaceDark, strongerLight, fainter

Implementation notes

color-scheme follows the theme so native controls match it.

Light

Neutral slot
Surface
Mark
Menu
Plain content
Disabled fill
Disabled text
Focus gap
Grain

Dark

Neutral slot
Surface
Mark
Menu
Plain content
Disabled fill
Disabled text
Focus gap
Grain

12. Components

Every component has three sizes: small, medium, and large. Medium is the default, with no size class, and md repeats that size so a breakpoint can return to it.

The component list is Button, Icon Button, Bump Button, Input, Textarea, Select, Checkbox, Radio, Toggle, Range, Alert, Badge, Card, Modal, and Message.

Button

A control. Control thickness, control radius, control fill when a slot is applied, rest elevation, control press light, squish, and the hover rise.

SmallMediumLarge
Height2.5rem3rem3.5rem
Inline padding1.25rem1.5rem2rem
Label size0.75rem0.875rem1rem
Content gap0.5rem0.5rem0.5rem

The width is the label plus the inline padding. There is no minimum width. The label is uppercase, weight 600, tracking 0.04em. Text has a 6px shadow at 40%. Dark text uses Neutral 50. Light text uses Neutral 950.

Icon Button

A button with no label. Its box is square. Width, height, and icon size:

SmallMediumLarge
Box2.5rem3rem3.5rem
Icon1.25rem1.5rem1.75rem

The shape comes from the control radius. There is no square variant and no circle variant. The icon uses the button's text color.

While loading, the icon stays and the orbit from the motion section is shown.

Bump Button

The close control on a modal. Transparent, no elevation, no radius, bump light. It sits 0.75rem from the top and the trailing edge of the panel.

SmallMediumLarge
Box1.5rem2rem2.5rem
Icon1rem1.25rem1.5rem

Its icon uses the text color of the surface it sits on.

Input

A field. Field thickness, field radius, field light while focused, squish. The value is transparent and inherits the field color. With no slot, the field text is the plain content color. With a slot, it is the surface content color.

SmallMediumLarge
Value size0.875rem1rem1.25rem
Padding, filled or focused1rem 0.75rem 0.5rem1.375rem 1rem 0.625rem1.75rem 1.25rem 0.75rem
Padding, empty and blurred0.75rem vertical1rem vertical1.25rem vertical

The label is a separate element. When the field is empty and not focused, the label sits in the value line at the value size, weight 400, in the current color at --percent-label-resting. When the field is focused or has a value, the label floats.

SmallMediumLarge
Floated label position0.375rem from the top, 0.75rem from the start0.75rem from the top, 1rem from the start0.875rem from the top, 1.25rem from the start
Floated label size0.625rem10px0.75rem

Start and end icons sit in the field and use the field's text color. They do not receive pointer events. The value padding and the label's start edge grow to clear a start icon.

SmallMediumLarge
Icon box0.875rem1rem1.25rem
Distance from the top0.9375rem1.25rem1.5625rem
Inset from the edge0.75rem1rem1.25rem
Value padding on that side2rem2.5rem3rem

Textarea

The same field as the input, including the floating label, the start icon, and the end icon. It grows vertically and does not grow horizontally. The empty resting state only reduces the top padding. The bottom padding stays.

SmallMediumLarge
Minimum height5rem7rem9rem
Padding, filled or focused1rem 0.75rem 0.75rem1.375rem 1rem 0.75rem1.75rem 1.25rem 1rem

Select

A field with a library chevron and a menu. The closed control matches the input's padding, label, text color, and focus light. The label stays floated, because a select always shows a value. The chevron is 1.25rem at medium, 1rem at small, and 1.5rem at large. It uses the field's text color, sits on the trailing edge, ignores pointer events, and turns 180° while the menu is open.

The menu is an overlay: menu tone, surface radius, surface thickness, overlay elevation. It opens 0.35rem below the field and pads 0.35rem. Options pad 0.55rem by 0.75rem, with a 0.65rem corner and 4px between them. Hover and selected mixes are the menu mixes from the color section.

The closed control reserves trailing space for the chevron: 2.25rem at small, 2.5rem at medium, 3rem at large.

Checkbox

A mark plus a label. The box uses mark thickness, the mark elevation, and the mark radius. The gap between box and label is 0.75rem at medium, 0.5rem at small, and 1rem at large. The box is the hit target: 24px at medium and 20px at small.

SmallMediumLarge
Box1.25rem1.5rem2rem
Label0.75rem0.875rem1rem

The empty box uses the mark color. The check is a masked copy of the Lucide check, in the current color, centered in the box. It is invisible and scaled to 0.4 when off, and fully visible at scale 1 when on. A slotted checked box uses the control fill and the control content color.

Radio

A mark plus a label. The ring uses the mark color. It is jelly: grain, blur, and mark thickness. It matches the checkbox sizes, including the 24px medium and 20px small hit target, and uses the control radius, so it is a circle in the default theme. The bullet is jelly with the grain. It has no blur and no thickness. It is 0.75rem, 0.875rem, and 1.25rem at small, medium, and large. It scales from 0 to 1 when selected. With no slot, the bullet is the plain content color at the material opacity. With a slot, the bullet is the control fill.

Toggle

A mark plus a label. The track and the thumb are both jelly: grain, blur, and mark thickness. Only the color differs. Both use the control radius.

SmallMediumLarge
Track2.5rem by 1.5rem3rem by 1.75rem3.5rem by 2rem
Thumb1rem1.25rem1.5rem
Thumb inset0.25rem0.25rem0.25rem
Travel when on1rem1.15rem1.5rem

The off track uses the mark color. The resting thumb is Neutral 500 mixed at 55% into white, at the material opacity. The on thumb is white at the material opacity. A slotted on track uses the control fill. The thumb moves over 300ms, eased out.

Range

A range slider. It is a mark. The track, the fill, and the thumb are jelly: grain, blur, and mark thickness. Only the color differs. The fill is clipped to the track. With no slot, the track uses the mark color and the fill uses the plain content color, both at the material opacity. A slot sets the fill to the control fill. The thumb uses the fill's color. All three use the control radius.

The track is the full width of the lane, so it lines up with the label. The thumb center travels from half a thumb in from the left to half a thumb in from the right. The fill ends at that center.

SmallMediumLarge
Lane height1.75rem2rem2.5rem
Track height0.5rem0.75rem1rem
Thumb1.25rem1.5rem2rem

The default lane is at most 24rem wide. Label size and the gap above the lane match the toggle.

Alert

A surface. Surface thickness, surface radius, rest elevation. The fill is the resting wash. With no slot, the solid is plain and the text is the plain content color. With a slot, the text is the surface content color. An optional icon is vertically centered on the first line and uses that text color.

SmallMediumLarge
Padding0.5rem 0.75rem0.75rem 1rem1rem 1.25rem
Gap0.5rem0.75rem1rem
Text0.875rem1rem1.25rem
Icon1rem1.25rem1.5rem

Badge

A compact surface. Surface thickness, mark elevation, control radius, so the default badge is a capsule. A slot uses the control fill and the control content color, not the wash. The label is uppercase, weight 600, tracking 0.04em.

SmallMediumLarge
Padding0.125rem 0.5rem0.25rem 0.625rem0.375rem 0.75rem
Text0.5rem10px0.75rem

Card

A surface. Surface thickness, surface radius, rest elevation. The fill is the resting wash. With no slot, the solid is plain and the text is the plain content color. With a slot, the text is the surface content color.

SmallMediumLarge
Padding0.75rem 1rem1rem 1.25rem1.25rem 1.5rem
Text0.875rem1rem1.25rem

Modal

An overlay. The shell covers the viewport and centers the panel. The veil is Neutral 950 at 40%. The shell has no fill and no focus outline.

The panel is a surface with surface thickness, surface radius, and the overlay elevation. It is at most 28rem wide, with 1rem of space on each side of the viewport. The fill is the resting wash. With no slot, the solid is plain and the text is the plain content color. With a slot, the text is the surface content color. The bump button sits in the panel.

SmallMediumLarge
Panel padding1rem1.5rem2rem

Message

The caption under a field. It is not an error style. It has its own slot, independent of the field. With no slot, the color is the plain content color. With a slot, the color is the solid. The gap above the message is 0.375rem.

SmallMediumLarge
Text0.625rem0.75rem0.875rem

Implementation notes

Small, medium, and large use -sm, -md, and -lg on the component class: btn-sm, btn-md, input-lg. A control with no size class is medium. btn-md repeats that medium size so a breakpoint can return to it, as in btn-lg md:btn-md. The icon button and the bump button use the button sizes, btn-sm, btn-md, and btn-lg.

The faces that receive squish, grain, and blur are btn, btn-icon, input, textarea, select, checkbox-box, radio-ring, toggle-track, toggle-thumb, range-track, range-fill, range-thumb, alert, badge, card, and modal-panel. The radio bullet, radio-ring::after, receives the grain only. Its color is at the material opacity. It has no blur and no inset. Any other element opts in with noise.

Start and end slots are input-start and input-end inside both input and textarea. The select chevron is picker-icon. The built-in chevron is a masked element. A custom icon is an SVG with the same class.

Light

Button
Icon button
Bump button
Input
Textarea
Select
Checkbox
Radio
Toggle
Range
Alert

Small

Medium

Large

Plain

Badge
SmallMediumLargePlain
Card

Small

Medium

Large

Plain

Modal
Message

Small

Medium

Large

Plain

Dark

Button
Icon button
Bump button
Input
Textarea
Select
Checkbox
Radio
Toggle
Range
Alert

Small

Medium

Large

Plain

Badge
SmallMediumLargePlain
Card

Small

Medium

Large

Plain

Modal
Message

Small

Medium

Large

Plain

13. Customization

A product may change:

  • The eight solids.
  • The eight content colors.
  • The eight surface content colors.
  • The surface, field, control, and mark radii.
  • The material opacity.
  • The material blur.
  • The noise tile size.
  • The elevation multiplier.

A product may not change, through the public contract:

  • The thickness recipes.
  • The press-light, field-light, bump, and loading-light mixes.
  • The wash percentages.
  • The relationship between rest, hover, and disabled elevation. The multiplier scales them together.
  • The typeface. The app's font is inherited.

Changing a solid without changing its content color or its surface content color leaves the previous text colors in place.

Implementation notes

Public custom properties: one solid, one content color, and one surface content color per slot, --radius-surface, --radius-field, --radius-control, --radius-mark, one opacity, one blur, one noise size, and one shadow multiplier defaulting to 1.

--color-black and --color-white are shared mix values. They are not part of this contract.

Shade, highlight, wash, the fills, the mark lift, the resting label, menu mixes, thickness, and drop shadows stay internal variables.

Light

Default
Default
Teal
Solid changes. Text color stays.
Sharp
Radius 0.25rem
Sheer
Opacity 20%
Lift
Elevation 2

Dark

Default
Default
Teal
Solid changes. Text color stays.
Sharp
Radius 0.25rem
Sheer
Opacity 20%
Lift
Elevation 2

14. Library contract

Jellyfy styles its own components and anything that opts into a role or into squish. It does not set the page font, and it does not require Tailwind. Defaults follow Tailwind's color variables, spacing variable, and the neutral ramp when they exist.

Class names are btn, btn-icon, btn-bump, input, textarea, select, checkbox, radio, toggle, range, alert, badge, card, modal, modal-panel, message, and noise. Slots and sizes are extra classes on the same element. noise adds the grain alone.

Light

Plain
No role
Control
Opted into the control role
Opted into squish

Dark

Plain
No role
Control
Opted into the control role
Opted into squish