Theme
Version
GitHub PyPI Discord
On this page

Component JS and CSS

A component often needs a little browser code or a few styles of its own: a panel that opens and closes, a chart that draws itself, a banner in a color the page chooses. Put that code on the component, in js and css. Citry adds it to every page that renders the component, once per page however many times the component appears.

This page covers code that belongs to one component. To add a library or a shared file, see Dependency files. For the order scripts run in and what happens when one fails, see When scripts run. To choose where the tags go in the page, see Place JavaScript and CSS. For the Vue attributes you write in templates, such as @click and v-show, see Vue in templates, and for the options the js passes to Vue, see Component options.

js and css attributes

from citry import Component

class Disclosure(Component):
    template = """
      <section class="disclosure">
        <button type="button" @click="toggle">
          Details
        </button>
        <div v-show="open">
          <c-slot />
        </div>
      </section>
    """

    js = """
      $component({
        data() {
          return { open: false };
        },
        methods: {
          toggle() {
            this.open = !this.open;
          },
        },
      });
    """

    css = """
      .disclosure {
        border: 1px solid #d0d5dd;
      }
    """

Python renders the HTML on the server. In the browser, each Disclosure on the page becomes a Vue component with its own open value, and the CSS styles all of them.

Component JavaScript

The code in js runs in the browser. It calls $component({...}) once, at its top level, with the same options as a Vue component. Component options covers what goes there:

For props and events between components, see Props and events. Where each value goes shows which Python value reaches the template, the JavaScript, and the CSS.

JS data must be JSON

Citry sends the values that js_data() returns to the browser as JSON. Each value must be one of:

  • str, int, bool, or None;
  • a float that is finite (NaN and infinity raise ValueError);
  • a list or tuple (sent as a list) of these values;
  • a dict with str keys and these values.

The top-level keys must be str too. Any other value raises an error when you turn the render into HTML by calling str() or .serialize() on it. That includes datetime, Decimal, set, enum members, subclasses of str such as Markup, and dataclass or Pydantic instances nested inside the data. The error names the component and the type:

TypeError: js_data() of component 'Sparkline_c92051' returned an
unsupported value: data sent to the browser must contain only dicts,
lists, strings, numbers, booleans, and None, got datetime.

Convert such values in js_data() first:

def js_data(self, kwargs: Kwargs, slots):
    return {
        "updatedAt": kwargs.updated_at.isoformat(),
        "total": str(kwargs.total),  # a Decimal
        "task": dataclasses.asdict(kwargs.task),
        "user": kwargs.user.model_dump(mode="json"),
    }

Returning a Secret raises the same TypeError, and the error names the key that holds it. Leave it out of js_data() and read it on the server; see Secrets.

Returning a dataclass or Pydantic instance as the whole result works: Citry turns its fields into the top-level keys. Values inside those fields must still follow the rules above.

JavaScript numbers lose precision above 2^53, so send large IDs as strings.

Send values to CSS

Return a mapping from css_data() to give one render's values to its CSS. Each key becomes a CSS custom property that you read with var(--<key>):

from citry import Component

class Banner(Component):
    class Kwargs:
        color: str = "#fde68a"

    def css_data(self, kwargs: Kwargs, slots):
        return {"banner_color": kwargs.color}

    template = """
      <div class="banner">
        <c-slot />
      </div>
    """

    css = """
      .banner {
        background: var(--banner_color);
      }
    """

Citry marks the component's root element with a generated attribute and sets the property only on the elements that carry it. Simplified, Banner() renders:

<style>
  .banner {
    background: var(--banner_color);
  }
</style>
<style>
  [data-ccss-b097b034] {
    --banner_color: #fde68a;
  }
</style>
<div class="banner" data-ccss-b097b034></div>

So the banner's background is #fde68a. Citry generates the attribute name from the values, so do not write selectors against it. A Banner(color="#bfdbfe") on the same page gets a different attribute and its own color. Renders with the same values share one attribute and one stylesheet.

The component needs its own css for this to work. Without it, Citry drops the css_data() values without an error.

Each value must be a string, a finite number (not a boolean), or None, which leaves the property empty. Any other value, such as a bool or a list, raises ValueError when the component renders. A key must be a string that is valid as the name of a custom property, without the leading --.

A string value with whitespace is sent as one quoted CSS string, unless it starts with a CSS function such as calc(...) or rgba(...), so it cannot hold a font list or a keyword such as !important. Any other string is used as written, and Citry raises ValueError when it has:

  • a ; or ! that is not inside brackets such as calc(...);
  • a brace, a comment, or a < or backslash outside a string or url();
  • a control character (an invisible ASCII character such as U+0001) other than tab, newline, carriage return and form feed, outside a quoted string;
  • an unbalanced bracket;
  • a malformed string or url().

Remove a control character from the text, or keep it inside a quoted string.

A value containing a null or surrogate character, `